diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 4583524002..cbccda55a1 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -23,8 +23,8 @@ import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit'; import * as nls from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; -import { ILabelService } from 'vs/platform/label/common/label'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; export function alertFormattingEdits(edits: ISingleEditOperation[]): void { @@ -86,30 +86,51 @@ export function getRealAndSyntheticDocumentFormattersOrdered(model: ITextModel): return result; } -export async function formatDocumentRangeWithFirstProvider( +export const enum FormattingMode { + Explicit = 1, + Silent = 2 +} + +export interface IFormattingEditProviderSelector { + (formatter: T[], document: ITextModel, mode: FormattingMode): Promise; +} + +export abstract class FormattingConflicts { + + private static readonly _selectors = new LinkedList(); + + static setFormatterSelector(selector: IFormattingEditProviderSelector): IDisposable { + const remove = FormattingConflicts._selectors.unshift(selector); + return { dispose: remove }; + } + + static async select(formatter: T[], document: ITextModel, mode: FormattingMode): Promise { + if (formatter.length === 0) { + return undefined; + } + const { value: selector } = FormattingConflicts._selectors.iterator().next(); + if (selector) { + return await selector(formatter, document, mode); + } + return formatter[0]; + } +} + +export async function formatDocumentRangeWithSelectedProvider( accessor: ServicesAccessor, editorOrModel: ITextModel | IActiveCodeEditor, range: Range, + mode: FormattingMode, token: CancellationToken -): Promise { +): Promise { const instaService = accessor.get(IInstantiationService); - const statusBarService = accessor.get(IStatusbarService); - const labelService = accessor.get(ILabelService); - const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel; - const [best, ...rest] = DocumentRangeFormattingEditProviderRegistry.ordered(model); - if (!best) { - return false; + const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model); + const selected = await FormattingConflicts.select(provider, model, mode); + if (selected) { + await instaService.invokeFunction(formatDocumentRangeWithProvider, selected, editorOrModel, range, token); } - const ret = await instaService.invokeFunction(formatDocumentRangeWithProvider, best, editorOrModel, range, token); - if (rest.length > 0) { - statusBarService.setStatusMessage( - nls.localize('random.pick', "$(tasklist) Formatted '{0}' with '{1}'", labelService.getUriLabel(model.uri, { relative: true }), best.displayName), - 5 * 1000 - ); - } - return ret; } export async function formatDocumentRangeWithProvider( @@ -181,29 +202,20 @@ export async function formatDocumentRangeWithProvider( return true; } -export async function formatDocumentWithFirstProvider( +export async function formatDocumentWithSelectedProvider( accessor: ServicesAccessor, editorOrModel: ITextModel | IActiveCodeEditor, + mode: FormattingMode, token: CancellationToken -): Promise { +): Promise { const instaService = accessor.get(IInstantiationService); - const statusBarService = accessor.get(IStatusbarService); - const labelService = accessor.get(ILabelService); - const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel; - const [best, ...rest] = getRealAndSyntheticDocumentFormattersOrdered(model); - if (!best) { - return false; + const provider = getRealAndSyntheticDocumentFormattersOrdered(model); + const selected = await FormattingConflicts.select(provider, model, mode); + if (selected) { + await instaService.invokeFunction(formatDocumentWithProvider, selected, editorOrModel, token); } - const ret = await instaService.invokeFunction(formatDocumentWithProvider, best, editorOrModel, token); - if (rest.length > 0) { - statusBarService.setStatusMessage( - nls.localize('random.pick', "$(tasklist) Formatted '{0}' with '{1}'", labelService.getUriLabel(model.uri, { relative: true }), best.displayName), - 5 * 1000 - ); - } - return ret; } export async function formatDocumentWithProvider( diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index d16c9d4184..b363cba00f 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -16,7 +16,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithFirstProvider, formatDocumentWithFirstProvider } from 'vs/editor/contrib/format/format'; +import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit'; import * as nls from 'vs/nls'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; @@ -211,7 +211,7 @@ class FormatOnPaste implements editorCommon.IEditorContribution { if (this.editor.getSelections().length > 1) { return; } - this._instantiationService.invokeFunction(formatDocumentRangeWithFirstProvider, this.editor, range, CancellationToken.None).catch(onUnexpectedError); + this._instantiationService.invokeFunction(formatDocumentRangeWithSelectedProvider, this.editor, range, FormattingMode.Silent, CancellationToken.None).catch(onUnexpectedError); } } @@ -240,7 +240,7 @@ class FormatDocumentAction extends EditorAction { async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { if (editor.hasModel()) { const instaService = accessor.get(IInstantiationService); - await instaService.invokeFunction(formatDocumentWithFirstProvider, editor, CancellationToken.None); + await instaService.invokeFunction(formatDocumentWithSelectedProvider, editor, FormattingMode.Explicit, CancellationToken.None); } } } @@ -276,7 +276,7 @@ class FormatSelectionAction extends EditorAction { if (range.isEmpty()) { range = new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber)); } - await instaService.invokeFunction(formatDocumentRangeWithFirstProvider, editor, range, CancellationToken.None); + await instaService.invokeFunction(formatDocumentRangeWithSelectedProvider, editor, range, FormattingMode.Explicit, CancellationToken.None); } } diff --git a/src/vs/platform/request/node/request.ts b/src/vs/platform/request/node/request.ts index 6dd3eed718..8ac77ca6b2 100644 --- a/src/vs/platform/request/node/request.ts +++ b/src/vs/platform/request/node/request.ts @@ -62,7 +62,7 @@ Registry.as(Extensions.Configuration) 'http.systemCertificates': { type: 'boolean', default: true, - description: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. (On Windows and Mac a reload of the window is required after turning this off.)") + description: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS. (On Windows and macOS a reload of the window is required after turning this off.)") } } }); diff --git a/src/vs/platform/workspaces/electron-browser/workspacesService.ts b/src/vs/platform/workspaces/electron-browser/workspacesService.ts index 45b4d77972..3f6d27265f 100644 --- a/src/vs/platform/workspaces/electron-browser/workspacesService.ts +++ b/src/vs/platform/workspaces/electron-browser/workspacesService.ts @@ -28,6 +28,6 @@ export class WorkspacesService implements IWorkspacesService { } getWorkspaceIdentifier(configPath: URI): Promise { - return this.channel.call('getWorkspaceIdentifier', configPath); + return this.channel.call('getWorkspaceIdentifier', configPath).then(reviveWorkspaceIdentifier); } } diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 554864b56c..fece587efc 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -86,7 +86,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha if (missingInstalledDependency.enablementState === EnablementState.Enabled || missingInstalledDependency.enablementState === EnablementState.WorkspaceEnabled) { this._notificationService.notify({ severity: Severity.Error, - message: localize('reload window', "Cannot activate extension '{0}' because it depends on extension '{1}', which is not loaded. Would you like to reload the window to load the extension?", extName, missingInstalledDependency.displayName), + message: localize('reload window', "Cannot activate the '{0}' extension because it depends on the '{1}' extension, which is not loaded. Would you like to reload the window to load the extension?", extName, missingInstalledDependency.displayName), actions: { primary: [new Action('reload', localize('reload', "Reload Window"), '', true, () => this._windowService.reloadWindow())] } @@ -94,7 +94,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha } else { this._notificationService.notify({ severity: Severity.Error, - message: localize('disabledDep', "Cannot activate extension '{0}' because it depends on extension '{1}', which is disabled. Would you like to enable the extension and reload the window?", extName, missingInstalledDependency.displayName), + message: localize('disabledDep', "Cannot activate the '{0}' extension because it depends on the '{1}' extension, which is disabled. Would you like to enable the extension and reload the window?", extName, missingInstalledDependency.displayName), actions: { primary: [new Action('enable', localize('enable dep', "Enable and Reload"), '', true, () => this._extensionsWorkbenchService.setEnablement([missingInstalledDependency], missingInstalledDependency.enablementState === EnablementState.Disabled ? EnablementState.Enabled : EnablementState.WorkspaceEnabled) @@ -110,7 +110,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha if (dependencyExtension) { this._notificationService.notify({ severity: Severity.Error, - message: localize('uninstalledDep', "Cannot activate extension '{0}' because it depends on extension '{1}', which is not installed. Would you like to install the extension and reload the window?", extName, dependencyExtension.displayName), + message: localize('uninstalledDep', "Cannot activate the '{0}' extension because it depends on the '{1}' extension, which is not installed. Would you like to install the extension and reload the window?", extName, dependencyExtension.displayName), actions: { primary: [new Action('install', localize('install missing dep', "Install and Reload"), '', true, () => this._extensionsWorkbenchService.install(dependencyExtension) @@ -118,7 +118,7 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha } }); } else { - this._notificationService.error(localize('unknownDep', "Cannot activate extension '{0}' because it depends on an unknown extension '{1}'.", extName, missingDependency)); + this._notificationService.error(localize('unknownDep', "Cannot activate the '{0}' extension because it depends on an unknown '{1}' extension .", extName, missingDependency)); } } diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts index a136cf6e01..8083a9d8a9 100644 --- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts @@ -21,7 +21,7 @@ import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger'; -import { formatDocumentWithFirstProvider } from 'vs/editor/contrib/format/format'; +import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -257,7 +257,7 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { return new Promise((resolve, reject) => { const source = new CancellationTokenSource(); const timeout = this._configurationService.getValue('editor.formatOnSaveTimeout', overrides); - const request = this._instantiationService.invokeFunction(formatDocumentWithFirstProvider, model, source.token); + const request = this._instantiationService.invokeFunction(formatDocumentWithSelectedProvider, model, FormattingMode.Silent, source.token); setTimeout(() => { reject(localize('timeout.formatOnSave', "Aborted format on save after {0}ms", timeout)); diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index 2d705c1c48..bd55cc833f 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -250,7 +250,7 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews } try { - await this._proxy.$deserializeWebviewPanel(handle, viewType, webview.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webview.group || ACTIVE_GROUP), webview.options); + await this._proxy.$deserializeWebviewPanel(handle, viewType, webview.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webview.group || 0), webview.options); } catch (error) { onUnexpectedError(error); webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType); @@ -312,7 +312,7 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, { active: true, visible: true, - position: editorGroupToViewColumn(this._editorGroupService, newActiveWebview.input.group || ACTIVE_GROUP) + position: editorGroupToViewColumn(this._editorGroupService, newActiveWebview.input.group || 0) }); return; } @@ -324,7 +324,7 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews this._proxy.$onDidChangeWebviewPanelViewState(this._activeWebview, { active: false, visible: this._editorService.visibleControls.some(editor => !!editor.input && editor.input.matches(oldActiveWebview)), - position: editorGroupToViewColumn(this._editorGroupService, oldActiveWebview.group || ACTIVE_GROUP), + position: editorGroupToViewColumn(this._editorGroupService, oldActiveWebview.group || 0), }); } } diff --git a/src/vs/workbench/api/node/extHostTextEditor.ts b/src/vs/workbench/api/node/extHostTextEditor.ts index fdd4d0148a..45f6387e0d 100644 --- a/src/vs/workbench/api/node/extHostTextEditor.ts +++ b/src/vs/workbench/api/node/extHostTextEditor.ts @@ -42,7 +42,7 @@ export interface ITextEditOperation { export interface IEditData { documentVersionId: number; edits: ITextEditOperation[]; - setEndOfLine: EndOfLine; + setEndOfLine: EndOfLine | undefined; undoStopBefore: boolean; undoStopAfter: boolean; } @@ -52,7 +52,7 @@ export class TextEditorEdit { private readonly _document: vscode.TextDocument; private readonly _documentVersionId: number; private _collectedEdits: ITextEditOperation[]; - private _setEndOfLine: EndOfLine; + private _setEndOfLine: EndOfLine | undefined; private readonly _undoStopBefore: boolean; private readonly _undoStopAfter: boolean; @@ -60,7 +60,7 @@ export class TextEditorEdit { this._document = document; this._documentVersionId = document.version; this._collectedEdits = []; - this._setEndOfLine = 0; + this._setEndOfLine = undefined; this._undoStopBefore = options.undoStopBefore; this._undoStopAfter = options.undoStopAfter; } @@ -607,7 +607,7 @@ export class ExtHostTextEditor implements vscode.TextEditor { }); return this._proxy.$tryApplyEdits(this._id, editData.documentVersionId, edits, { - setEndOfLine: editData.setEndOfLine && TypeConverters.EndOfLine.from(editData.setEndOfLine), + setEndOfLine: typeof editData.setEndOfLine === 'number' ? TypeConverters.EndOfLine.from(editData.setEndOfLine) : undefined, undoStopBefore: editData.undoStopBefore, undoStopAfter: editData.undoStopAfter }); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts index 57b19db87b..820419741f 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts @@ -16,7 +16,8 @@ import { hash } from 'vs/base/common/hash'; export class Call { constructor( readonly item: CallHierarchyItem, - readonly locations: Location[] + readonly locations: Location[], + readonly parent: Call | undefined ) { } } @@ -39,20 +40,20 @@ export class SingleDirectionDataSource implements IAsyncDataSource new Call(item, locations)); + return calls.map(([item, locations]) => new Call(item, locations, element)); } catch { return []; } } else { // 'root' - return [new Call(element, [{ uri: element.uri, range: Range.lift(element.range).collapseToStart() }])]; + return [new Call(element, [{ uri: element.uri, range: Range.lift(element.range).collapseToStart() }], undefined)]; } } } export class IdentityProvider implements IIdentityProvider { getId(element: Call): { toString(): string; } { - return hash(element.item.uri.toString(), hash(JSON.stringify(element.item.range))); + return hash(element.item.uri.toString(), hash(JSON.stringify(element.item.range))).toString() + (element.parent ? this.getId(element.parent) : ''); } } diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css b/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css index e256e7458d..a619224f62 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css +++ b/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css @@ -13,9 +13,9 @@ } .monaco-workbench .call-hierarchy[data-state="message"] .message { - display: inherit; - text-align: center; - padding-top: 3em; + display: flex; + align-items: center; + justify-content: center; height: 100%; } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index cb75672a40..8031a80a62 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -839,6 +839,11 @@ registerEditorAction(AcceptReplInputAction); registerEditorAction(ReplCopyAllAction); class SelectReplActionItem extends FocusSessionActionItem { + + protected getActionContext(_: string, index: number): any { + return this.debugService.getModel().getSessions(true)[index]; + } + protected getSessions(): ReadonlyArray { return this.debugService.getModel().getSessions(true).filter(s => !sessionsToIgnore.has(s)); } @@ -856,8 +861,7 @@ class SelectReplAction extends Action { super(id, label); } - run(sessionName: string): Promise { - const session = this.debugService.getModel().getSessions(true).filter(p => p.getLabel() === sessionName).pop(); + run(session: IDebugSession): Promise { // If session is already the focused session we need to manualy update the tree since view model will not send a focused change event if (session && session.state !== State.Inactive && session !== this.debugService.getViewModel().focusedSession) { this.debugService.focusStackFrame(undefined, undefined, session, true); diff --git a/src/vs/workbench/contrib/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/electron-browser/debugConfigurationManager.ts index 0e968d2b74..f3a12c638a 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/electron-browser/debugConfigurationManager.ts @@ -601,8 +601,8 @@ class Launch extends AbstractLaunch implements ILaunch { revealIfVisible: true }, }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => ({ editor, created }))); - }, (error) => { - throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error)); + }, (error: Error) => { + throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error.message)); }); } } diff --git a/src/vs/workbench/contrib/debug/node/debugger.ts b/src/vs/workbench/contrib/debug/node/debugger.ts index d4c15f71a3..206347fab2 100644 --- a/src/vs/workbench/contrib/debug/node/debugger.ts +++ b/src/vs/workbench/contrib/debug/node/debugger.ts @@ -11,7 +11,7 @@ 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, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; +import { IConfig, IDebuggerContribution, IDebugAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, ITerminalSettings, IDebugger, IDebugSession, IAdapterDescriptor, IDebugAdapterServer } 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'; @@ -187,15 +187,6 @@ export class Debugger implements IDebugger { } private inExtHost(): boolean { - const debugConfigs = this.configurationService.getValue('debug'); - if (typeof debugConfigs.extensionHostDebugAdapter === 'boolean') { - return debugConfigs.extensionHostDebugAdapter; - } - /* - return !!debugConfigs.extensionHostDebugAdapter - || this.configurationManager.needsToRunInExtHost(this.type) - || (!!this.mainExtensionDescription && this.mainExtensionDescription.extensionLocation.scheme !== 'file'); - */ return true; } diff --git a/src/vs/workbench/contrib/format/browser/format.contribution.ts b/src/vs/workbench/contrib/format/browser/format.contribution.ts index 2b48a553eb..56e01a9ef8 100644 --- a/src/vs/workbench/contrib/format/browser/format.contribution.ts +++ b/src/vs/workbench/contrib/format/browser/format.contribution.ts @@ -3,6 +3,5 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/format.contribution'; import './formatActionsMultiple'; import './formatActionsNone'; diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 1fcd2667e9..610610c04b 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -6,28 +6,165 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { DocumentRangeFormattingEditProviderRegistry } from 'vs/editor/common/modes'; +import { DocumentRangeFormattingEditProviderRegistry, DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider } from 'vs/editor/common/modes'; import * as nls from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IQuickInputService, IQuickPickItem, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { formatDocumentRangeWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered } from 'vs/editor/contrib/format/format'; +import { formatDocumentRangeWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered, FormattingConflicts, FormattingMode } from 'vs/editor/contrib/format/format'; import { Range } from 'vs/editor/common/core/range'; -import { showExtensionQuery } from 'vs/workbench/contrib/format/browser/showExtensionQuery'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ITextModel } from 'vs/editor/common/model'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; +import { ILabelService } from 'vs/platform/label/common/label'; + +type FormattingEditProvider = DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider; + +class DefaultFormatter extends Disposable implements IWorkbenchContribution { + + static configName = 'editor.defaultFormatter'; + + static extensionIds: string[] = []; + static extensionDescriptions: string[] = []; + + constructor( + @IExtensionService private readonly _extensionService: IExtensionService, + @IConfigurationService private readonly _configService: IConfigurationService, + @INotificationService private readonly _notificationService: INotificationService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IModeService private readonly _modeService: IModeService, + @IStatusbarService private readonly _statusbarService: IStatusbarService, + @ILabelService private readonly _labelService: ILabelService, + ) { + super(); + this._register(this._extensionService.onDidChangeExtensions(this._updateConfigValues, this)); + this._register(FormattingConflicts.setFormatterSelector((formatter, document, mode) => this._selectFormatter(formatter, document, mode))); + this._updateConfigValues(); + } + + private async _updateConfigValues(): Promise { + const extensions = await this._extensionService.getExtensions(); + + DefaultFormatter.extensionIds.length = 0; + DefaultFormatter.extensionDescriptions.length = 0; + for (const extension of extensions) { + if (extension.main) { + DefaultFormatter.extensionIds.push(extension.identifier.value); + DefaultFormatter.extensionDescriptions.push(extension.description || ''); + } + } + } + + static _maybeQuotes(s: string): string { + return s.match(/\s/) ? `'${s}'` : s; + } + + private async _selectFormatter(formatter: T[], document: ITextModel, mode: FormattingMode): Promise { + + const defaultFormatterId = this._configService.getValue(DefaultFormatter.configName, { + resource: document.uri, + overrideIdentifier: document.getModeId() + }); + + if (defaultFormatterId) { + // good -> formatter configured + const [defaultFormatter] = formatter.filter(formatter => ExtensionIdentifier.equals(formatter.extensionId, defaultFormatterId)); + if (defaultFormatter) { + // formatter available + return defaultFormatter; + + } else { + // formatter gone + const extension = await this._extensionService.getExtension(defaultFormatterId); + const label = this._labelService.getUriLabel(document.uri, { relative: true }); + const message = extension + ? nls.localize('miss', "Extension '{0}' cannot format '{1}'", extension.displayName || extension.name, label) + : nls.localize('gone', "Extension '{0}' is configured as formatter but not available", defaultFormatterId); + this._statusbarService.setStatusMessage(message, 4000); + return undefined; + } + + } else if (formatter.length === 1) { + // ok -> nothing configured but only one formatter available + return formatter[0]; + } + + const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId(); + const silent = mode === FormattingMode.Silent; + const message = nls.localize('config.needed', "There are multiple formatters for {0}-files. Select a default formatter to continue.", DefaultFormatter._maybeQuotes(langName)); + + return new Promise((resolve, reject) => { + this._notificationService.prompt( + Severity.Info, + message, + [{ label: nls.localize('do.config', "Configure..."), run: () => this._pickAndPersistDefaultFormatter(formatter, document).then(resolve, reject) }], + { silent, onCancel: resolve } + ); + + if (silent) { + // don't wait when formatting happens without interaction + // but pick some formatter... + resolve(formatter[0]); + } + }); + } + + private async _pickAndPersistDefaultFormatter(formatter: T[], document: ITextModel): Promise { + const picks = formatter.map((formatter, index) => { + return { + index, + label: formatter.displayName || formatter.extensionId || '?' + }; + }); + const langName = this._modeService.getLanguageName(document.getModeId()) || document.getModeId(); + const pick = await this._quickInputService.pick(picks, { placeHolder: nls.localize('select', "Select a default formatter for {0}-files", DefaultFormatter._maybeQuotes(langName)) }); + if (!pick || !formatter[pick.index].extensionId) { + return undefined; + } + this._configService.updateValue(DefaultFormatter.configName, formatter[pick.index].extensionId!.value, { + resource: document.uri, + overrideIdentifier: document.getModeId() + }); + return formatter[pick.index]; + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution( + DefaultFormatter, + LifecyclePhase.Restored +); + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'editor', + order: 5, + type: 'object', + overridable: true, + properties: { + [DefaultFormatter.configName]: { + description: nls.localize('formatter.default', "Defines a default formatter which takes precedence over all other formatter settings. Must be the identifier of an extension contributing a formatter."), + type: 'string', + default: null, + enum: DefaultFormatter.extensionIds, + markdownEnumDescriptions: DefaultFormatter.extensionDescriptions + } + } +}); interface IIndexedPick extends IQuickPickItem { index: number; } -const openExtensionAction: IQuickInputButton = { - tooltip: nls.localize('show.ext', "Show extension..."), - iconClass: 'format-show-extension' -}; - function logFormatterTelemetry(telemetryService: ITelemetryService, mode: 'document' | 'range', options: T[], pick?: T) { function extKey(obj: T): string { @@ -48,6 +185,47 @@ function logFormatterTelemetry( }); } +async function showFormatterPick(accessor: ServicesAccessor, model: ITextModel, formatters: FormattingEditProvider[]): Promise { + const quickPickService = accessor.get(IQuickInputService); + const configService = accessor.get(IConfigurationService); + const modeService = accessor.get(IModeService); + + const overrides = { resource: model.uri, overrideIdentifier: model.getModeId() }; + const defaultFormatter = configService.getValue(DefaultFormatter.configName, overrides); + + const picks = formatters.map((provider, index) => { + return { + index, + label: provider.displayName || '', + description: ExtensionIdentifier.equals(provider.extensionId, defaultFormatter) ? nls.localize('def', "(default)") : undefined, + }; + }); + + const configurePick: IQuickPickItem = { + label: nls.localize('config', "Configure Default Formatter...") + }; + + const pick = await quickPickService.pick([...picks, { type: 'separator' }, configurePick], { placeHolder: nls.localize('format.placeHolder', "Select a formatter") }); + if (!pick) { + // dismissed + return undefined; + + } else if (pick === configurePick) { + // config default + const langName = modeService.getLanguageName(model.getModeId()) || model.getModeId(); + const pick = await quickPickService.pick(picks, { placeHolder: nls.localize('select', "Select a default formatter for {0}-files", DefaultFormatter._maybeQuotes(langName)) }); + if (pick && formatters[pick.index].extensionId) { + configService.updateValue(DefaultFormatter.configName, formatters[pick.index].extensionId!.value, overrides); + } + return undefined; + + } else { + // picked one + return (pick).index; + } + +} + registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { constructor() { @@ -68,32 +246,14 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { return; } const instaService = accessor.get(IInstantiationService); - const quickPickService = accessor.get(IQuickInputService); - const viewletService = accessor.get(IViewletService); const telemetryService = accessor.get(ITelemetryService); const model = editor.getModel(); - const provider = getRealAndSyntheticDocumentFormattersOrdered(model); - const picks = provider.map((provider, index) => { - return { - index, - label: provider.displayName || '', - buttons: [openExtensionAction] - }; - }); - - const pick = await quickPickService.pick(picks, { - placeHolder: nls.localize('format.placeHolder', "Select a formatter"), - onDidTriggerItemButton: (e) => { - const { extensionId } = provider[e.item.index]; - return showExtensionQuery(viewletService, `@id:${extensionId!.value}`); - } - }); + const pick = await instaService.invokeFunction(showFormatterPick, model, provider); if (pick) { - await instaService.invokeFunction(formatDocumentWithProvider, provider[pick.index], editor, CancellationToken.None); + await instaService.invokeFunction(formatDocumentWithProvider, provider[pick], editor, CancellationToken.None); } - - logFormatterTelemetry(telemetryService, 'document', provider, pick && provider[pick.index]); + logFormatterTelemetry(telemetryService, 'document', provider, typeof pick === 'number' && provider[pick] || undefined); } }); @@ -118,36 +278,20 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction { return; } const instaService = accessor.get(IInstantiationService); - const quickPickService = accessor.get(IQuickInputService); - const viewletService = accessor.get(IViewletService); const telemetryService = accessor.get(ITelemetryService); - const model = editor.getModel(); + const model = editor.getModel(); let range: Range = editor.getSelection(); if (range.isEmpty()) { range = new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber)); } const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model); - const picks = provider.map((provider, index) => { - return { - index, - label: provider.displayName || '', - buttons: [openExtensionAction] - }; - }); - - const pick = await quickPickService.pick(picks, { - placeHolder: nls.localize('format.placeHolder', "Select a formatter"), - onDidTriggerItemButton: (e) => { - const { extensionId } = provider[e.item.index]; - return showExtensionQuery(viewletService, `@id:${extensionId!.value}`); - } - }); + const pick = await instaService.invokeFunction(showFormatterPick, model, provider); if (pick) { - await instaService.invokeFunction(formatDocumentRangeWithProvider, provider[pick.index], editor, range, CancellationToken.None); + await instaService.invokeFunction(formatDocumentRangeWithProvider, provider[pick], editor, range, CancellationToken.None); } - logFormatterTelemetry(telemetryService, 'range', provider, pick && provider[pick.index]); + logFormatterTelemetry(telemetryService, 'range', provider, typeof pick === 'number' && provider[pick] || undefined); } }); diff --git a/src/vs/workbench/contrib/format/browser/media/configure-inverse.svg b/src/vs/workbench/contrib/format/browser/media/configure-inverse.svg deleted file mode 100644 index 61baaea2b8..0000000000 --- a/src/vs/workbench/contrib/format/browser/media/configure-inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/contrib/format/browser/media/configure.svg b/src/vs/workbench/contrib/format/browser/media/configure.svg deleted file mode 100644 index 3dec2ba50f..0000000000 --- a/src/vs/workbench/contrib/format/browser/media/configure.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/contrib/format/browser/media/format.contribution.css b/src/vs/workbench/contrib/format/browser/media/format.contribution.css deleted file mode 100644 index 74dab0e2bb..0000000000 --- a/src/vs/workbench/contrib/format/browser/media/format.contribution.css +++ /dev/null @@ -1,13 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .format-show-extension { - background-image: url('configure.svg'); -} - -.vs-dark .monaco-workbench .format-show-extension, -.hc-black .monaco-workbench .format-show-extension { - background-image: url('configure-inverse.svg'); -} diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 6f0e5afb6d..df08c1c32a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -1181,6 +1181,10 @@ export class SCMViewlet extends ViewContainerViewlet implements IViewModel { this.viewsModel.setVisible(MainPanel.ID, false); } + if (repositoryCount === 1) { + this.viewsModel.setVisible(this.viewDescriptors[0].id, true); + } + toggleClass(this.el, 'empty', repositoryCount === 0); this.repositoryCount = repositoryCount; } diff --git a/src/vs/workbench/contrib/search/common/queryBuilder.ts b/src/vs/workbench/contrib/search/common/queryBuilder.ts index d5fccc5624..942f4faa73 100644 --- a/src/vs/workbench/contrib/search/common/queryBuilder.ts +++ b/src/vs/workbench/contrib/search/common/queryBuilder.ts @@ -230,7 +230,7 @@ export class QueryBuilder { parseSearchPaths(pattern: string): ISearchPathsInfo { const isSearchPath = (segment: string) => { // A segment is a search path if it is an absolute path or starts with ./, ../, .\, or ..\ - return path.isAbsolute(segment) || /^\.\.?[\/\\]/.test(segment); + return path.isAbsolute(segment) || /^\.\.?([\/\\]|$)/.test(segment); }; const segments = splitGlobPattern(pattern) @@ -339,7 +339,7 @@ export class QueryBuilder { const workspaceUri = this.workspaceContextService.getWorkspace().folders[0].uri; searchPath = normalizeSlashes(searchPath); - if (strings.startsWith(searchPath, '../')) { + if (strings.startsWith(searchPath, '../') || searchPath === '..') { const resolvedPath = path.posix.resolve(workspaceUri.path, searchPath); return [{ searchPath: workspaceUri.with({ path: resolvedPath }) diff --git a/src/vs/workbench/contrib/search/test/common/queryBuilder.test.ts b/src/vs/workbench/contrib/search/test/common/queryBuilder.test.ts index b0196914af..4b5458e69f 100644 --- a/src/vs/workbench/contrib/search/test/common/queryBuilder.test.ts +++ b/src/vs/workbench/contrib/search/test/common/queryBuilder.test.ts @@ -667,6 +667,14 @@ suite('QueryBuilder', () => { }] } ], + [ + '..', + { + searchPaths: [{ + searchPath: getUri('/foo') + }] + } + ], [ '..\\bar', { diff --git a/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts index a466c7b25b..982077f427 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts @@ -458,7 +458,7 @@ export class TerminalTaskSystem implements ITaskSystem { const resolvedVariables = this.resolveVariablesFromSet(this.currentTask.systemInfo, this.currentTask.workspaceFolder!, task, variables); return resolvedVariables.then((resolvedVariables) => { - if (resolvedVariables) { + if (resolvedVariables && task.command && task.command.runtime) { this.currentTask.resolvedVariables = resolvedVariables; return this.executeInTerminal(task, trigger, new VariableResolver(this.currentTask.workspaceFolder!, this.currentTask.systemInfo, resolvedVariables.variables, this.configurationResolverService)); } else { diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index 866faa8717..6164e5c9e7 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -325,15 +325,15 @@ export class BulkEdit { } else if (!edit.newUri && edit.oldUri) { // delete file - if (!options.ignoreIfNotExists || await this._fileService.existsFile(edit.oldUri)) { + if (await this._fileService.existsFile(edit.oldUri)) { let useTrash = this._configurationService.getValue('files.enableTrash'); if (useTrash && !(await this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) { useTrash = false; // not supported by provider } - await this._textFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive }); + } else if (!options.ignoreIfNotExists) { + throw new Error(`${edit.oldUri} does not exist and can not be deleted`); } - } else if (edit.newUri && !edit.oldUri) { // create file if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.existsFile(edit.newUri)) { diff --git a/test/smoke/src/areas/extensions/extensions.test.ts b/test/smoke/src/areas/extensions/extensions.test.ts index 1ef8877a60..3cf595ce8f 100644 --- a/test/smoke/src/areas/extensions/extensions.test.ts +++ b/test/smoke/src/areas/extensions/extensions.test.ts @@ -20,7 +20,6 @@ export function setup() { await app.workbench.extensions.installExtension(extensionName); - await app.reload(); await app.workbench.extensions.waitForExtensionsViewlet(); await app.workbench.quickopen.runCommand('Smoke Test Check'); await app.workbench.statusbar.waitForStatusbarText('smoke test', 'VS Code Smoke Test Check'); diff --git a/test/smoke/src/vscode/code.ts b/test/smoke/src/vscode/code.ts index 56ed67a914..9f94c93d88 100644 --- a/test/smoke/src/vscode/code.ts +++ b/test/smoke/src/vscode/code.ts @@ -113,7 +113,6 @@ export async function spawn(options: SpawnOptions): Promise { '--skip-release-notes', '--sticky-quickopen', '--disable-telemetry', - '--disable-extensions', '--disable-updates', '--disable-crash-reporter', `--extensions-dir=${options.extensionsPath}`,