mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-04-01 01:20:31 -04:00
Merge from vscode 8aa90d444f5d051984e8055f547c4252d53479b3 (#5587)
* Merge from vscode 8aa90d444f5d051984e8055f547c4252d53479b3 * pipeline errors * fix build
This commit is contained in:
@@ -24,7 +24,7 @@ import { IViewContainersRegistry, ViewContainer, Extensions as ViewContainerExte
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks';
|
||||
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
|
||||
import { IOutputService } from 'vs/workbench/contrib/output/common/output';
|
||||
import { ITerminalConfiguration } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
|
||||
export const VIEWLET_ID = 'workbench.view.debug';
|
||||
export const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID);
|
||||
@@ -111,7 +111,7 @@ export interface IExpression extends IReplElement, IExpressionContainer {
|
||||
}
|
||||
|
||||
export interface IDebugger {
|
||||
createDebugAdapter(session: IDebugSession, outputService: IOutputService): Promise<IDebugAdapter>;
|
||||
createDebugAdapter(session: IDebugSession): Promise<IDebugAdapter>;
|
||||
runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise<number | undefined>;
|
||||
getCustomTelemetryService(): Promise<TelemetryService | undefined>;
|
||||
}
|
||||
@@ -573,13 +573,7 @@ export interface ITerminalSettings {
|
||||
osxExec: string,
|
||||
linuxExec: string
|
||||
};
|
||||
integrated: {
|
||||
shell: {
|
||||
osx: string,
|
||||
windows: string,
|
||||
linux: string
|
||||
}
|
||||
};
|
||||
integrated: ITerminalConfiguration;
|
||||
}
|
||||
|
||||
export interface IConfigurationManager {
|
||||
@@ -609,7 +603,6 @@ export interface IConfigurationManager {
|
||||
|
||||
activateDebuggers(activationEvent: string, debugType?: string): Promise<void>;
|
||||
|
||||
needsToRunInExtHost(debugType: string): boolean;
|
||||
hasDebugConfigurationProvider(debugType: string): boolean;
|
||||
|
||||
registerDebugConfigurationProvider(debugConfigurationProvider: IDebugConfigurationProvider): IDisposable;
|
||||
@@ -618,9 +611,6 @@ export interface IConfigurationManager {
|
||||
registerDebugAdapterDescriptorFactory(debugAdapterDescriptorFactory: IDebugAdapterDescriptorFactory): IDisposable;
|
||||
unregisterDebugAdapterDescriptorFactory(debugAdapterDescriptorFactory: IDebugAdapterDescriptorFactory): void;
|
||||
|
||||
registerDebugAdapterTrackerFactory(debugAdapterTrackerFactory: IDebugAdapterTrackerFactory): IDisposable;
|
||||
unregisterDebugAdapterTrackerFactory(debugAdapterTrackerFactory: IDebugAdapterTrackerFactory): void;
|
||||
|
||||
resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: any): Promise<any>;
|
||||
getDebugAdapterDescriptor(session: IDebugSession): Promise<IAdapterDescriptor | undefined>;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, ITerminalSettings, ITerminalLauncher, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IDebugAdapterTrackerFactory, IDebugService } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, ITerminalSettings, ITerminalLauncher, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IDebugService } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { Debugger } from 'vs/workbench/contrib/debug/node/debugger';
|
||||
import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
@@ -52,7 +52,6 @@ export class ConfigurationManager implements IConfigurationManager {
|
||||
private _onDidSelectConfigurationName = new Emitter<void>();
|
||||
private configProviders: IDebugConfigurationProvider[];
|
||||
private adapterDescriptorFactories: IDebugAdapterDescriptorFactory[];
|
||||
private adapterTrackerFactories: IDebugAdapterTrackerFactory[];
|
||||
private debugAdapterFactories: Map<string, IDebugAdapterFactory>;
|
||||
private terminalLauncher: ITerminalLauncher;
|
||||
private debugConfigurationTypeContext: IContextKey<string>;
|
||||
@@ -72,7 +71,6 @@ export class ConfigurationManager implements IConfigurationManager {
|
||||
) {
|
||||
this.configProviders = [];
|
||||
this.adapterDescriptorFactories = [];
|
||||
this.adapterTrackerFactories = [];
|
||||
this.debuggers = [];
|
||||
this.toDispose = [];
|
||||
this.registerListeners(lifecycleService);
|
||||
@@ -164,24 +162,6 @@ export class ConfigurationManager implements IConfigurationManager {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// debug adapter trackers
|
||||
|
||||
registerDebugAdapterTrackerFactory(debugAdapterTrackerFactory: IDebugAdapterTrackerFactory): IDisposable {
|
||||
this.adapterTrackerFactories.push(debugAdapterTrackerFactory);
|
||||
return {
|
||||
dispose: () => {
|
||||
this.unregisterDebugAdapterTrackerFactory(debugAdapterTrackerFactory);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
unregisterDebugAdapterTrackerFactory(debugAdapterTrackerFactory: IDebugAdapterTrackerFactory): void {
|
||||
const ix = this.adapterTrackerFactories.indexOf(debugAdapterTrackerFactory);
|
||||
if (ix >= 0) {
|
||||
this.adapterTrackerFactories.splice(ix, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// debug configurations
|
||||
|
||||
registerDebugConfigurationProvider(debugConfigurationProvider: IDebugConfigurationProvider): IDisposable {
|
||||
@@ -206,13 +186,6 @@ export class ConfigurationManager implements IConfigurationManager {
|
||||
return providers.length > 0;
|
||||
}
|
||||
|
||||
needsToRunInExtHost(debugType: string): boolean {
|
||||
|
||||
// if the given debugType matches any registered tracker factory we need to run the DA in the EH
|
||||
const providers = this.adapterTrackerFactories.filter(p => p.type === debugType || p.type === '*');
|
||||
return providers.length > 0;
|
||||
}
|
||||
|
||||
resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: IConfig): Promise<IConfig | null | undefined> {
|
||||
return this.activateDebuggers('onDebugResolve', type).then(() => {
|
||||
// pipe the config through the promises sequentially. Append at the end the '*' types
|
||||
|
||||
@@ -25,7 +25,6 @@ import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { normalizeDriveLetter } from 'vs/base/common/labels';
|
||||
import { IOutputService } from 'vs/workbench/contrib/output/common/output';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
@@ -62,7 +61,6 @@ export class DebugSession implements IDebugSession {
|
||||
private _parentSession: IDebugSession | undefined,
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IOutputService private readonly outputService: IOutputService,
|
||||
@IWindowService private readonly windowService: IWindowService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IViewletService private readonly viewletService: IViewletService,
|
||||
@@ -167,7 +165,7 @@ export class DebugSession implements IDebugSession {
|
||||
|
||||
return dbgr.getCustomTelemetryService().then(customTelemetryService => {
|
||||
|
||||
return dbgr.createDebugAdapter(this, this.outputService).then(debugAdapter => {
|
||||
return dbgr.createDebugAdapter(this).then(debugAdapter => {
|
||||
|
||||
this.raw = new RawDebugSession(debugAdapter, dbgr, this.telemetryService, customTelemetryService, this.environmentService);
|
||||
|
||||
|
||||
@@ -11,11 +11,8 @@ import { isObject } from 'vs/base/common/types';
|
||||
import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc';
|
||||
import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema';
|
||||
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { IConfig, IDebuggerContribution, IDebugAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, ITerminalSettings, IDebugger, IDebugSession, IAdapterDescriptor, IDebugAdapterServer } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IConfig, IDebuggerContribution, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, ITerminalSettings, IDebugger, IDebugSession } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IOutputService } from 'vs/workbench/contrib/output/common/output';
|
||||
import { ExecutableDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter';
|
||||
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
|
||||
import * as ConfigurationResolverUtils from 'vs/workbench/services/configurationResolver/common/configurationResolverUtils';
|
||||
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
|
||||
@@ -38,7 +35,6 @@ export class Debugger implements IDebugger {
|
||||
constructor(private configurationManager: IConfigurationManager, dbgContribution: IDebuggerContribution, extensionDescription: IExtensionDescription,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@ITextResourcePropertiesService private readonly resourcePropertiesService: ITextResourcePropertiesService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IConfigurationResolverService private readonly configurationResolverService: IConfigurationResolverService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
) {
|
||||
@@ -97,97 +93,25 @@ export class Debugger implements IDebugger {
|
||||
}
|
||||
}
|
||||
|
||||
public createDebugAdapter(session: IDebugSession, outputService: IOutputService): Promise<IDebugAdapter> {
|
||||
public createDebugAdapter(session: IDebugSession): Promise<IDebugAdapter> {
|
||||
return this.configurationManager.activateDebuggers('onDebugAdapterProtocolTracker', this.type).then(_ => {
|
||||
if (this.inExtHost()) {
|
||||
const da = this.configurationManager.createDebugAdapter(session);
|
||||
if (da) {
|
||||
return Promise.resolve(da);
|
||||
}
|
||||
throw new Error(nls.localize('cannot.find.da', "Cannot find debug adapter for type '{0}'.", this.type));
|
||||
} else {
|
||||
return this.getAdapterDescriptor(session).then(adapterDescriptor => {
|
||||
switch (adapterDescriptor.type) {
|
||||
case 'executable':
|
||||
return new ExecutableDebugAdapter(adapterDescriptor, this.type, outputService);
|
||||
case 'server':
|
||||
return new SocketDebugAdapter(adapterDescriptor);
|
||||
case 'implementation':
|
||||
// TODO@AW: this.inExtHost() should now return true
|
||||
return Promise.resolve(this.configurationManager.createDebugAdapter(session));
|
||||
default:
|
||||
throw new Error('unknown descriptor type');
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err && err.message) {
|
||||
throw new Error(nls.localize('cannot.create.da.with.err', "Cannot create debug adapter ({0}).", err.message));
|
||||
} else {
|
||||
throw new Error(nls.localize('cannot.create.da', "Cannot create debug adapter."));
|
||||
}
|
||||
});
|
||||
const da = this.configurationManager.createDebugAdapter(session);
|
||||
if (da) {
|
||||
return Promise.resolve(da);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getAdapterDescriptor(session: IDebugSession): Promise<IAdapterDescriptor> {
|
||||
|
||||
// a "debugServer" attribute in the launch config takes precedence
|
||||
if (typeof session.configuration.debugServer === 'number') {
|
||||
return Promise.resolve(<IDebugAdapterServer>{
|
||||
type: 'server',
|
||||
port: session.configuration.debugServer
|
||||
});
|
||||
}
|
||||
|
||||
// try the new "createDebugAdapterDescriptor" and the deprecated "provideDebugAdapter" API
|
||||
return this.configurationManager.getDebugAdapterDescriptor(session).then(adapter => {
|
||||
|
||||
if (adapter) {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
// try deprecated command based extension API "adapterExecutableCommand" to determine the executable
|
||||
if (this.debuggerContribution.adapterExecutableCommand) {
|
||||
console.info('debugAdapterExecutable attribute in package.json is deprecated and support for it will be removed soon; please use DebugAdapterDescriptorFactory.createDebugAdapterDescriptor instead.');
|
||||
const rootFolder = session.root ? session.root.uri.toString() : undefined;
|
||||
return this.commandService.executeCommand<IDebugAdapterExecutable>(this.debuggerContribution.adapterExecutableCommand, rootFolder).then(ae => {
|
||||
if (ae) {
|
||||
return <IAdapterDescriptor>{
|
||||
type: 'executable',
|
||||
command: ae.command,
|
||||
args: ae.args || []
|
||||
};
|
||||
}
|
||||
throw new Error('command adapterExecutableCommand did not return proper command.');
|
||||
});
|
||||
}
|
||||
|
||||
// fallback: use executable information from package.json
|
||||
const ae = ExecutableDebugAdapter.platformAdapterExecutable(this.mergedExtensionDescriptions, this.type);
|
||||
if (ae === undefined) {
|
||||
throw new Error('no executable specified in package.json');
|
||||
}
|
||||
return ae;
|
||||
throw new Error(nls.localize('cannot.find.da', "Cannot find debug adapter for type '{0}'.", this.type));
|
||||
});
|
||||
}
|
||||
|
||||
substituteVariables(folder: IWorkspaceFolder | undefined, config: IConfig): Promise<IConfig> {
|
||||
if (this.inExtHost()) {
|
||||
return this.configurationManager.substituteVariables(this.type, folder, config).then(config => {
|
||||
return this.configurationResolverService.resolveWithInteractionReplace(folder, config, 'launch', this.variables);
|
||||
});
|
||||
} else {
|
||||
return this.configurationManager.substituteVariables(this.type, folder, config).then(config => {
|
||||
return this.configurationResolverService.resolveWithInteractionReplace(folder, config, 'launch', this.variables);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise<number | undefined> {
|
||||
const config = this.configurationService.getValue<ITerminalSettings>('terminal');
|
||||
return this.configurationManager.runInTerminal(this.inExtHost() ? this.type : '*', args, config);
|
||||
}
|
||||
|
||||
private inExtHost(): boolean {
|
||||
return true;
|
||||
return this.configurationManager.runInTerminal(this.type, args, config);
|
||||
}
|
||||
|
||||
get label(): string {
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as pfs from 'vs/base/node/pfs';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { getDefaultShell } from 'vs/workbench/contrib/terminal/node/terminal';
|
||||
|
||||
const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console");
|
||||
|
||||
@@ -314,13 +315,13 @@ export function prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments
|
||||
let shell: string;
|
||||
const shell_config = config.integrated.shell;
|
||||
if (env.isWindows) {
|
||||
shell = shell_config.windows;
|
||||
shell = shell_config.windows || getDefaultShell(env.Platform.Windows);
|
||||
shellType = ShellType.cmd;
|
||||
} else if (env.isLinux) {
|
||||
shell = shell_config.linux;
|
||||
shell = shell_config.linux || getDefaultShell(env.Platform.Linux);
|
||||
shellType = ShellType.bash;
|
||||
} else if (env.isMacintosh) {
|
||||
shell = shell_config.osx;
|
||||
shell = shell_config.osx || getDefaultShell(env.Platform.Mac);
|
||||
shellType = ShellType.bash;
|
||||
} else {
|
||||
throw new Error('Unknown platform');
|
||||
|
||||
@@ -14,7 +14,7 @@ import { DebugSession } from 'vs/workbench/contrib/debug/electron-browser/debugS
|
||||
import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel';
|
||||
|
||||
function createMockSession(model: DebugModel, name = 'mockSession', parentSession?: DebugSession | undefined): DebugSession {
|
||||
return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, parentSession, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!);
|
||||
return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, parentSession, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!);
|
||||
}
|
||||
|
||||
suite('Debug - Model', () => {
|
||||
@@ -436,7 +436,7 @@ suite('Debug - Model', () => {
|
||||
// Repl output
|
||||
|
||||
test('repl output', () => {
|
||||
const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!);
|
||||
const session = new DebugSession({ resolved: { name: 'mockSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!);
|
||||
const repl = new ReplModel(session);
|
||||
repl.appendToRepl('first line\n', severity.Error);
|
||||
repl.appendToRepl('second line ', severity.Error);
|
||||
|
||||
@@ -130,7 +130,7 @@ suite('Debug - Debugger', () => {
|
||||
const testResourcePropertiesService = new TestTextResourcePropertiesService(configurationService);
|
||||
|
||||
setup(() => {
|
||||
_debugger = new Debugger(configurationManager, debuggerContribution, extensionDescriptor0, configurationService, testResourcePropertiesService, undefined!, undefined!, undefined!);
|
||||
_debugger = new Debugger(configurationManager, debuggerContribution, extensionDescriptor0, configurationService, testResourcePropertiesService, undefined!, undefined!);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
|
||||
@@ -19,6 +19,8 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib
|
||||
import { IAsyncDataSource, ITreeNode } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
|
||||
export interface IExtensionTemplateData {
|
||||
icon: HTMLImageElement;
|
||||
@@ -217,4 +219,46 @@ export class ExtensionsTree extends WorkbenchAsyncDataTree<IExtensionData, IExte
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtensionData implements IExtensionData {
|
||||
|
||||
readonly extension: IExtension;
|
||||
readonly parent: IExtensionData | null;
|
||||
private readonly getChildrenExtensionIds: (extension: IExtension) => string[];
|
||||
private readonly childrenExtensionIds: string[];
|
||||
private readonly extensionsWorkbenchService: IExtensionsWorkbenchService;
|
||||
|
||||
constructor(extension: IExtension, parent: IExtensionData | null, getChildrenExtensionIds: (extension: IExtension) => string[], extensionsWorkbenchService: IExtensionsWorkbenchService) {
|
||||
this.extension = extension;
|
||||
this.parent = parent;
|
||||
this.getChildrenExtensionIds = getChildrenExtensionIds;
|
||||
this.extensionsWorkbenchService = extensionsWorkbenchService;
|
||||
this.childrenExtensionIds = this.getChildrenExtensionIds(extension);
|
||||
}
|
||||
|
||||
get hasChildren(): boolean {
|
||||
return isNonEmptyArray(this.childrenExtensionIds);
|
||||
}
|
||||
|
||||
async getChildren(): Promise<IExtensionData[] | null> {
|
||||
if (this.hasChildren) {
|
||||
const localById = this.extensionsWorkbenchService.local.reduce((result, e) => { result.set(e.identifier.id.toLowerCase(), e); return result; }, new Map<string, IExtension>());
|
||||
const result: IExtension[] = [];
|
||||
const toQuery: string[] = [];
|
||||
for (const extensionId of this.childrenExtensionIds) {
|
||||
const id = extensionId.toLowerCase();
|
||||
const local = localById.get(id);
|
||||
if (local) {
|
||||
result.push(local);
|
||||
} else {
|
||||
toQuery.push(id);
|
||||
}
|
||||
}
|
||||
const galleryResult = await this.extensionsWorkbenchService.queryGallery({ names: this.childrenExtensionIds, pageSize: this.childrenExtensionIds.length }, CancellationToken.None);
|
||||
result.push(...galleryResult.firstPage);
|
||||
return result.map(extension => new ExtensionData(extension, this, this.getChildrenExtensionIds, this.extensionsWorkbenchService));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -69,14 +69,6 @@ export interface IExtension {
|
||||
readonly isMalicious: boolean;
|
||||
}
|
||||
|
||||
export interface IExtensionDependencies {
|
||||
dependencies: IExtensionDependencies[];
|
||||
hasDependencies: boolean;
|
||||
identifier: string;
|
||||
extension: IExtension;
|
||||
dependent: IExtensionDependencies | null;
|
||||
}
|
||||
|
||||
export const SERVICE_ID = 'extensionsWorkbenchService';
|
||||
|
||||
export const IExtensionsWorkbenchService = createDecorator<IExtensionsWorkbenchService>(SERVICE_ID);
|
||||
@@ -97,7 +89,6 @@ export interface IExtensionsWorkbenchService {
|
||||
installVersion(extension: IExtension, version: string): Promise<IExtension>;
|
||||
reinstall(extension: IExtension): Promise<IExtension>;
|
||||
setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise<void>;
|
||||
loadDependencies(extension: IExtension, token: CancellationToken): Promise<IExtensionDependencies | null>;
|
||||
open(extension: IExtension, sideByside?: boolean): Promise<any>;
|
||||
checkForUpdates(): Promise<void>;
|
||||
allowedBadgeProviders: string[];
|
||||
|
||||
@@ -24,7 +24,7 @@ import { IExtensionTipsService } from 'vs/platform/extensionManagement/common/ex
|
||||
import { IExtensionManifest, IKeyBinding, IView, IViewContainer, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
|
||||
import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, IExtension, IExtensionDependencies, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, IExtension, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/electron-browser/extensionsWidgets';
|
||||
import { EditorOptions } from 'vs/workbench/common/editor';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
@@ -43,13 +43,13 @@ import { Color } from 'vs/base/common/color';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ExtensionsTree, IExtensionData } from 'vs/workbench/contrib/extensions/browser/extensionsViewer';
|
||||
import { ExtensionsTree, ExtensionData } from 'vs/workbench/contrib/extensions/browser/extensionsViewer';
|
||||
import { ShowCurrentReleaseNotesAction } from 'vs/workbench/contrib/update/electron-browser/update';
|
||||
import { KeybindingParser } from 'vs/base/common/keybindingParser';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { getDefaultValue } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { isUndefined, withUndefinedAsNull } from 'vs/base/common/types';
|
||||
import { isUndefined } from 'vs/base/common/types';
|
||||
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
@@ -181,7 +181,6 @@ export class ExtensionEditor extends BaseEditor {
|
||||
private extensionReadme: Cache<string> | null;
|
||||
private extensionChangelog: Cache<string> | null;
|
||||
private extensionManifest: Cache<IExtensionManifest | null> | null;
|
||||
private extensionDependencies: Cache<IExtensionDependencies | null> | null;
|
||||
|
||||
private layoutParticipants: ILayoutParticipant[] = [];
|
||||
private contentDisposables: IDisposable[] = [];
|
||||
@@ -210,7 +209,6 @@ export class ExtensionEditor extends BaseEditor {
|
||||
this.extensionReadme = null;
|
||||
this.extensionChangelog = null;
|
||||
this.extensionManifest = null;
|
||||
this.extensionDependencies = null;
|
||||
}
|
||||
|
||||
createEditor(parent: HTMLElement): void {
|
||||
@@ -298,7 +296,6 @@ export class ExtensionEditor extends BaseEditor {
|
||||
this.extensionReadme = new Cache(() => createCancelablePromise(token => extension.getReadme(token)));
|
||||
this.extensionChangelog = new Cache(() => createCancelablePromise(token => extension.getChangelog(token)));
|
||||
this.extensionManifest = new Cache(() => createCancelablePromise(token => extension.getManifest(token)));
|
||||
this.extensionDependencies = new Cache(() => createCancelablePromise(token => this.extensionsWorkbenchService.loadDependencies(extension, token)));
|
||||
|
||||
const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, this.iconContainer, true);
|
||||
const onError = Event.once(domEvent(this.icon, 'error'));
|
||||
@@ -643,69 +640,28 @@ export class ExtensionEditor extends BaseEditor {
|
||||
}
|
||||
|
||||
private openDependencies(extension: IExtension): Promise<IActiveElement> {
|
||||
if (extension.dependencies.length === 0) {
|
||||
if (arrays.isFalsyOrEmpty(extension.dependencies)) {
|
||||
append(this.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies");
|
||||
return Promise.resolve(this.content);
|
||||
}
|
||||
|
||||
return this.loadContents(() => this.extensionDependencies!.get())
|
||||
.then<IActiveElement, IActiveElement>(extensionDependencies => {
|
||||
if (extensionDependencies) {
|
||||
const content = $('div', { class: 'subcontent' });
|
||||
const scrollableContent = new DomScrollableElement(content, {});
|
||||
append(this.content, scrollableContent.getDomNode());
|
||||
this.contentDisposables.push(scrollableContent);
|
||||
const content = $('div', { class: 'subcontent' });
|
||||
const scrollableContent = new DomScrollableElement(content, {});
|
||||
append(this.content, scrollableContent.getDomNode());
|
||||
this.contentDisposables.push(scrollableContent);
|
||||
|
||||
const dependenciesTree = this.renderDependencies(content, extensionDependencies);
|
||||
const layout = () => {
|
||||
scrollableContent.scanDomNode();
|
||||
const scrollDimensions = scrollableContent.getScrollDimensions();
|
||||
dependenciesTree.layout(scrollDimensions.height);
|
||||
};
|
||||
const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout });
|
||||
this.contentDisposables.push(toDisposable(removeLayoutParticipant));
|
||||
const dependenciesTree = this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extension, null, extension => extension.dependencies || [], this.extensionsWorkbenchService), content);
|
||||
const layout = () => {
|
||||
scrollableContent.scanDomNode();
|
||||
const scrollDimensions = scrollableContent.getScrollDimensions();
|
||||
dependenciesTree.layout(scrollDimensions.height);
|
||||
};
|
||||
const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout });
|
||||
this.contentDisposables.push(toDisposable(removeLayoutParticipant));
|
||||
|
||||
this.contentDisposables.push(dependenciesTree);
|
||||
scrollableContent.scanDomNode();
|
||||
return { focus() { dependenciesTree.domFocus(); } };
|
||||
} else {
|
||||
append(this.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies");
|
||||
return Promise.resolve(this.content);
|
||||
}
|
||||
}, error => {
|
||||
append(this.content, $('p.nocontent')).textContent = error;
|
||||
this.notificationService.error(error);
|
||||
return this.content;
|
||||
});
|
||||
}
|
||||
|
||||
private renderDependencies(container: HTMLElement, extensionDependencies: IExtensionDependencies): ExtensionsTree {
|
||||
class ExtensionData implements IExtensionData {
|
||||
|
||||
private readonly extensionDependencies: IExtensionDependencies;
|
||||
|
||||
constructor(extensionDependencies: IExtensionDependencies) {
|
||||
this.extensionDependencies = extensionDependencies;
|
||||
}
|
||||
|
||||
get extension(): IExtension {
|
||||
return this.extensionDependencies.extension;
|
||||
}
|
||||
|
||||
get parent(): IExtensionData | null {
|
||||
return this.extensionDependencies.dependent ? new ExtensionData(this.extensionDependencies.dependent) : null;
|
||||
}
|
||||
|
||||
get hasChildren(): boolean {
|
||||
return this.extensionDependencies.hasDependencies;
|
||||
}
|
||||
|
||||
getChildren(): Promise<IExtensionData[] | null> {
|
||||
return this.extensionDependencies.dependencies ? Promise.resolve(this.extensionDependencies.dependencies.map(d => new ExtensionData(d))) : Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
return this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extensionDependencies), container);
|
||||
this.contentDisposables.push(dependenciesTree);
|
||||
scrollableContent.scanDomNode();
|
||||
return Promise.resolve({ focus() { dependenciesTree.domFocus(); } });
|
||||
}
|
||||
|
||||
private openExtensionPack(extension: IExtension): Promise<IActiveElement> {
|
||||
@@ -714,7 +670,7 @@ export class ExtensionEditor extends BaseEditor {
|
||||
append(this.content, scrollableContent.getDomNode());
|
||||
this.contentDisposables.push(scrollableContent);
|
||||
|
||||
const extensionsPackTree = this.renderExtensionPack(content, extension);
|
||||
const extensionsPackTree = this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extension, null, extension => extension.extensionPack || [], this.extensionsWorkbenchService), content);
|
||||
const layout = () => {
|
||||
scrollableContent.scanDomNode();
|
||||
const scrollDimensions = scrollableContent.getScrollDimensions();
|
||||
@@ -728,35 +684,6 @@ export class ExtensionEditor extends BaseEditor {
|
||||
return Promise.resolve({ focus() { extensionsPackTree.domFocus(); } });
|
||||
}
|
||||
|
||||
private renderExtensionPack(container: HTMLElement, extension: IExtension): ExtensionsTree {
|
||||
const extensionsWorkbenchService = this.extensionsWorkbenchService;
|
||||
class ExtensionData implements IExtensionData {
|
||||
|
||||
readonly extension: IExtension;
|
||||
readonly parent: IExtensionData | null;
|
||||
|
||||
constructor(extension: IExtension, parent?: IExtensionData) {
|
||||
this.extension = extension;
|
||||
this.parent = withUndefinedAsNull(parent);
|
||||
}
|
||||
|
||||
get hasChildren(): boolean {
|
||||
return this.extension.extensionPack.length > 0;
|
||||
}
|
||||
|
||||
getChildren(): Promise<IExtensionData[] | null> {
|
||||
if (this.hasChildren) {
|
||||
const names = arrays.distinct(this.extension.extensionPack, e => e.toLowerCase());
|
||||
return extensionsWorkbenchService.queryGallery({ names, pageSize: names.length }, CancellationToken.None)
|
||||
.then(result => result.firstPage.map(extension => new ExtensionData(extension, this)));
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
return this.instantiationService.createInstance(ExtensionsTree, new ExtensionData(extension), container);
|
||||
}
|
||||
|
||||
private renderSettings(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean {
|
||||
const contributes = manifest.contributes;
|
||||
const configuration = contributes && contributes.configuration;
|
||||
|
||||
@@ -1558,7 +1558,7 @@ export class InstallExtensionsAction extends OpenExtensionsViewletAction {
|
||||
export class ShowEnabledExtensionsAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.extensions.action.showEnabledExtensions';
|
||||
static LABEL = localize('showEnabledExtensions', 'Show Enabled Extensions');
|
||||
static LABEL = localize('showEnabledExtensions', "Show Enabled Extensions");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
|
||||
@@ -23,8 +23,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
// {{SQL CARBON EDIT}}
|
||||
import { IExtension, IExtensionDependencies, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IURLService, IURLHandler } from 'vs/platform/url/common/url';
|
||||
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
|
||||
@@ -323,53 +322,6 @@ ${this.description}
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionDependencies implements IExtensionDependencies {
|
||||
|
||||
private _hasDependencies: boolean | null = null;
|
||||
|
||||
constructor(private _extension: IExtension, private _identifier: string, private _map: Map<string, IExtension>, private _dependent: IExtensionDependencies | null = null) { }
|
||||
|
||||
get hasDependencies(): boolean {
|
||||
if (this._hasDependencies === null) {
|
||||
this._hasDependencies = this.computeHasDependencies();
|
||||
}
|
||||
return this._hasDependencies;
|
||||
}
|
||||
|
||||
get extension(): IExtension {
|
||||
return this._extension;
|
||||
}
|
||||
|
||||
get identifier(): string {
|
||||
return this._identifier;
|
||||
}
|
||||
|
||||
get dependent(): IExtensionDependencies | null {
|
||||
return this._dependent;
|
||||
}
|
||||
|
||||
get dependencies(): IExtensionDependencies[] {
|
||||
if (!this.hasDependencies) {
|
||||
return [];
|
||||
}
|
||||
return this._extension.dependencies.map(id => new ExtensionDependencies(this._map.get(id)!, id, this._map, this));
|
||||
}
|
||||
|
||||
private computeHasDependencies(): boolean {
|
||||
if (this._extension && this._extension.dependencies.length > 0) {
|
||||
let dependent = this._dependent;
|
||||
while (dependent !== null) {
|
||||
if (dependent.identifier === this.identifier) {
|
||||
return false;
|
||||
}
|
||||
dependent = dependent.dependent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Extensions extends Disposable {
|
||||
|
||||
private readonly _onChange: Emitter<Extension | undefined> = new Emitter<Extension | undefined>();
|
||||
@@ -666,27 +618,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
|
||||
});
|
||||
}
|
||||
|
||||
loadDependencies(extension: IExtension, token: CancellationToken): Promise<IExtensionDependencies | null> {
|
||||
if (!extension.dependencies.length) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return this.extensionService.getExtensionsReport()
|
||||
.then(report => {
|
||||
const maliciousSet = getMaliciousExtensionsSet(report);
|
||||
|
||||
return this.galleryService.loadAllDependencies((<Extension>extension).dependencies.map(id => ({ id })), token)
|
||||
.then(galleryExtensions => {
|
||||
const extensions: IExtension[] = [...this.local, ...galleryExtensions.map(galleryExtension => this.fromGallery(galleryExtension, maliciousSet))];
|
||||
const map = new Map<string, IExtension>();
|
||||
for (const extension of extensions) {
|
||||
map.set(extension.identifier.id, extension);
|
||||
}
|
||||
return new ExtensionDependencies(extension, extension.identifier.id, map);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
open(extension: IExtension, sideByside: boolean = false): Promise<any> {
|
||||
return Promise.resolve(this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), undefined, sideByside ? SIDE_GROUP : ACTIVE_GROUP));
|
||||
}
|
||||
|
||||
@@ -480,248 +480,6 @@ suite('ExtensionsWorkbenchServiceTest', () => {
|
||||
assert.ok(target.calledOnce);
|
||||
});
|
||||
|
||||
test('test extension dependencies when empty', async () => {
|
||||
testObject = await aWorkbenchService();
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a')));
|
||||
|
||||
return testObject.queryGallery(CancellationToken.None).then(page => {
|
||||
return testObject.loadDependencies(page.firstPage[0], CancellationToken.None).then(dependencies => {
|
||||
assert.equal(null, dependencies);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('test one level extension dependencies without cycle', async () => {
|
||||
testObject = await aWorkbenchService();
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', {}, { dependencies: ['pub.b', 'pub.c', 'pub.d'] })));
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'loadAllDependencies', [aGalleryExtension('b'), aGalleryExtension('c'), aGalleryExtension('d')]);
|
||||
|
||||
return testObject.queryGallery(CancellationToken.None).then(page => {
|
||||
const extension = page.firstPage[0];
|
||||
return testObject.loadDependencies(extension, CancellationToken.None).then(actual => {
|
||||
assert.ok(actual!.hasDependencies);
|
||||
assert.equal(extension, actual!.extension);
|
||||
assert.equal(null, actual!.dependent);
|
||||
assert.equal(3, actual!.dependencies.length);
|
||||
assert.equal('pub.a', actual!.identifier);
|
||||
let dependent = actual;
|
||||
|
||||
actual = dependent!.dependencies[0];
|
||||
assert.ok(!actual.hasDependencies);
|
||||
assert.equal('pub.b', actual.extension.identifier.id);
|
||||
assert.equal('pub.b', actual.identifier);
|
||||
assert.equal(dependent, actual.dependent);
|
||||
assert.equal(0, actual.dependencies.length);
|
||||
|
||||
actual = dependent!.dependencies[1];
|
||||
assert.ok(!actual.hasDependencies);
|
||||
assert.equal('pub.c', actual.extension.identifier.id);
|
||||
assert.equal('pub.c', actual.identifier);
|
||||
assert.equal(dependent, actual.dependent);
|
||||
assert.equal(0, actual.dependencies.length);
|
||||
|
||||
actual = dependent!.dependencies[2];
|
||||
assert.ok(!actual.hasDependencies);
|
||||
assert.equal('pub.d', actual.extension.identifier.id);
|
||||
assert.equal('pub.d', actual.identifier);
|
||||
assert.equal(dependent, actual.dependent);
|
||||
assert.equal(0, actual.dependencies.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('test one level extension dependencies with cycle', async () => {
|
||||
testObject = await aWorkbenchService();
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', {}, { dependencies: ['pub.b', 'pub.a'] })));
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'loadAllDependencies', [aGalleryExtension('b'), aGalleryExtension('a')]);
|
||||
|
||||
return testObject.queryGallery(CancellationToken.None).then(page => {
|
||||
const extension = page.firstPage[0];
|
||||
return testObject.loadDependencies(extension, CancellationToken.None).then(actual => {
|
||||
assert.ok(actual!.hasDependencies);
|
||||
assert.equal(extension, actual!.extension);
|
||||
assert.equal(null, actual!.dependent);
|
||||
assert.equal(2, actual!.dependencies.length);
|
||||
assert.equal('pub.a', actual!.identifier);
|
||||
let dependent = actual;
|
||||
|
||||
actual = dependent!.dependencies[0]!;
|
||||
assert.ok(!actual.hasDependencies);
|
||||
assert.equal('pub.b', actual.extension.identifier.id);
|
||||
assert.equal('pub.b', actual.identifier);
|
||||
assert.equal(dependent, actual.dependent);
|
||||
assert.equal(0, actual.dependencies.length);
|
||||
|
||||
actual = dependent!.dependencies[1]!;
|
||||
assert.ok(!actual.hasDependencies);
|
||||
assert.equal('pub.a', actual.extension.identifier.id);
|
||||
assert.equal('pub.a', actual.identifier);
|
||||
assert.equal(dependent, actual.dependent);
|
||||
assert.equal(0, actual.dependencies.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('test one level extension dependencies with missing dependencies', async () => {
|
||||
testObject = await aWorkbenchService();
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', {}, { dependencies: ['pub.b', 'pub.a'] })));
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'loadAllDependencies', [aGalleryExtension('a')]);
|
||||
|
||||
return testObject.queryGallery(CancellationToken.None).then(page => {
|
||||
const extension = page.firstPage[0];
|
||||
return testObject.loadDependencies(extension, CancellationToken.None).then(actual => {
|
||||
assert.ok(actual!.hasDependencies);
|
||||
assert.equal(extension, actual!.extension);
|
||||
assert.equal(null, actual!.dependent);
|
||||
assert.equal(2, actual!.dependencies.length);
|
||||
assert.equal('pub.a', actual!.identifier);
|
||||
let dependent = actual;
|
||||
|
||||
actual = dependent!.dependencies[0]!;
|
||||
assert.ok(!actual.hasDependencies);
|
||||
assert.equal(null, actual.extension);
|
||||
assert.equal('pub.b', actual.identifier);
|
||||
assert.equal(dependent, actual.dependent);
|
||||
assert.equal(0, actual.dependencies.length);
|
||||
|
||||
actual = dependent!.dependencies[1]!;
|
||||
assert.ok(!actual.hasDependencies);
|
||||
assert.equal('pub.a', actual.extension.identifier.id);
|
||||
assert.equal('pub.a', actual.identifier);
|
||||
assert.equal(dependent, actual.dependent);
|
||||
assert.equal(0, actual.dependencies.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('test one level extension dependencies with in built dependencies', async () => {
|
||||
const local = aLocalExtension('inbuilt', {}, { type: ExtensionType.System });
|
||||
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
|
||||
testObject = await aWorkbenchService();
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', {}, { dependencies: ['pub.inbuilt', 'pub.a'] })));
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'loadAllDependencies', [aGalleryExtension('a')]);
|
||||
|
||||
return testObject.queryGallery(CancellationToken.None).then(page => {
|
||||
const extension = page.firstPage[0];
|
||||
return testObject.loadDependencies(extension, CancellationToken.None).then(actual => {
|
||||
assert.ok(actual!.hasDependencies);
|
||||
assert.equal(extension, actual!.extension);
|
||||
assert.equal(null, actual!.dependent);
|
||||
assert.equal(2, actual!.dependencies.length);
|
||||
assert.equal('pub.a', actual!.identifier);
|
||||
let dependent = actual;
|
||||
|
||||
actual = dependent!.dependencies[0]!;
|
||||
assert.ok(!actual.hasDependencies);
|
||||
assert.equal('pub.inbuilt', actual.extension.identifier.id);
|
||||
assert.equal('pub.inbuilt', actual.identifier);
|
||||
assert.equal(dependent, actual.dependent);
|
||||
assert.equal(0, actual.dependencies.length);
|
||||
|
||||
|
||||
actual = dependent!.dependencies[1]!;
|
||||
assert.ok(!actual.hasDependencies);
|
||||
assert.equal('pub.a', actual.extension.identifier.id);
|
||||
assert.equal('pub.a', actual.identifier);
|
||||
assert.equal(dependent, actual.dependent);
|
||||
assert.equal(0, actual.dependencies.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('test more than one level of extension dependencies', async () => {
|
||||
const local = aLocalExtension('c', { extensionDependencies: ['pub.d'] }, { type: ExtensionType.System });
|
||||
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
|
||||
testObject = await aWorkbenchService();
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', {}, { dependencies: ['pub.b', 'pub.c'] })));
|
||||
instantiationService.stubPromise(IExtensionGalleryService, 'loadAllDependencies', [
|
||||
aGalleryExtension('b', {}, { dependencies: ['pub.d', 'pub.e'] }),
|
||||
aGalleryExtension('d', {}, { dependencies: ['pub.f', 'pub.c'] }),
|
||||
aGalleryExtension('e')]);
|
||||
|
||||
return testObject.queryGallery(CancellationToken.None).then(page => {
|
||||
const extension = page.firstPage[0];
|
||||
return testObject.loadDependencies(extension, CancellationToken.None).then(a => {
|
||||
assert.ok(a!.hasDependencies);
|
||||
assert.equal(extension, a!.extension);
|
||||
assert.equal(null, a!.dependent);
|
||||
assert.equal(2, a!.dependencies.length);
|
||||
assert.equal('pub.a', a!.identifier);
|
||||
|
||||
let b = a!.dependencies[0];
|
||||
assert.ok(b.hasDependencies);
|
||||
assert.equal('pub.b', b.extension.identifier.id);
|
||||
assert.equal('pub.b', b.identifier);
|
||||
assert.equal(a, b.dependent);
|
||||
assert.equal(2, b.dependencies.length);
|
||||
|
||||
let c = a!.dependencies[1];
|
||||
assert.ok(c.hasDependencies);
|
||||
assert.equal('pub.c', c.extension.identifier.id);
|
||||
assert.equal('pub.c', c.identifier);
|
||||
assert.equal(a, c.dependent);
|
||||
assert.equal(1, c.dependencies.length);
|
||||
|
||||
let d = b.dependencies[0];
|
||||
assert.ok(d.hasDependencies);
|
||||
assert.equal('pub.d', d.extension.identifier.id);
|
||||
assert.equal('pub.d', d.identifier);
|
||||
assert.equal(b, d.dependent);
|
||||
assert.equal(2, d.dependencies.length);
|
||||
|
||||
let e = b.dependencies[1];
|
||||
assert.ok(!e.hasDependencies);
|
||||
assert.equal('pub.e', e.extension.identifier.id);
|
||||
assert.equal('pub.e', e.identifier);
|
||||
assert.equal(b, e.dependent);
|
||||
assert.equal(0, e.dependencies.length);
|
||||
|
||||
let f = d.dependencies[0];
|
||||
assert.ok(!f.hasDependencies);
|
||||
assert.equal(null, f.extension);
|
||||
assert.equal('pub.f', f.identifier);
|
||||
assert.equal(d, f.dependent);
|
||||
assert.equal(0, f.dependencies.length);
|
||||
|
||||
c = d.dependencies[1];
|
||||
assert.ok(c.hasDependencies);
|
||||
assert.equal('pub.c', c.extension.identifier.id);
|
||||
assert.equal('pub.c', c.identifier);
|
||||
assert.equal(d, c.dependent);
|
||||
assert.equal(1, c.dependencies.length);
|
||||
|
||||
d = c.dependencies[0];
|
||||
assert.ok(!d.hasDependencies);
|
||||
assert.equal('pub.d', d.extension.identifier.id);
|
||||
assert.equal('pub.d', d.identifier);
|
||||
assert.equal(c, d.dependent);
|
||||
assert.equal(0, d.dependencies.length);
|
||||
|
||||
c = a!.dependencies[1];
|
||||
d = c.dependencies[0];
|
||||
assert.ok(d.hasDependencies);
|
||||
assert.equal('pub.d', d.extension.identifier.id);
|
||||
assert.equal('pub.d', d.identifier);
|
||||
assert.equal(c, d.dependent);
|
||||
assert.equal(2, d.dependencies.length);
|
||||
|
||||
f = d.dependencies[0];
|
||||
assert.ok(!f.hasDependencies);
|
||||
assert.equal(null, f.extension);
|
||||
assert.equal('pub.f', f.identifier);
|
||||
assert.equal(d, f.dependent);
|
||||
assert.equal(0, f.dependencies.length);
|
||||
|
||||
c = d.dependencies[1];
|
||||
assert.ok(!c.hasDependencies);
|
||||
assert.equal('pub.c', c.extension.identifier.id);
|
||||
assert.equal('pub.c', c.identifier);
|
||||
assert.equal(d, c.dependent);
|
||||
assert.equal(0, c.dependencies.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('test uninstalled extensions are always enabled', async () => {
|
||||
return instantiationService.get(IExtensionEnablementService).setEnablement([aLocalExtension('b')], EnablementState.Disabled)
|
||||
.then(() => instantiationService.get(IExtensionEnablementService).setEnablement([aLocalExtension('c')], EnablementState.WorkspaceDisabled))
|
||||
|
||||
@@ -94,7 +94,7 @@ CommandsRegistry.registerCommand({
|
||||
const directoriesToOpen = distinct(stats.filter(data => data.success).map(({ stat }) => stat!.isDirectory ? stat!.resource.fsPath : paths.dirname(stat!.resource.fsPath)));
|
||||
return directoriesToOpen.map(dir => {
|
||||
if (configurationService.getValue<IExternalTerminalConfiguration>().terminal.explorerKind === 'integrated') {
|
||||
const instance = integratedTerminalService.createTerminal({ cwd: dir }, true);
|
||||
const instance = integratedTerminalService.createTerminal({ cwd: dir });
|
||||
if (instance && (resources.length === 1 || !resource || dir === resource.fsPath || dir === paths.dirname(resource.fsPath))) {
|
||||
integratedTerminalService.setActiveInstance(instance);
|
||||
integratedTerminalService.showPanel(true);
|
||||
|
||||
@@ -196,7 +196,7 @@ export const tocData: ITOCEntry = {
|
||||
]
|
||||
};
|
||||
|
||||
export const knownAcronyms = new Set();
|
||||
export const knownAcronyms = new Set<string>();
|
||||
[
|
||||
'css',
|
||||
'html',
|
||||
@@ -209,3 +209,7 @@ export const knownAcronyms = new Set();
|
||||
'id',
|
||||
'php',
|
||||
].forEach(str => knownAcronyms.add(str));
|
||||
|
||||
export const knownTermMappings = new Map<string, string>();
|
||||
knownTermMappings.set('power shell', 'PowerShell');
|
||||
knownTermMappings.set('powershell', 'PowerShell');
|
||||
|
||||
@@ -11,7 +11,7 @@ import { localize } from 'vs/nls';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { SettingsTarget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets';
|
||||
import { ITOCEntry, knownAcronyms } from 'vs/workbench/contrib/preferences/browser/settingsLayout';
|
||||
import { ITOCEntry, knownAcronyms, knownTermMappings } from 'vs/workbench/contrib/preferences/browser/settingsLayout';
|
||||
import { MODIFIED_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences';
|
||||
import { IExtensionSetting, ISearchResult, ISetting, SettingValueType } from 'vs/workbench/services/preferences/common/preferences';
|
||||
|
||||
@@ -404,8 +404,8 @@ export function settingKeyToDisplayFormat(key: string, groupId = ''): { category
|
||||
}
|
||||
|
||||
function wordifyKey(key: string): string {
|
||||
return key
|
||||
.replace(/\.([a-z0-9])/g, (match, p1) => ` › ${p1.toUpperCase()}`) // Replace dot with spaced '>'
|
||||
key = key
|
||||
.replace(/\.([a-z0-9])/g, (_, p1) => ` › ${p1.toUpperCase()}`) // Replace dot with spaced '>'
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2') // Camel case to spacing, fooBar => foo Bar
|
||||
.replace(/^[a-z]/g, match => match.toUpperCase()) // Upper casing all first letters, foo => Foo
|
||||
.replace(/\b\w+\b/g, match => { // Upper casing known acronyms
|
||||
@@ -413,6 +413,12 @@ function wordifyKey(key: string): string {
|
||||
match.toUpperCase() :
|
||||
match;
|
||||
});
|
||||
|
||||
for (let [k, v] of knownTermMappings) {
|
||||
key = key.replace(new RegExp(`\\b${k}\\b`, 'gi'), v);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
function trimCategoryForGroup(category: string, groupId: string): string {
|
||||
|
||||
@@ -1225,8 +1225,14 @@ export class SettingsEditor2 extends BaseEditor {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearFilterLinkContainer.style.display = this.viewState.tagFilters && this.viewState.tagFilters.size > 0
|
||||
? 'initial'
|
||||
: 'none';
|
||||
|
||||
if (!this.searchResultModel) {
|
||||
this.countElement.style.display = 'none';
|
||||
DOM.removeClass(this.rootElement, 'no-results');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tocTreeModel && this.tocTreeModel.settingsTreeRoot) {
|
||||
@@ -1239,9 +1245,6 @@ export class SettingsEditor2 extends BaseEditor {
|
||||
|
||||
this.countElement.style.display = 'block';
|
||||
DOM.toggleClass(this.rootElement, 'no-results', count === 0);
|
||||
this.clearFilterLinkContainer.style.display = this.viewState.tagFilters && this.viewState.tagFilters.size > 0
|
||||
? 'initial'
|
||||
: 'none';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,20 @@ suite('SettingsTree', () => {
|
||||
category: '',
|
||||
label: 'Foo'
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
settingKeyToDisplayFormat('foo.1leading.number'),
|
||||
{
|
||||
category: 'Foo › 1leading',
|
||||
label: 'Number'
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
settingKeyToDisplayFormat('foo.1Leading.number'),
|
||||
{
|
||||
category: 'Foo › 1 Leading',
|
||||
label: 'Number'
|
||||
});
|
||||
});
|
||||
|
||||
test('settingKeyToDisplayFormat - with category', () => {
|
||||
@@ -101,19 +115,21 @@ suite('SettingsTree', () => {
|
||||
category: 'Something Else',
|
||||
label: 'Etc'
|
||||
});
|
||||
});
|
||||
|
||||
test('settingKeyToDisplayFormat - known acronym/term', () => {
|
||||
assert.deepEqual(
|
||||
settingKeyToDisplayFormat('foo.1leading.number'),
|
||||
settingKeyToDisplayFormat('css.someCssSetting'),
|
||||
{
|
||||
category: 'Foo › 1leading',
|
||||
label: 'Number'
|
||||
category: 'CSS',
|
||||
label: 'Some CSS Setting'
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
settingKeyToDisplayFormat('foo.1Leading.number'),
|
||||
settingKeyToDisplayFormat('powershell.somePowerShellSetting'),
|
||||
{
|
||||
category: 'Foo › 1 Leading',
|
||||
label: 'Number'
|
||||
category: 'PowerShell',
|
||||
label: 'Some PowerShell Setting'
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme';
|
||||
|
||||
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
|
||||
import { RemoteExtensionLogFileName, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
import { MenuId, IMenuService, MenuItemAction, IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions';
|
||||
import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/contrib/output/common/output';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { DialogChannel } from 'vs/platform/dialogs/node/dialogIpc';
|
||||
import { DownloadServiceChannel } from 'vs/platform/download/node/downloadIpc';
|
||||
import { LogLevelSetterChannel } from 'vs/platform/log/node/logIpc';
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
import { IDiagnosticInfoOptions, IRemoteDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnosticsService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IProgressService2, IProgress, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
import { PersistenConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { ReloadWindowAction } from 'vs/workbench/electron-browser/actions/windowActions';
|
||||
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
|
||||
const WINDOW_ACTIONS_COMMAND_ID = 'remote.showActions';
|
||||
|
||||
export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
private windowIndicatorEntry: IStatusbarEntryAccessor | undefined;
|
||||
private windowCommandMenu: IMenu;
|
||||
private hasWindowActions: boolean = false;
|
||||
private remoteAuthority: string | undefined;
|
||||
private disconnected: boolean = false;
|
||||
|
||||
constructor(
|
||||
@IStatusbarService private readonly statusbarService: IStatusbarService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IContextKeyService private contextKeyService: IContextKeyService,
|
||||
@IMenuService private menuService: IMenuService,
|
||||
@IQuickInputService private readonly quickInputService: IQuickInputService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
|
||||
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.windowCommandMenu = this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService);
|
||||
this._register(this.windowCommandMenu);
|
||||
|
||||
this._register(CommandsRegistry.registerCommand(WINDOW_ACTIONS_COMMAND_ID, _ => this.showIndicatorActions(this.windowCommandMenu)));
|
||||
|
||||
this.remoteAuthority = environmentService.configuration.remoteAuthority;
|
||||
if (this.remoteAuthority) {
|
||||
// Pending entry until extensions are ready
|
||||
this.renderWindowIndicator(nls.localize('host.open', "$(sync~spin) Opening Remote..."));
|
||||
}
|
||||
|
||||
extensionService.whenInstalledExtensionsRegistered().then(_ => {
|
||||
if (this.remoteAuthority) {
|
||||
this._register(this.labelService.onDidChangeFormatters(e => this.updateWindowIndicator()));
|
||||
remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority).then(() => this.setDisconnected(false), () => this.setDisconnected(true));
|
||||
}
|
||||
this._register(this.windowCommandMenu.onDidChange(e => this.updateWindowActions()));
|
||||
this.updateWindowIndicator();
|
||||
});
|
||||
|
||||
const connection = remoteAgentService.getConnection();
|
||||
if (connection) {
|
||||
this._register(connection.onDidStateChange((e) => {
|
||||
switch (e.type) {
|
||||
case PersistenConnectionEventType.ConnectionLost:
|
||||
case PersistenConnectionEventType.ReconnectionPermanentFailure:
|
||||
case PersistenConnectionEventType.ReconnectionRunning:
|
||||
case PersistenConnectionEventType.ReconnectionWait:
|
||||
this.setDisconnected(true);
|
||||
break;
|
||||
case PersistenConnectionEventType.ConnectionGain:
|
||||
this.setDisconnected(false);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private setDisconnected(isDisconnected: boolean): void {
|
||||
if (this.disconnected !== isDisconnected) {
|
||||
this.disconnected = isDisconnected;
|
||||
this.updateWindowIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
private updateWindowIndicator(): void {
|
||||
const windowActionCommand = this.windowCommandMenu.getActions().length ? WINDOW_ACTIONS_COMMAND_ID : undefined;
|
||||
if (this.remoteAuthority) {
|
||||
const hostLabel = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.remoteAuthority) || this.remoteAuthority;
|
||||
if (!this.disconnected) {
|
||||
this.renderWindowIndicator(`$(remote) ${hostLabel}`, nls.localize('host.tooltip', "Editing on {0}", hostLabel), windowActionCommand);
|
||||
} else {
|
||||
this.renderWindowIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from")} ${hostLabel}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel));
|
||||
}
|
||||
} else {
|
||||
if (windowActionCommand) {
|
||||
this.renderWindowIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a remote window"), windowActionCommand);
|
||||
} else if (this.windowIndicatorEntry) {
|
||||
this.windowIndicatorEntry.dispose();
|
||||
this.windowIndicatorEntry = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateWindowActions() {
|
||||
const newHasWindowActions = this.windowCommandMenu.getActions().length > 0;
|
||||
if (newHasWindowActions !== this.hasWindowActions) {
|
||||
this.hasWindowActions = newHasWindowActions;
|
||||
this.updateWindowIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
private renderWindowIndicator(text: string, tooltip?: string, command?: string): void {
|
||||
const properties: IStatusbarEntry = {
|
||||
backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND), color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), text, tooltip, command
|
||||
};
|
||||
if (this.windowIndicatorEntry) {
|
||||
this.windowIndicatorEntry.update(properties);
|
||||
} else {
|
||||
this.windowIndicatorEntry = this.statusbarService.addEntry(properties, StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */);
|
||||
}
|
||||
}
|
||||
|
||||
private showIndicatorActions(menu: IMenu) {
|
||||
|
||||
const actions = menu.getActions();
|
||||
|
||||
const items: (IQuickPickItem | IQuickPickSeparator)[] = [];
|
||||
for (let actionGroup of actions) {
|
||||
if (items.length) {
|
||||
items.push({ type: 'separator' });
|
||||
}
|
||||
for (let action of actionGroup[1]) {
|
||||
if (action instanceof MenuItemAction) {
|
||||
let label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value;
|
||||
if (action.item.category) {
|
||||
const category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value;
|
||||
label = nls.localize('cat.title', "{0}: {1}", category, label);
|
||||
}
|
||||
items.push({
|
||||
type: 'item',
|
||||
id: action.item.id,
|
||||
label
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const quickPick = this.quickInputService.createQuickPick();
|
||||
quickPick.items = items;
|
||||
quickPick.canSelectMany = false;
|
||||
quickPick.onDidAccept(_ => {
|
||||
const selectedItems = quickPick.selectedItems;
|
||||
if (selectedItems.length === 1) {
|
||||
this.commandService.executeCommand(selectedItems[0].id!);
|
||||
}
|
||||
quickPick.hide();
|
||||
});
|
||||
quickPick.show();
|
||||
}
|
||||
}
|
||||
|
||||
class LogOutputChannels extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
constructor(
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService
|
||||
) {
|
||||
super();
|
||||
remoteAgentService.getEnvironment().then(remoteEnv => {
|
||||
if (remoteEnv) {
|
||||
const outputChannelRegistry = Registry.as<IOutputChannelRegistry>(OutputExt.OutputChannels);
|
||||
outputChannelRegistry.registerChannel({ id: 'remoteExtensionLog', label: nls.localize('remoteExtensionLog', "Remote Server"), file: resources.joinPath(remoteEnv.logsPath, `${RemoteExtensionLogFileName}.log`), log: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteChannelsContribution implements IWorkbenchContribution {
|
||||
|
||||
constructor(
|
||||
@ILogService logService: ILogService,
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IDialogService dialogService: IDialogService
|
||||
) {
|
||||
const connection = remoteAgentService.getConnection();
|
||||
if (connection) {
|
||||
connection.registerChannel('dialog', new DialogChannel(dialogService));
|
||||
connection.registerChannel('download', new DownloadServiceChannel());
|
||||
connection.registerChannel('loglevel', new LogLevelSetterChannel(logService));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteAgentDiagnosticListener implements IWorkbenchContribution {
|
||||
constructor(
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
|
||||
@ILabelService labelService: ILabelService
|
||||
) {
|
||||
ipc.on('vscode:getDiagnosticInfo', (event: Event, request: { replyChannel: string, args: IDiagnosticInfoOptions }): void => {
|
||||
const connection = remoteAgentService.getConnection();
|
||||
if (connection) {
|
||||
const hostName = labelService.getHostLabel(REMOTE_HOST_SCHEME, connection.remoteAuthority);
|
||||
remoteAgentService.getDiagnosticInfo(request.args)
|
||||
.then(info => {
|
||||
if (info) {
|
||||
(info as IRemoteDiagnosticInfo).hostName = hostName;
|
||||
}
|
||||
|
||||
ipc.send(request.replyChannel, info);
|
||||
})
|
||||
.catch(e => {
|
||||
const errorMessage = e && e.message ? `Fetching remote diagnostics for '${hostName}' failed: ${e.message}` : `Fetching remote diagnostics for '${hostName}' failed.`;
|
||||
ipc.send(request.replyChannel, { hostName, errorMessage });
|
||||
});
|
||||
} else {
|
||||
ipc.send(request.replyChannel);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ProgressReporter {
|
||||
private _currentProgress: IProgress<IProgressStep> | null = null;
|
||||
private lastReport: string | null = null;
|
||||
|
||||
constructor(currentProgress: IProgress<IProgressStep> | null) {
|
||||
this._currentProgress = currentProgress;
|
||||
}
|
||||
|
||||
set currentProgress(progress: IProgress<IProgressStep>) {
|
||||
this._currentProgress = progress;
|
||||
}
|
||||
|
||||
report(message?: string) {
|
||||
if (message) {
|
||||
this.lastReport = message;
|
||||
}
|
||||
|
||||
if (this.lastReport && this._currentProgress) {
|
||||
this._currentProgress.report({ message: this.lastReport });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteAgentConnectionStatusListener implements IWorkbenchContribution {
|
||||
constructor(
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
|
||||
@IProgressService2 progressService: IProgressService2,
|
||||
@IDialogService dialogService: IDialogService,
|
||||
@ICommandService commandService: ICommandService
|
||||
) {
|
||||
const connection = remoteAgentService.getConnection();
|
||||
if (connection) {
|
||||
let currentProgressPromiseResolve: (() => void) | null = null;
|
||||
let progressReporter: ProgressReporter | null = null;
|
||||
let currentTimer: ReconnectionTimer | null = null;
|
||||
|
||||
connection.onDidStateChange((e) => {
|
||||
if (currentTimer) {
|
||||
currentTimer.dispose();
|
||||
currentTimer = null;
|
||||
}
|
||||
switch (e.type) {
|
||||
case PersistenConnectionEventType.ConnectionLost:
|
||||
if (!currentProgressPromiseResolve) {
|
||||
let promise = new Promise<void>((resolve) => currentProgressPromiseResolve = resolve);
|
||||
progressService!.withProgress(
|
||||
{ location: ProgressLocation.Dialog },
|
||||
(progress: IProgress<IProgressStep> | null) => { progressReporter = new ProgressReporter(progress!); return promise; },
|
||||
() => {
|
||||
currentProgressPromiseResolve!();
|
||||
promise = new Promise<void>((resolve) => currentProgressPromiseResolve = resolve);
|
||||
progressService!.withProgress({ location: ProgressLocation.Notification }, (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; });
|
||||
progressReporter!.report();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
progressReporter!.report(nls.localize('connectionLost', "Connection Lost"));
|
||||
break;
|
||||
case PersistenConnectionEventType.ReconnectionWait:
|
||||
currentTimer = new ReconnectionTimer(progressReporter!, Date.now() + 1000 * e.durationSeconds);
|
||||
break;
|
||||
case PersistenConnectionEventType.ReconnectionRunning:
|
||||
progressReporter!.report(nls.localize('reconnectionRunning', "Attempting to reconnect..."));
|
||||
break;
|
||||
case PersistenConnectionEventType.ReconnectionPermanentFailure:
|
||||
currentProgressPromiseResolve!();
|
||||
currentProgressPromiseResolve = null;
|
||||
progressReporter = null;
|
||||
|
||||
dialogService.show(Severity.Error, nls.localize('reconnectionPermanentFailure', "Cannot reconnect. Please reload the window."), [nls.localize('reloadWindow', "Reload Window"), nls.localize('cancel', "Cancel")], { cancelId: 1 }).then(choice => {
|
||||
// Reload the window
|
||||
if (choice === 0) {
|
||||
commandService.executeCommand(ReloadWindowAction.ID);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case PersistenConnectionEventType.ConnectionGain:
|
||||
currentProgressPromiseResolve!();
|
||||
currentProgressPromiseResolve = null;
|
||||
progressReporter = null;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ReconnectionTimer implements IDisposable {
|
||||
private readonly _progressReporter: ProgressReporter;
|
||||
private readonly _completionTime: number;
|
||||
private readonly _token: NodeJS.Timeout;
|
||||
|
||||
constructor(progressReporter: ProgressReporter, completionTime: number) {
|
||||
this._progressReporter = progressReporter;
|
||||
this._completionTime = completionTime;
|
||||
this._token = setInterval(() => this._render(), 1000);
|
||||
this._render();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
clearInterval(this._token);
|
||||
}
|
||||
|
||||
private _render() {
|
||||
const remainingTimeMs = this._completionTime - Date.now();
|
||||
if (remainingTimeMs < 0) {
|
||||
return;
|
||||
}
|
||||
const remainingTime = Math.ceil(remainingTimeMs / 1000);
|
||||
if (remainingTime === 1) {
|
||||
this._progressReporter.report(nls.localize('reconnectionWaitOne', "Attempting to reconnect in {0} second...", remainingTime));
|
||||
} else {
|
||||
this._progressReporter.report(nls.localize('reconnectionWaitMany', "Attempting to reconnect in {0} seconds...", remainingTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteTelemetryEnablementUpdater extends Disposable implements IWorkbenchContribution {
|
||||
constructor(
|
||||
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.updateRemoteTelemetryEnablement();
|
||||
|
||||
this._register(configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('telemetry.enableTelemetry')) {
|
||||
this.updateRemoteTelemetryEnablement();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private updateRemoteTelemetryEnablement(): Promise<void> {
|
||||
if (!this.configurationService.getValue('telemetry.enableTelemetry')) {
|
||||
return this.remoteAgentService.disableTelemetry();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchContributionsExtensions.Workbench);
|
||||
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteChannelsContribution, LifecyclePhase.Starting);
|
||||
workbenchContributionsRegistry.registerWorkbenchContribution(LogOutputChannels, LifecyclePhase.Eventually);
|
||||
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentDiagnosticListener, LifecyclePhase.Eventually);
|
||||
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually);
|
||||
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteWindowActiveIndicator, LifecyclePhase.Starting);
|
||||
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteTelemetryEnablementUpdater, LifecyclePhase.Ready);
|
||||
|
||||
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
|
||||
.registerConfiguration({
|
||||
id: 'remote',
|
||||
title: nls.localize('remote', "Remote"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'remote.extensionKind': {
|
||||
type: 'object',
|
||||
markdownDescription: nls.localize('remote.extensionKind', "Override the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote. By overriding an extension's default kind using this setting, you specify if that extension should be installed and enabled locally or remotely."),
|
||||
patternProperties: {
|
||||
'([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$': {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'ui',
|
||||
'workspace'
|
||||
],
|
||||
enumDescriptions: [
|
||||
nls.localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."),
|
||||
nls.localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.")
|
||||
],
|
||||
default: 'ui'
|
||||
},
|
||||
},
|
||||
default: {
|
||||
'pub.name': 'ui'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -342,6 +342,9 @@ export class SearchView extends ViewletPanel {
|
||||
return this.inputPatternExcludes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning: a bit expensive due to updating the view title
|
||||
*/
|
||||
protected updateActions(): void {
|
||||
for (const action of this.actions) {
|
||||
action.update();
|
||||
@@ -1386,6 +1389,8 @@ export class SearchView extends ViewletPanel {
|
||||
|
||||
let visibleMatches = 0;
|
||||
|
||||
let updatedActionsForFileCount = false;
|
||||
|
||||
// Handle UI updates in an interval to show frequent progress and results
|
||||
const uiRefreshHandle: any = setInterval(() => {
|
||||
if (!this.searching) {
|
||||
@@ -1399,7 +1404,9 @@ export class SearchView extends ViewletPanel {
|
||||
visibleMatches = fileCount;
|
||||
this.refreshAndUpdateCount();
|
||||
}
|
||||
if (fileCount > 0) {
|
||||
|
||||
if (fileCount > 0 && !updatedActionsForFileCount) {
|
||||
updatedActionsForFileCount = true;
|
||||
this.updateActions();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
@@ -274,7 +274,7 @@ export class QueryBuilder {
|
||||
* Split search paths (./ or ../ or absolute paths in the includePatterns) into absolute paths and globs applied to those paths
|
||||
*/
|
||||
private expandSearchPathPatterns(searchPaths: string[]): ISearchPathPattern[] {
|
||||
if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY || !searchPaths || !searchPaths.length) {
|
||||
if (!searchPaths || !searchPaths.length) {
|
||||
// No workspace => ignore search paths
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
|
||||
export const TERMINAL_PICKER_PREFIX = 'term ';
|
||||
|
||||
@@ -85,7 +86,7 @@ export class ToggleTerminalAction extends TogglePanelAction {
|
||||
if (this.terminalService.terminalInstances.length === 0) {
|
||||
// If there is not yet an instance attempt to create it here so that we can suggest a
|
||||
// new shell on Windows (and not do so when the panel is restored on reload).
|
||||
const newTerminalInstance = this.terminalService.createTerminal(undefined, true);
|
||||
const newTerminalInstance = this.terminalService.createTerminal(undefined);
|
||||
const toDispose = newTerminalInstance.onProcessIdReady(() => {
|
||||
newTerminalInstance.focus();
|
||||
toDispose.dispose();
|
||||
@@ -329,7 +330,7 @@ export class CreateNewTerminalAction extends Action {
|
||||
if (folders.length <= 1) {
|
||||
// Allow terminal service to handle the path when there is only a
|
||||
// single root
|
||||
instancePromise = Promise.resolve(this.terminalService.createTerminal(undefined, true));
|
||||
instancePromise = Promise.resolve(this.terminalService.createTerminal(undefined));
|
||||
} else {
|
||||
const options: IPickOptions<IQuickPickItem> = {
|
||||
placeHolder: nls.localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal")
|
||||
@@ -339,7 +340,7 @@ export class CreateNewTerminalAction extends Action {
|
||||
// Don't create the instance if the workspace picker was canceled
|
||||
return null;
|
||||
}
|
||||
return this.terminalService.createTerminal({ cwd: workspace.uri }, true);
|
||||
return this.terminalService.createTerminal({ cwd: workspace.uri });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -366,7 +367,7 @@ export class CreateNewInActiveWorkspaceTerminalAction extends Action {
|
||||
}
|
||||
|
||||
public run(event?: any): Promise<any> {
|
||||
const instance = this.terminalService.createTerminal(undefined, true);
|
||||
const instance = this.terminalService.createTerminal(undefined);
|
||||
if (!instance) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
@@ -720,6 +721,14 @@ export class SwitchTerminalAction extends Action {
|
||||
if (!item || !item.split) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (item === SwitchTerminalActionViewItem.SEPARATOR) {
|
||||
this.terminalService.refreshActiveTab();
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (item === SelectDefaultShellWindowsTerminalAction.LABEL) {
|
||||
this.terminalService.refreshActiveTab();
|
||||
return this.terminalService.selectDefaultWindowsShell();
|
||||
}
|
||||
const selectedTabIndex = parseInt(item.split(':')[0], 10) - 1;
|
||||
this.terminalService.setActiveTabByIndex(selectedTabIndex);
|
||||
return this.terminalService.showPanel(true);
|
||||
@@ -728,11 +737,14 @@ export class SwitchTerminalAction extends Action {
|
||||
|
||||
export class SwitchTerminalActionViewItem extends SelectActionViewItem {
|
||||
|
||||
public static readonly SEPARATOR = '─────────';
|
||||
|
||||
constructor(
|
||||
action: IAction,
|
||||
@ITerminalService private readonly terminalService: ITerminalService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextViewService contextViewService: IContextViewService
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
@IWorkbenchEnvironmentService private workbenchEnvironmentService: IWorkbenchEnvironmentService
|
||||
) {
|
||||
super(null, action, terminalService.getTabLabels().map(label => <ISelectOptionItem>{ text: label }), terminalService.activeTabIndex, contextViewService, { ariaLabel: nls.localize('terminals', 'Open Terminals.') });
|
||||
|
||||
@@ -743,7 +755,13 @@ export class SwitchTerminalActionViewItem extends SelectActionViewItem {
|
||||
}
|
||||
|
||||
private _updateItems(): void {
|
||||
this.setOptions(this.terminalService.getTabLabels().map(label => <ISelectOptionItem>{ text: label }), this.terminalService.activeTabIndex);
|
||||
const items = this.terminalService.getTabLabels().map(label => <ISelectOptionItem>{ text: label });
|
||||
let enableSelectDefaultShell = this.workbenchEnvironmentService.configuration.remoteAuthority ? false : isWindows;
|
||||
if (enableSelectDefaultShell) {
|
||||
items.push({ text: SwitchTerminalActionViewItem.SEPARATOR });
|
||||
items.push({ text: SelectDefaultShellWindowsTerminalAction.LABEL });
|
||||
}
|
||||
this.setOptions(items, this.terminalService.activeTabIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -635,7 +635,7 @@ export class TerminalInstance implements ITerminalInstance {
|
||||
|
||||
private _measureRenderTime(): void {
|
||||
const frameTimes: number[] = [];
|
||||
const textRenderLayer = this._xterm._core.renderer._renderLayers[0];
|
||||
const textRenderLayer = this._xterm._core._renderCoordinator._renderer._renderLayers[0];
|
||||
const originalOnGridChanged = textRenderLayer.onGridChanged;
|
||||
|
||||
const evaluateCanvasRenderer = () => {
|
||||
@@ -647,11 +647,7 @@ export class TerminalInstance implements ITerminalInstance {
|
||||
const promptChoices: IPromptChoice[] = [
|
||||
{
|
||||
label: nls.localize('yes', "Yes"),
|
||||
run: () => {
|
||||
this._configurationService.updateValue('terminal.integrated.rendererType', 'dom', ConfigurationTarget.USER).then(() => {
|
||||
this._notificationService.info(nls.localize('terminal.rendererInAllNewTerminals', "The terminal is now using the fallback renderer."));
|
||||
});
|
||||
}
|
||||
run: () => this._configurationService.updateValue('terminal.integrated.rendererType', 'dom', ConfigurationTarget.USER)
|
||||
} as IPromptChoice,
|
||||
{
|
||||
label: nls.localize('no', "No"),
|
||||
@@ -1264,7 +1260,7 @@ export class TerminalInstance implements ITerminalInstance {
|
||||
// maximize on Windows/Linux would fire an event saying that the terminal was not
|
||||
// visible.
|
||||
if (this._xterm.getOption('rendererType') === 'canvas') {
|
||||
this._xterm._core.renderer.onIntersectionChange({ intersectionRatio: 1 });
|
||||
this._xterm._core._renderCoordinator._onIntersectionChange({ intersectionRatio: 1 });
|
||||
// HACK: Force a refresh of the screen to ensure links are refresh corrected.
|
||||
// This can probably be removed when the above hack is fixed in Chromium.
|
||||
this._xterm.refresh(0, this._xterm.rows - 1);
|
||||
|
||||
@@ -88,6 +88,9 @@ export class TerminalLinkHandler {
|
||||
this._gitDiffPostImagePattern = /^\+\+\+ b\/(\S*)/;
|
||||
|
||||
this._tooltipCallback = (e: MouseEvent) => {
|
||||
if (!this._widgetManager) {
|
||||
return;
|
||||
}
|
||||
if (this._terminalService && this._terminalService.configHelper.config.rendererType === 'dom') {
|
||||
const target = (e.target as HTMLElement);
|
||||
this._widgetManager.showMessage(target.offsetLeft, target.offsetTop, this._getLinkHoverString());
|
||||
@@ -115,7 +118,11 @@ export class TerminalLinkHandler {
|
||||
const options: ILinkMatcherOptions = {
|
||||
matchIndex,
|
||||
tooltipCallback: this._tooltipCallback,
|
||||
leaveCallback: () => this._widgetManager.closeMessage(),
|
||||
leaveCallback: () => {
|
||||
if (this._widgetManager) {
|
||||
this._widgetManager.closeMessage();
|
||||
}
|
||||
},
|
||||
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
|
||||
priority: CUSTOM_LINK_PRIORITY
|
||||
};
|
||||
@@ -132,7 +139,11 @@ export class TerminalLinkHandler {
|
||||
this._xterm.webLinksInit(wrappedHandler, {
|
||||
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(uri, callback),
|
||||
tooltipCallback: this._tooltipCallback,
|
||||
leaveCallback: () => this._widgetManager.closeMessage(),
|
||||
leaveCallback: () => {
|
||||
if (this._widgetManager) {
|
||||
this._widgetManager.closeMessage();
|
||||
}
|
||||
},
|
||||
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e)
|
||||
});
|
||||
}
|
||||
@@ -144,7 +155,11 @@ export class TerminalLinkHandler {
|
||||
this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
|
||||
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback),
|
||||
tooltipCallback: this._tooltipCallback,
|
||||
leaveCallback: () => this._widgetManager.closeMessage(),
|
||||
leaveCallback: () => {
|
||||
if (this._widgetManager) {
|
||||
this._widgetManager.closeMessage();
|
||||
}
|
||||
},
|
||||
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
|
||||
priority: LOCAL_LINK_PRIORITY
|
||||
});
|
||||
@@ -158,7 +173,11 @@ export class TerminalLinkHandler {
|
||||
matchIndex: 1,
|
||||
validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback),
|
||||
tooltipCallback: this._tooltipCallback,
|
||||
leaveCallback: () => this._widgetManager.closeMessage(),
|
||||
leaveCallback: () => {
|
||||
if (this._widgetManager) {
|
||||
this._widgetManager.closeMessage();
|
||||
}
|
||||
},
|
||||
willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
|
||||
priority: LOCAL_LINK_PRIORITY
|
||||
};
|
||||
@@ -242,12 +261,16 @@ export class TerminalLinkHandler {
|
||||
private _getLinkHoverString(): string {
|
||||
const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
|
||||
if (editorConf.multiCursorModifier === 'ctrlCmd') {
|
||||
return nls.localize('terminalLinkHandler.followLinkAlt', 'Alt + click to follow link');
|
||||
if (platform.isMacintosh) {
|
||||
return nls.localize('terminalLinkHandler.followLinkAlt.mac', "Option + click to follow link");
|
||||
} else {
|
||||
return nls.localize('terminalLinkHandler.followLinkAlt', "Alt + click to follow link");
|
||||
}
|
||||
}
|
||||
if (platform.isMacintosh) {
|
||||
return nls.localize('terminalLinkHandler.followLinkCmd', 'Cmd + click to follow link');
|
||||
return nls.localize('terminalLinkHandler.followLinkCmd', "Cmd + click to follow link");
|
||||
}
|
||||
return nls.localize('terminalLinkHandler.followLinkCtrl', 'Ctrl + click to follow link');
|
||||
return nls.localize('terminalLinkHandler.followLinkCtrl', "Ctrl + click to follow link");
|
||||
}
|
||||
|
||||
private get osPath(): IPath {
|
||||
|
||||
@@ -3,21 +3,19 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { ITerminalService, TERMINAL_PANEL_ID, ITerminalInstance, IShellLaunchConfig, NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { ITerminalService, TERMINAL_PANEL_ID, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { TerminalService as CommonTerminalService } from 'vs/workbench/contrib/terminal/common/terminalService';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { TerminalPanel } from 'vs/workbench/contrib/terminal/browser/terminalPanel';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance';
|
||||
@@ -36,7 +34,6 @@ export abstract class TerminalService extends CommonTerminalService implements I
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IDialogService dialogService: IDialogService,
|
||||
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
|
||||
@IWorkbenchEnvironmentService private _environmentService: IWorkbenchEnvironmentService,
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService
|
||||
@@ -52,7 +49,7 @@ export abstract class TerminalService extends CommonTerminalService implements I
|
||||
return instance;
|
||||
}
|
||||
|
||||
public createTerminal(shell: IShellLaunchConfig = {}, wasNewTerminalAction?: boolean): ITerminalInstance {
|
||||
public createTerminal(shell: IShellLaunchConfig = {}): ITerminalInstance {
|
||||
const terminalTab = this._instantiationService.createInstance(TerminalTab,
|
||||
this._terminalFocusContextKey,
|
||||
this.configHelper,
|
||||
@@ -68,74 +65,9 @@ export abstract class TerminalService extends CommonTerminalService implements I
|
||||
this.setActiveInstanceByIndex(0);
|
||||
}
|
||||
this._onInstancesChanged.fire();
|
||||
this._suggestShellChange(wasNewTerminalAction);
|
||||
return instance;
|
||||
}
|
||||
|
||||
private _suggestShellChange(wasNewTerminalAction?: boolean): void {
|
||||
// Only suggest on Windows since $SHELL works great for macOS/Linux
|
||||
if (!platform.isWindows) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._environmentService.configuration.remoteAuthority) {
|
||||
// Don't suggest if the opened workspace is remote
|
||||
return;
|
||||
}
|
||||
|
||||
// Only suggest when the terminal instance is being created by an explicit user action to
|
||||
// launch a terminal, as opposed to something like tasks, debug, panel restore, etc.
|
||||
if (!wasNewTerminalAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._environmentService.configuration.remoteAuthority) {
|
||||
// Don't suggest if the opened workspace is remote
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't suggest if the user has explicitly opted out
|
||||
const neverSuggest = this._storageService.getBoolean(NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, StorageScope.GLOBAL, false);
|
||||
if (neverSuggest) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Never suggest if the setting is non-default already (ie. they set the setting manually)
|
||||
if (this.configHelper.config.shell.windows !== this.getDefaultShell(platform.Platform.Windows)) {
|
||||
this._storageService.store(NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, true, StorageScope.GLOBAL);
|
||||
return;
|
||||
}
|
||||
|
||||
this._notificationService.prompt(
|
||||
Severity.Info,
|
||||
nls.localize('terminal.integrated.chooseWindowsShellInfo', "You can change the default terminal shell by selecting the customize button."),
|
||||
[{
|
||||
label: nls.localize('customize', "Customize"),
|
||||
run: () => {
|
||||
this.selectDefaultWindowsShell().then(shell => {
|
||||
if (!shell) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
// Launch a new instance with the newly selected shell
|
||||
const instance = this.createTerminal({
|
||||
executable: shell,
|
||||
args: this.configHelper.config.shellArgs.windows
|
||||
});
|
||||
if (instance) {
|
||||
this.setActiveInstance(instance);
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
label: nls.localize('never again', "Don't Show Again"),
|
||||
isSecondary: true,
|
||||
run: () => this._storageService.store(NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY, true, StorageScope.GLOBAL)
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
public focusFindWidget(): Promise<void> {
|
||||
return this.showPanel(false).then(() => {
|
||||
const panel = this._panelService.getActivePanel() as TerminalPanel;
|
||||
|
||||
@@ -37,7 +37,6 @@ export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED = new RawContextKey
|
||||
export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED.toNegated();
|
||||
|
||||
export const IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY = 'terminal.integrated.isWorkspaceShellAllowed';
|
||||
export const NEVER_SUGGEST_SELECT_WINDOWS_SHELL_STORAGE_KEY = 'terminal.integrated.neverSuggestSelectWindowsShell';
|
||||
export const NEVER_MEASURE_RENDER_TIME_STORAGE_KEY = 'terminal.integrated.neverMeasureRenderTime';
|
||||
|
||||
// The creation of extension host terminals is delayed by this value (milliseconds). The purpose of
|
||||
@@ -64,9 +63,9 @@ export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '50
|
||||
|
||||
export interface ITerminalConfiguration {
|
||||
shell: {
|
||||
linux: string;
|
||||
osx: string;
|
||||
windows: string;
|
||||
linux: string | null;
|
||||
osx: string | null;
|
||||
windows: string | null;
|
||||
};
|
||||
shellArgs: {
|
||||
linux: string[];
|
||||
@@ -216,10 +215,8 @@ export interface ITerminalService {
|
||||
/**
|
||||
* Creates a terminal.
|
||||
* @param shell The shell launch configuration to use.
|
||||
* @param wasNewTerminalAction Whether this was triggered by a new terminal action, if so a
|
||||
* default shell selection dialog may display.
|
||||
*/
|
||||
createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance;
|
||||
createTerminal(shell?: IShellLaunchConfig): ITerminalInstance;
|
||||
|
||||
/**
|
||||
* Creates a terminal renderer.
|
||||
@@ -245,6 +242,12 @@ export interface ITerminalService {
|
||||
setActiveTabToPrevious(): void;
|
||||
setActiveTabByIndex(tabIndex: number): void;
|
||||
|
||||
/**
|
||||
* Fire the onActiveTabChanged event, this will trigger the terminal dropdown to be updated,
|
||||
* among other things.
|
||||
*/
|
||||
refreshActiveTab(): void;
|
||||
|
||||
showPanel(focus?: boolean): Promise<void>;
|
||||
hidePanel(): void;
|
||||
focusFindWidget(): Promise<void>;
|
||||
|
||||
@@ -98,6 +98,7 @@ function _getLangEnvVariable(locale?: string) {
|
||||
es: 'ES',
|
||||
fi: 'FI',
|
||||
fr: 'FR',
|
||||
hu: 'HU',
|
||||
it: 'IT',
|
||||
ja: 'JP',
|
||||
ko: 'KR',
|
||||
|
||||
@@ -41,7 +41,7 @@ export abstract class TerminalService implements ITerminalService {
|
||||
public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; }
|
||||
public get terminalTabs(): ITerminalTab[] { return this._terminalTabs; }
|
||||
|
||||
private readonly _onActiveTabChanged = new Emitter<void>();
|
||||
protected readonly _onActiveTabChanged = new Emitter<void>();
|
||||
public get onActiveTabChanged(): Event<void> { return this._onActiveTabChanged.event; }
|
||||
protected readonly _onInstanceCreated = new Emitter<ITerminalInstance>();
|
||||
public get onInstanceCreated(): Event<ITerminalInstance> { return this._onInstanceCreated.event; }
|
||||
@@ -104,6 +104,7 @@ export abstract class TerminalService implements ITerminalService {
|
||||
protected abstract _getWslPath(path: string): Promise<string>;
|
||||
protected abstract _getWindowsBuildNumber(): number;
|
||||
|
||||
public abstract refreshActiveTab(): void;
|
||||
public abstract createTerminal(shell?: IShellLaunchConfig, wasNewTerminalAction?: boolean): ITerminalInstance;
|
||||
public abstract createInstance(terminalFocusContextKey: IContextKey<boolean>, configHelper: ITerminalConfigHelper, container: HTMLElement, shellLaunchConfig: IShellLaunchConfig, doCreateProcess: boolean): ITerminalInstance;
|
||||
public abstract getDefaultShell(platform: Platform): string;
|
||||
|
||||
@@ -20,7 +20,6 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { ipcRenderer as ipc } from 'electron';
|
||||
import { IOpenFileRequest } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IQuickInputService, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
@@ -45,11 +44,10 @@ export class TerminalService extends BrowserTerminalService implements ITerminal
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IDialogService dialogService: IDialogService,
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService
|
||||
) {
|
||||
super(contextKeyService, panelService, layoutService, lifecycleService, storageService, notificationService, dialogService, instantiationService, environmentService, extensionService, fileService, remoteAgentService);
|
||||
super(contextKeyService, panelService, layoutService, lifecycleService, storageService, notificationService, dialogService, instantiationService, extensionService, fileService, remoteAgentService);
|
||||
|
||||
this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper, linuxDistro);
|
||||
ipc.on('vscode:openFiles', (_event: any, request: IOpenFileRequest) => {
|
||||
@@ -102,6 +100,11 @@ export class TerminalService extends BrowserTerminalService implements ITerminal
|
||||
return getDefaultShell(p);
|
||||
}
|
||||
|
||||
public refreshActiveTab(): void {
|
||||
// Fire active instances changed
|
||||
this._onActiveTabChanged.fire();
|
||||
}
|
||||
|
||||
public selectDefaultWindowsShell(): Promise<string | undefined> {
|
||||
return this._detectWindowsShells().then(shells => {
|
||||
const options: IPickOptions<IQuickPickItem> = {
|
||||
|
||||
@@ -62,13 +62,14 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
|
||||
!is32ProcessOn64Windows &&
|
||||
getWindowsBuildNumber() >= 18309;
|
||||
|
||||
const options: pty.IPtyForkOptions = {
|
||||
const options: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions = {
|
||||
name: shellName,
|
||||
cwd,
|
||||
env,
|
||||
cols,
|
||||
rows,
|
||||
experimentalUseConpty: useConpty
|
||||
experimentalUseConpty: useConpty,
|
||||
conptyInheritCursor: true
|
||||
};
|
||||
|
||||
// TODO: Need to verify whether executable is on $PATH, otherwise things like cmd.exe will break
|
||||
@@ -91,14 +92,14 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
|
||||
this._processStartupComplete = new Promise<void>(c => {
|
||||
this.onProcessIdReady(() => c());
|
||||
});
|
||||
ptyProcess.on('data', (data) => {
|
||||
ptyProcess.on('data', data => {
|
||||
this._onProcessData.fire(data);
|
||||
if (this._closeTimeout) {
|
||||
clearTimeout(this._closeTimeout);
|
||||
this._queueProcessExit();
|
||||
}
|
||||
});
|
||||
ptyProcess.on('exit', (code) => {
|
||||
ptyProcess.on('exit', code => {
|
||||
this._exitCode = code;
|
||||
this._queueProcessExit();
|
||||
});
|
||||
@@ -126,12 +127,14 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
|
||||
setTimeout(() => {
|
||||
this._sendProcessTitle(ptyProcess);
|
||||
}, 0);
|
||||
// Setup polling
|
||||
this._titleInterval = setInterval(() => {
|
||||
if (this._currentTitle !== ptyProcess.process) {
|
||||
this._sendProcessTitle(ptyProcess);
|
||||
}
|
||||
}, 200);
|
||||
// Setup polling for non-Windows, for Windows `process` doesn't change
|
||||
if (!platform.isWindows) {
|
||||
this._titleInterval = setInterval(() => {
|
||||
if (this._currentTitle !== ptyProcess.process) {
|
||||
this._sendProcessTitle(ptyProcess);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow any trailing data events to be sent before the exit event is sent.
|
||||
@@ -189,7 +192,7 @@ export class TerminalProcess implements ITerminalChildProcess, IDisposable {
|
||||
if (this._isDisposed || !this._ptyProcess) {
|
||||
return;
|
||||
}
|
||||
this._logService.trace('IPty#write', data);
|
||||
this._logService.trace('IPty#write', `${data.length} characters`);
|
||||
this._ptyProcess.write(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import * as modes from 'vs/editor/common/modes';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
|
||||
@@ -118,7 +118,7 @@ class WebviewProtocolProvider extends Disposable {
|
||||
private readonly _extensionLocation: URI | undefined,
|
||||
private readonly _getLocalResourceRoots: () => ReadonlyArray<URI>,
|
||||
private readonly _environmentService: IEnvironmentService,
|
||||
private readonly _textFileService: ITextFileService,
|
||||
private readonly _fileService: IFileService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -137,11 +137,11 @@ class WebviewProtocolProvider extends Disposable {
|
||||
|
||||
const appRootUri = URI.file(this._environmentService.appRoot);
|
||||
|
||||
registerFileProtocol(contents, WebviewProtocol.CoreResource, this._textFileService, undefined, () => [
|
||||
registerFileProtocol(contents, WebviewProtocol.CoreResource, this._fileService, undefined, () => [
|
||||
appRootUri
|
||||
]);
|
||||
|
||||
registerFileProtocol(contents, WebviewProtocol.VsCodeResource, this._textFileService, this._extensionLocation, () =>
|
||||
registerFileProtocol(contents, WebviewProtocol.VsCodeResource, this._fileService, this._extensionLocation, () =>
|
||||
this._getLocalResourceRoots()
|
||||
);
|
||||
}
|
||||
@@ -165,7 +165,8 @@ class WebviewPortMappingProvider extends Disposable {
|
||||
|
||||
session.onBeforeRequest(async (details) => {
|
||||
const uri = URI.parse(details.url);
|
||||
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
|
||||
const allowedSchemes = ['http', 'https', 'ws', 'wss'];
|
||||
if (allowedSchemes.indexOf(uri.scheme) === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -379,7 +380,7 @@ export class WebviewElement extends Disposable implements Webview {
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@ITextFileService textFileService: ITextFileService,
|
||||
@IFileService fileService: IFileService,
|
||||
@ITunnelService tunnelService: ITunnelService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@@ -422,7 +423,7 @@ export class WebviewElement extends Disposable implements Webview {
|
||||
this._options.extension ? this._options.extension.location : undefined,
|
||||
() => (this.content.options.localResourceRoots || []),
|
||||
environmentService,
|
||||
textFileService));
|
||||
fileService));
|
||||
|
||||
this._register(new WebviewPortMappingProvider(
|
||||
session,
|
||||
|
||||
@@ -7,17 +7,20 @@ import { extname, sep } from 'vs/base/common/path';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import * as electron from 'electron';
|
||||
|
||||
type BufferProtocolCallback = (buffer?: Buffer | electron.MimeTypedBuffer | { error: number }) => void;
|
||||
|
||||
export const enum WebviewProtocol {
|
||||
CoreResource = 'vscode-core-resource',
|
||||
VsCodeResource = 'vscode-resource',
|
||||
}
|
||||
|
||||
function resolveContent(textFileService: ITextFileService, resource: URI, mime: string, callback: any): void {
|
||||
textFileService.read(resource, { encoding: 'binary' }).then(contents => {
|
||||
function resolveContent(fileService: IFileService, resource: URI, mime: string, callback: BufferProtocolCallback): void {
|
||||
fileService.readFile(resource).then(contents => {
|
||||
callback({
|
||||
data: Buffer.from(contents.value, contents.encoding),
|
||||
data: Buffer.from(contents.value.buffer),
|
||||
mimeType: mime
|
||||
});
|
||||
}, (err) => {
|
||||
@@ -27,9 +30,9 @@ function resolveContent(textFileService: ITextFileService, resource: URI, mime:
|
||||
}
|
||||
|
||||
export function registerFileProtocol(
|
||||
contents: Electron.WebContents,
|
||||
contents: electron.WebContents,
|
||||
protocol: WebviewProtocol,
|
||||
textFileService: ITextFileService,
|
||||
fileService: IFileService,
|
||||
extensionLocation: URI | undefined,
|
||||
getRoots: () => ReadonlyArray<URI>
|
||||
) {
|
||||
@@ -51,10 +54,10 @@ export function registerFileProtocol(
|
||||
requestResourcePath: requestUri.path
|
||||
})
|
||||
});
|
||||
resolveContent(textFileService, redirectedUri, getMimeType(requestUri), callback);
|
||||
resolveContent(fileService, redirectedUri, getMimeType(requestUri), callback);
|
||||
return;
|
||||
} else {
|
||||
resolveContent(textFileService, normalizedPath, getMimeType(normalizedPath), callback);
|
||||
resolveContent(fileService, normalizedPath, getMimeType(normalizedPath), callback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export default () => `
|
||||
<li><a href="command:workbench.action.openDocumentationUrl">${escape(localize('welcomePage.productDocumentation', "Product documentation"))}</a></li>
|
||||
<li><a href="https://github.com/Microsoft/vscode">${escape(localize('welcomePage.gitHubRepository', "GitHub repository"))}</a></li>
|
||||
<li><a href="http://stackoverflow.com/questions/tagged/vscode?sort=votes&pageSize=50">${escape(localize('welcomePage.stackOverflow', "Stack Overflow"))}</a></li>
|
||||
<li><a href="command:workbench.action.openNewsletterSignupUrl">${escape(localize('welcomePage.newsletterSignup', "Join our Newsletter"))}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="showOnStartup"><input type="checkbox" id="showOnStartup" class="checkbox"> <label class="caption" for="showOnStartup">${escape(localize('welcomePage.showOnStartup', "Show welcome page on startup"))}</label></p>
|
||||
|
||||
Reference in New Issue
Block a user