diff --git a/extensions/configuration-editing/src/extension.ts b/extensions/configuration-editing/src/extension.ts index 8b2574d474..20d34ef153 100644 --- a/extensions/configuration-editing/src/extension.ts +++ b/extensions/configuration-editing/src/extension.ts @@ -7,6 +7,7 @@ import { getLocation, parse, visit } from 'jsonc-parser'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { SettingsDocument } from './settingsDocumentHelper'; +import { provideInstalledExtensionProposals } from './extensionsProposals'; const localize = nls.loadMessageBundle(); export function activate(context: vscode.ExtensionContext): void { @@ -80,7 +81,7 @@ function registerExtensionsCompletionsInExtensionsDocument(): vscode.Disposable const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); if (location.path[0] === 'recommendations') { const extensionsContent = parse(document.getText()); - return provideInstalledExtensionProposals(extensionsContent, range); + return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], range, false); } return []; } @@ -94,41 +95,13 @@ function registerExtensionsCompletionsInWorkspaceConfigurationDocument(): vscode const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); if (location.path[0] === 'extensions' && location.path[1] === 'recommendations') { const extensionsContent = parse(document.getText())['extensions']; - return provideInstalledExtensionProposals(extensionsContent, range); + return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], range, false); } return []; } }); } -function provideInstalledExtensionProposals(extensionsContent: IExtensionsContent, range: vscode.Range): vscode.ProviderResult { - const alreadyEnteredExtensions = extensionsContent && extensionsContent.recommendations || []; - if (Array.isArray(alreadyEnteredExtensions)) { - const knownExtensionProposals = vscode.extensions.all.filter(e => - !(e.id.startsWith('vscode.') - || e.id === 'Microsoft.vscode-markdown' - || alreadyEnteredExtensions.indexOf(e.id) > -1)); - if (knownExtensionProposals.length) { - return knownExtensionProposals.map(e => { - const item = new vscode.CompletionItem(e.id); - const insertText = `"${e.id}"`; - item.kind = vscode.CompletionItemKind.Value; - item.insertText = insertText; - item.range = range; - item.filterText = insertText; - return item; - }); - } else { - const example = new vscode.CompletionItem(localize('exampleExtension', "Example")); - example.insertText = '"vscode.csharp"'; - example.kind = vscode.CompletionItemKind.Value; - example.range = range; - return [example]; - } - } - return undefined; -} - vscode.languages.registerDocumentSymbolProvider({ pattern: '**/launch.json', language: 'jsonc' }, { provideDocumentSymbols(document: vscode.TextDocument, _token: vscode.CancellationToken): vscode.ProviderResult { const result: vscode.SymbolInformation[] = []; diff --git a/extensions/configuration-editing/src/extensionsProposals.ts b/extensions/configuration-editing/src/extensionsProposals.ts new file mode 100644 index 0000000000..f447303f85 --- /dev/null +++ b/extensions/configuration-editing/src/extensionsProposals.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + + +export function provideInstalledExtensionProposals(existing: string[], range: vscode.Range, includeBuiltinExtensions: boolean): vscode.ProviderResult { + if (Array.isArray(existing)) { + const extensions = includeBuiltinExtensions ? vscode.extensions.all : vscode.extensions.all.filter(e => !(e.id.startsWith('vscode.') || e.id === 'Microsoft.vscode-markdown')); + const knownExtensionProposals = extensions.filter(e => existing.indexOf(e.id) === -1); + if (knownExtensionProposals.length) { + return knownExtensionProposals.map(e => { + const item = new vscode.CompletionItem(e.id); + const insertText = `"${e.id}"`; + item.kind = vscode.CompletionItemKind.Value; + item.insertText = insertText; + item.range = range; + item.filterText = insertText; + return item; + }); + } else { + const example = new vscode.CompletionItem(localize('exampleExtension', "Example")); + example.insertText = '"vscode.csharp"'; + example.kind = vscode.CompletionItemKind.Value; + example.range = range; + return [example]; + } + } + return undefined; +} + diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index 918b7b2e04..16c9c8f4a0 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { getLocation, Location } from 'jsonc-parser'; +import { getLocation, Location, parse } from 'jsonc-parser'; import * as nls from 'vscode-nls'; +import { provideInstalledExtensionProposals } from './extensionsProposals'; const localize = nls.loadMessageBundle(); @@ -13,7 +14,7 @@ export class SettingsDocument { constructor(private document: vscode.TextDocument) { } - public provideCompletionItems(position: vscode.Position, _token: vscode.CancellationToken): vscode.ProviderResult { + public provideCompletionItems(position: vscode.Position, _token: vscode.CancellationToken): vscode.ProviderResult { const location = getLocation(this.document.getText(), this.document.offsetAt(position)); const range = this.document.getWordRangeAtPosition(position) || new vscode.Range(position, position); @@ -41,6 +42,15 @@ export class SettingsDocument { }); } + // sync.ignoredExtensions + if (location.path[0] === 'sync.ignoredExtensions') { + let ignoredExtensions = []; + try { + ignoredExtensions = parse(this.document.getText())['sync.ignoredExtensions']; + } catch (e) {/* ignore error */ } + return provideInstalledExtensionProposals(ignoredExtensions, range, true); + } + return this.provideLanguageOverridesCompletionItems(location, position); } diff --git a/extensions/git/package.json b/extensions/git/package.json index 5e8bf47d71..1eaf39ad44 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1847,7 +1847,16 @@ { "view": "workbench.scm", "contents": "%view.workbench.scm.workspace%", - "when": "config.git.enabled && !git.missing && workbenchState == workspace" + "when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount != 0" + }, + { + "view": "workbench.scm", + "contents": "%view.workbench.scm.emptyWorkspace%", + "when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount == 0" + }, + { + "view": "workbench.explorer.emptyView", + "contents": "%view.workbench.cloneRepository%" } ] }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 53325c0f5f..6328edd99b 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -152,7 +152,9 @@ "colors.submodule": "Color for submodule resources.", "view.workbench.scm.missing": "A valid git installation was not detected, more details can be found in the [git output](command:git.showOutput).\nPlease [install git](https://git-scm.com/), or learn more about how to use Git and source control in VS Code in [our docs](https://aka.ms/vscode-scm).\nIf you're using a different version control system, you can [search the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22) for additional extensions.", "view.workbench.scm.disabled": "If you would like to use git features, please enable git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.empty": "In order to use git features, you can open a folder containing a git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone from URL](command:git.clone)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.folder": "The folder currently open doesn't have a git repository.\n[Initialize Repository](command:git.init)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.workspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Initialize Repository](command:git.init)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm)." + "view.workbench.scm.empty": "In order to use git features, you can open a folder containing a git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone Repository](command:git.clone)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.folder": "The folder currently open doesn't have a git repository.\n[Initialize Repository](command:git.init?%5Btrue%5D)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.workspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Initialize Repository](command:git.init)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.emptyWorkspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Add Folder to Workspace](command:workbench.action.addRootFolder)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.cloneRepository": "You can also clone a repository from a URL. To learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).\n[Clone Repository](command:git.clone)" } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 281190c1c6..ecce3fbc74 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -566,24 +566,29 @@ export class CommandCenter { } @command('git.init') - async init(): Promise { + async init(skipFolderPrompt = false): Promise { let repositoryPath: string | undefined = undefined; let askToOpen = true; if (workspace.workspaceFolders) { - const placeHolder = localize('init', "Pick workspace folder to initialize git repo in"); - const pick = { label: localize('choose', "Choose Folder...") }; - const items: { label: string, folder?: WorkspaceFolder }[] = [ - ...workspace.workspaceFolders.map(folder => ({ label: folder.name, description: folder.uri.fsPath, folder })), - pick - ]; - const item = await window.showQuickPick(items, { placeHolder, ignoreFocusOut: true }); - - if (!item) { - return; - } else if (item.folder) { - repositoryPath = item.folder.uri.fsPath; + if (skipFolderPrompt && workspace.workspaceFolders.length === 1) { + repositoryPath = workspace.workspaceFolders[0].uri.fsPath; askToOpen = false; + } else { + const placeHolder = localize('init', "Pick workspace folder to initialize git repo in"); + const pick = { label: localize('choose', "Choose Folder...") }; + const items: { label: string, folder?: WorkspaceFolder }[] = [ + ...workspace.workspaceFolders.map(folder => ({ label: folder.name, description: folder.uri.fsPath, folder })), + pick + ]; + const item = await window.showQuickPick(items, { placeHolder, ignoreFocusOut: true }); + + if (!item) { + return; + } else if (item.folder) { + repositoryPath = item.folder.uri.fsPath; + askToOpen = false; + } } } diff --git a/extensions/vscode-account/package.json b/extensions/vscode-account/package.json index 1cc728195e..c4df52ce73 100644 --- a/extensions/vscode-account/package.json +++ b/extensions/vscode-account/package.json @@ -1,8 +1,8 @@ { "name": "vscode-account", "publisher": "vscode", - "displayName": "Account", - "description": "", + "displayName": "%displayName%", + "description": "%description%", "version": "0.0.1", "engines": { "vscode": "^1.42.0" @@ -15,6 +15,20 @@ "*" ], "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "microsoft.signin", + "title": "%signIn%", + "category": "%displayName%" + }, + { + "command": "microsoft.signout", + "title": "%signOut%", + "category": "%displayName%" + } + ] + }, "scripts": { "vscode:prepublish": "npm run compile", "compile": "gulp compile-extension:vscode-account", diff --git a/extensions/vscode-account/package.nls.json b/extensions/vscode-account/package.nls.json new file mode 100644 index 0000000000..8211a3f6e9 --- /dev/null +++ b/extensions/vscode-account/package.nls.json @@ -0,0 +1,6 @@ +{ + "displayName": "Microsoft Account", + "description": "Microsoft authentication provider", + "signIn": "Sign in", + "signOut": "Sign out" +} diff --git a/extensions/vscode-account/src/extension.ts b/extensions/vscode-account/src/extension.ts index e38a45bd8f..0f14f377de 100644 --- a/extensions/vscode-account/src/extension.ts +++ b/extensions/vscode-account/src/extension.ts @@ -6,13 +6,15 @@ import * as vscode from 'vscode'; import { AzureActiveDirectoryService, onDidChangeSessions } from './AADHelper'; -export async function activate(_: vscode.ExtensionContext) { +export const DEFAULT_SCOPES = 'https://management.core.windows.net/.default offline_access'; + +export async function activate(context: vscode.ExtensionContext) { const loginService = new AzureActiveDirectoryService(); await loginService.initialize(); - vscode.authentication.registerAuthenticationProvider({ + context.subscriptions.push(vscode.authentication.registerAuthenticationProvider({ id: 'MSA', displayName: 'Microsoft', onDidChangeSessions: onDidChangeSessions.event, @@ -28,7 +30,37 @@ export async function activate(_: vscode.ExtensionContext) { logout: async (id: string) => { return loginService.logout(id); } - }); + })); + + context.subscriptions.push(vscode.commands.registerCommand('microsoft.signin', () => { + return loginService.login(DEFAULT_SCOPES); + })); + + context.subscriptions.push(vscode.commands.registerCommand('microsoft.signout', async () => { + const sessions = loginService.sessions; + if (sessions.length === 0) { + return; + } + + if (sessions.length === 1) { + await loginService.logout(loginService.sessions[0].id); + onDidChangeSessions.fire(); + return; + } + + const selectedSession = await vscode.window.showQuickPick(sessions.map(session => { + return { + id: session.id, + label: session.accountName + }; + })); + + if (selectedSession) { + await loginService.logout(selectedSession.id); + onDidChangeSessions.fire(); + return; + } + })); return; } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index 6ec71738df..a17c28cf44 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -5,7 +5,7 @@ @font-face { font-family: "codicon"; - src: url("./codicon.ttf?d0510f6ecacbb2788db2b3162273a3d8") format("truetype"); + src: url("./codicon.ttf?279add2ec8b3d516ca20a123230cbf9f") format("truetype"); } .codicon[class*='codicon-'] { @@ -303,6 +303,7 @@ .codicon-paintcan:before { content: "\eb2a" } .codicon-pin:before { content: "\eb2b" } .codicon-play:before { content: "\eb2c" } +.codicon-run:before { content: "\eb2c" } .codicon-plug:before { content: "\eb2d" } .codicon-preserve-case:before { content: "\eb2e" } .codicon-preview:before { content: "\eb2f" } @@ -413,5 +414,6 @@ .codicon-feedback:before { content: "\eb96" } .codicon-group-by-ref-type:before { content: "\eb97" } .codicon-ungroup-by-ref-type:before { content: "\eb98" } -.codicon-debug-alt-2:before { content: "\f101" } -.codicon-debug-alt:before { content: "\f102" } +.codicon-bell-dot:before { content: "\f101" } +.codicon-debug-alt-2:before { content: "\f102" } +.codicon-debug-alt:before { content: "\f103" } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index f712f5cd05..df86f7d4d9 100644 Binary files a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf and b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 2bc82bb539..ab85513935 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -52,6 +52,7 @@ export interface IListViewOptions { readonly useShadows?: boolean; readonly verticalScrollMode?: ScrollbarVisibility; readonly setRowLineHeight?: boolean; + readonly setRowHeight?: boolean; readonly supportDynamicHeights?: boolean; readonly mouseSupport?: boolean; readonly horizontalScrolling?: boolean; @@ -63,6 +64,7 @@ const DefaultOptions = { useShadows: true, verticalScrollMode: ScrollbarVisibility.Auto, setRowLineHeight: true, + setRowHeight: true, supportDynamicHeights: false, dnd: { getDragElements(e: T) { return [e]; }, @@ -174,6 +176,7 @@ export class ListView implements ISpliceable, IDisposable { private dragOverAnimationStopDisposable: IDisposable = Disposable.None; private dragOverMouseY: number = 0; private setRowLineHeight: boolean; + private setRowHeight: boolean; private supportDynamicHeights: boolean; private horizontalScrolling: boolean; private additionalScrollHeight: number; @@ -262,6 +265,7 @@ export class ListView implements ISpliceable, IDisposable { domEvent(window, 'dragend')(this.onDragEnd, this, this.disposables); this.setRowLineHeight = getOrDefault(options, o => o.setRowLineHeight, DefaultOptions.setRowLineHeight); + this.setRowHeight = getOrDefault(options, o => o.setRowHeight, DefaultOptions.setRowHeight); this.supportDynamicHeights = getOrDefault(options, o => o.supportDynamicHeights, DefaultOptions.supportDynamicHeights); this.dnd = getOrDefault, IListViewDragAndDrop>(options, o => o.dnd, DefaultOptions.dnd); @@ -614,7 +618,10 @@ export class ListView implements ISpliceable, IDisposable { private updateItemInDOM(item: IItem, index: number): void { item.row!.domNode!.style.top = `${this.elementTop(index)}px`; - item.row!.domNode!.style.height = `${item.size}px`; + + if (this.setRowHeight) { + item.row!.domNode!.style.height = `${item.size}px`; + } if (this.setRowLineHeight) { item.row!.domNode!.style.lineHeight = `${item.size}px`; diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 4f4aa38b9b..1b7a4af75e 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -847,6 +847,7 @@ export interface IListOptions { readonly useShadows?: boolean; readonly verticalScrollMode?: ScrollbarVisibility; readonly setRowLineHeight?: boolean; + readonly setRowHeight?: boolean; readonly supportDynamicHeights?: boolean; readonly mouseSupport?: boolean; readonly horizontalScrolling?: boolean; diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index aa6e6a7896..5bd32b7446 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -453,8 +453,7 @@ export class QuickInputList { if ((what === 'Previous' || what === 'PreviousPage') && this.list.getFocus()[0] === 0) { what = 'Last'; } - - (this.list as any)['focus' + what](); + this.list['focus' + what as 'focusFirst' | 'focusLast' | 'focusNext' | 'focusPrevious' | 'focusNextPage' | 'focusPreviousPage'](); this.list.reveal(this.list.getFocus()[0]); } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 2a927b74be..1566972f1e 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -677,8 +677,8 @@ export class CodeApplication extends Disposable { const noRecentEntry = args['skip-add-to-recently-opened'] === true; const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined; - // new window if "-n" was used without paths - if (args['new-window'] && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { + // new window if "-n" or "--remote" was used without paths + if ((args['new-window'] || args.remote) && !hasCliArgs && !hasFolderURIs && !hasFileURIs) { return windowsMainService.open({ context, cli: args, diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 170e1774bc..654d61326b 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -86,7 +86,7 @@ class VisualEditorState { constructor( private _contextMenuService: IContextMenuService, - private _clipboardService: IClipboardService | null + private _clipboardService: IClipboardService ) { this._zones = []; this.inlineDiffMargins = []; @@ -136,7 +136,7 @@ class VisualEditorState { this._zones.push(zoneId); this._zonesMap[String(zoneId)] = true; - if (newDecorations.zones[i].diff && viewZone.marginDomNode && this._clipboardService) { + if (newDecorations.zones[i].diff && viewZone.marginDomNode) { viewZone.suppressMouseDown = false; this.inlineDiffMargins.push(new InlineDiffMargin(zoneId, viewZone.marginDomNode, editor, newDecorations.zones[i].diff!, this._contextMenuService, this._clipboardService)); } @@ -223,7 +223,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE constructor( domElement: HTMLElement, options: IDiffEditorOptions, - clipboardService: IClipboardService | null, + @IClipboardService clipboardService: IClipboardService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index c8dc0ea75f..e627663961 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -12,10 +12,10 @@ import { DeleteOperations } from 'vs/editor/common/controller/cursorDeleteOperat import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { TypeOperations, TypeWithAutoClosingCommand } from 'vs/editor/common/controller/cursorTypeOperations'; import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { Range, IRange } from 'vs/editor/common/core/range'; import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, ICursorStateComputer } from 'vs/editor/common/model'; +import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation } from 'vs/editor/common/model'; import { RawContentChangedType } from 'vs/editor/common/model/textModelEvents'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; @@ -903,8 +903,8 @@ class CommandExecutor { if (commandsData.hadTrackedEditOperation && filteredOperations.length > 0) { filteredOperations[0]._isTracked = true; } - let selectionsAfter = ctx.model.pushEditOperations(ctx.selectionsBefore, filteredOperations, (inverseEditOperations: IIdentifiedSingleEditOperation[]): Selection[] => { - let groupedInverseEditOperations: IIdentifiedSingleEditOperation[][] = []; + let selectionsAfter = ctx.model.pushEditOperations(ctx.selectionsBefore, filteredOperations, (inverseEditOperations: IValidEditOperation[]): Selection[] => { + let groupedInverseEditOperations: IValidEditOperation[][] = []; for (let i = 0; i < ctx.selectionsBefore.length; i++) { groupedInverseEditOperations[i] = []; } @@ -915,7 +915,7 @@ class CommandExecutor { } groupedInverseEditOperations[op.identifier.major].push(op); } - const minorBasedSorter = (a: IIdentifiedSingleEditOperation, b: IIdentifiedSingleEditOperation) => { + const minorBasedSorter = (a: IValidEditOperation, b: IValidEditOperation) => { return a.identifier!.minor - b.identifier!.minor; }; let cursorSelections: Selection[] = []; @@ -1000,8 +1000,8 @@ class CommandExecutor { let operations: IIdentifiedSingleEditOperation[] = []; let operationMinor = 0; - const addEditOperation = (selection: Range, text: string | null, forceMoveMarkers: boolean = false) => { - if (selection.isEmpty() && text === '') { + const addEditOperation = (range: IRange, text: string | null, forceMoveMarkers: boolean = false) => { + if (Range.isEmpty(range) && text === '') { // This command wants to add a no-op => no thank you return; } @@ -1010,7 +1010,7 @@ class CommandExecutor { major: majorIdentifier, minor: operationMinor++ }, - range: selection, + range: range, text: text, forceMoveMarkers: forceMoveMarkers, isAutoWhitespaceEdit: command.insertsAutoWhitespace @@ -1018,12 +1018,13 @@ class CommandExecutor { }; let hadTrackedEditOperation = false; - const addTrackedEditOperation = (selection: Range, text: string | null, forceMoveMarkers?: boolean) => { + const addTrackedEditOperation = (selection: IRange, text: string | null, forceMoveMarkers?: boolean) => { hadTrackedEditOperation = true; addEditOperation(selection, text, forceMoveMarkers); }; - const trackSelection = (selection: Selection, trackPreviousOnEmpty?: boolean) => { + const trackSelection = (_selection: ISelection, trackPreviousOnEmpty?: boolean) => { + const selection = Selection.liftSelection(_selection); let stickiness: TrackedRangeStickiness; if (selection.isEmpty()) { if (typeof trackPreviousOnEmpty === 'boolean') { @@ -1093,7 +1094,7 @@ class CommandExecutor { const previousOp = operations[i - 1]; const currentOp = operations[i]; - if (previousOp.range.getStartPosition().isBefore(currentOp.range.getEndPosition())) { + if (Range.getStartPosition(previousOp.range).isBefore(Range.getEndPosition(currentOp.range))) { let loserMajor: number; diff --git a/src/vs/editor/common/core/range.ts b/src/vs/editor/common/core/range.ts index fb5cbd813c..ff42818662 100644 --- a/src/vs/editor/common/core/range.ts +++ b/src/vs/editor/common/core/range.ts @@ -264,14 +264,28 @@ export class Range { * Return the end position (which will be after or equal to the start position) */ public getEndPosition(): Position { - return new Position(this.endLineNumber, this.endColumn); + return Range.getEndPosition(this); + } + + /** + * Return the end position (which will be after or equal to the start position) + */ + public static getEndPosition(range: IRange): Position { + return new Position(range.endLineNumber, range.endColumn); } /** * Return the start position (which will be before or equal to the end position) */ public getStartPosition(): Position { - return new Position(this.startLineNumber, this.startColumn); + return Range.getStartPosition(this); + } + + /** + * Return the start position (which will be before or equal to the end position) + */ + public static getStartPosition(range: IRange): Position { + return new Position(range.startLineNumber, range.startColumn); } /** diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 392160f547..4f17a5f98b 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -10,7 +10,7 @@ import { ConfigurationChangedEvent, IComputedEditorOptions, IEditorOptions } fro import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; -import { IIdentifiedSingleEditOperation, IModelDecorationsChangeAccessor, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { IModelDecorationsChangeAccessor, ITextModel, OverviewRulerLane, TrackedRangeStickiness, IValidEditOperation } from 'vs/editor/common/model'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; /** @@ -22,7 +22,7 @@ export interface IEditOperationBuilder { * @param range The range to replace (delete). May be empty to represent a simple insert. * @param text The text to replace with. May be null to represent a simple delete. */ - addEditOperation(range: Range, text: string | null, forceMoveMarkers?: boolean): void; + addEditOperation(range: IRange, text: string | null, forceMoveMarkers?: boolean): void; /** * Add a new edit operation (a replace operation). @@ -30,7 +30,7 @@ export interface IEditOperationBuilder { * @param range The range to replace (delete). May be empty to represent a simple insert. * @param text The text to replace with. May be null to represent a simple delete. */ - addTrackedEditOperation(range: Range, text: string | null, forceMoveMarkers?: boolean): void; + addTrackedEditOperation(range: IRange, text: string | null, forceMoveMarkers?: boolean): void; /** * Track `selection` when applying edit operations. @@ -51,7 +51,7 @@ export interface ICursorStateComputerData { /** * Get the inverse edit operations of the added edit operations. */ - getInverseEditOperations(): IIdentifiedSingleEditOperation[]; + getInverseEditOperations(): IValidEditOperation[]; /** * Get a previously tracked selection. * @param id The unique identifier returned by `trackSelection`. diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index dc970cea98..41b3931f7a 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -335,7 +335,7 @@ export interface IIdentifiedSingleEditOperation { /** * The range to replace. This can be empty to emulate a simple insert. */ - range: Range; + range: IRange; /** * The text to replace with. This can be null to emulate a simple delete. */ @@ -358,6 +358,27 @@ export interface IIdentifiedSingleEditOperation { _isTracked?: boolean; } +export interface IValidEditOperation { + /** + * An identifier associated with this single edit operation. + * @internal + */ + identifier: ISingleEditOperationIdentifier | null; + /** + * The range to replace. This can be empty to emulate a simple insert. + */ + range: Range; + /** + * The text to replace with. This can be null to emulate a simple delete. + */ + text: string | null; + /** + * This indicates that this operation has "insert" semantics. + * i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved. + */ + forceMoveMarkers: boolean; +} + /** * A callback that can compute the cursor state after applying a series of edit operations. */ @@ -365,7 +386,7 @@ export interface ICursorStateComputer { /** * A callback that can compute the resulting cursors state after some edit operations have been executed. */ - (inverseEditOperations: IIdentifiedSingleEditOperation[]): Selection[] | null; + (inverseEditOperations: IValidEditOperation[]): Selection[] | null; } export class TextModelResolvedOptions { @@ -1063,7 +1084,7 @@ export interface ITextModel { * @param operations The edit operations. * @return The inverse edit operations, that, when applied, will bring the model back to the previous state. */ - applyEdits(operations: IIdentifiedSingleEditOperation[]): IIdentifiedSingleEditOperation[]; + applyEdits(operations: IIdentifiedSingleEditOperation[]): IValidEditOperation[]; /** * Change the end of line sequence without recording in the undo stack. @@ -1206,6 +1227,20 @@ export const enum ModelConstants { FIRST_LINE_DETECTION_LENGTH_LIMIT = 1000 } +/** + * @internal + */ +export class ValidAnnotatedEditOperation implements IIdentifiedSingleEditOperation { + constructor( + public readonly identifier: ISingleEditOperationIdentifier | null, + public readonly range: Range, + public readonly text: string | null, + public readonly forceMoveMarkers: boolean, + public readonly isAutoWhitespaceEdit: boolean, + public readonly _isTracked: boolean, + ) { } +} + /** * @internal */ @@ -1234,7 +1269,7 @@ export interface ITextBuffer { getLineLastNonWhitespaceColumn(lineNumber: number): number; setEOL(newEOL: '\r\n' | '\n'): void; - applyEdits(rawOperations: IIdentifiedSingleEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult; + applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult; findMatchesLineByLine(searchRange: Range, searchData: SearchData, captureMatches: boolean, limitResultCount: number): FindMatch[]; } @@ -1244,7 +1279,7 @@ export interface ITextBuffer { export class ApplyEditsResult { constructor( - public readonly reverseEdits: IIdentifiedSingleEditOperation[], + public readonly reverseEdits: IValidEditOperation[], public readonly changes: IInternalModelContentChange[], public readonly trimAutoWhitespaceLineNumbers: number[] | null ) { } diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index 8bc4b12381..9a803c4ac5 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -5,11 +5,11 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Selection } from 'vs/editor/common/core/selection'; -import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; +import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; interface IEditOperation { - operations: IIdentifiedSingleEditOperation[]; + operations: IValidEditOperation[]; } interface IStackElement { @@ -174,7 +174,7 @@ export class EditStack { return stackElement!.afterCursorState; } - private static _computeCursorState(cursorStateComputer: ICursorStateComputer | null, inverseEditOperations: IIdentifiedSingleEditOperation[]): Selection[] | null { + private static _computeCursorState(cursorStateComputer: ICursorStateComputer | null, inverseEditOperations: IValidEditOperation[]): Selection[] | null { try { return cursorStateComputer ? cursorStateComputer(inverseEditOperations) : null; } catch (e) { diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 9e4c3af46b..8a184ddd4f 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -6,7 +6,7 @@ import * as strings from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { ApplyEditsResult, EndOfLinePreference, FindMatch, IIdentifiedSingleEditOperation, IInternalModelContentChange, ISingleEditOperationIdentifier, ITextBuffer, ITextSnapshot } from 'vs/editor/common/model'; +import { ApplyEditsResult, EndOfLinePreference, FindMatch, IInternalModelContentChange, ISingleEditOperationIdentifier, ITextBuffer, ITextSnapshot, ValidAnnotatedEditOperation, IValidEditOperation } from 'vs/editor/common/model'; import { PieceTreeBase, StringBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase'; import { SearchData } from 'vs/editor/common/model/textModelSearch'; @@ -21,7 +21,7 @@ export interface IValidatedEditOperation { isAutoWhitespaceEdit: boolean; } -export interface IReverseSingleEditOperation extends IIdentifiedSingleEditOperation { +export interface IReverseSingleEditOperation extends IValidEditOperation { sortIndex: number; } @@ -201,7 +201,7 @@ export class PieceTreeTextBuffer implements ITextBuffer { this._pieceTree.setEOL(newEOL); } - public applyEdits(rawOperations: IIdentifiedSingleEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult { + public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean): ApplyEditsResult { let mightContainRTL = this._mightContainRTL; let mightContainNonBasicASCII = this._mightContainNonBasicASCII; let canReduceOperations = true; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 91787ebfca..a7ab45f793 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1154,18 +1154,40 @@ export class TextModel extends Disposable implements model.ITextModel { } } + private _validateEditOperation(rawOperation: model.IIdentifiedSingleEditOperation): model.ValidAnnotatedEditOperation { + if (rawOperation instanceof model.ValidAnnotatedEditOperation) { + return rawOperation; + } + return new model.ValidAnnotatedEditOperation( + rawOperation.identifier || null, + this.validateRange(rawOperation.range), + rawOperation.text, + rawOperation.forceMoveMarkers || false, + rawOperation.isAutoWhitespaceEdit || false, + rawOperation._isTracked || false + ); + } + + private _validateEditOperations(rawOperations: model.IIdentifiedSingleEditOperation[]): model.ValidAnnotatedEditOperation[] { + const result: model.ValidAnnotatedEditOperation[] = []; + for (let i = 0, len = rawOperations.length; i < len; i++) { + result[i] = this._validateEditOperation(rawOperations[i]); + } + return result; + } + public pushEditOperations(beforeCursorState: Selection[], editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { try { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); - return this._pushEditOperations(beforeCursorState, editOperations, cursorStateComputer); + return this._pushEditOperations(beforeCursorState, this._validateEditOperations(editOperations), cursorStateComputer); } finally { this._eventEmitter.endDeferredEmit(); this._onDidChangeDecorations.endDeferredEmit(); } } - private _pushEditOperations(beforeCursorState: Selection[], editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { + private _pushEditOperations(beforeCursorState: Selection[], editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { if (this._options.trimAutoWhitespace && this._trimAutoWhitespaceLines) { // Go through each saved line number and insert a trim whitespace edit // if it is safe to do so (no conflicts with other edits). @@ -1238,10 +1260,8 @@ export class TextModel extends Disposable implements model.ITextModel { } if (allowTrimLine) { - editOperations.push({ - range: new Range(trimLineNumber, 1, trimLineNumber, maxLineColumn), - text: null - }); + const trimRange = new Range(trimLineNumber, 1, trimLineNumber, maxLineColumn); + editOperations.push(new model.ValidAnnotatedEditOperation(null, trimRange, null, false, false, false)); } } @@ -1252,21 +1272,18 @@ export class TextModel extends Disposable implements model.ITextModel { return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer); } - public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[]): model.IIdentifiedSingleEditOperation[] { + public applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[]): model.IValidEditOperation[] { try { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); - return this._applyEdits(rawOperations); + return this._applyEdits(this._validateEditOperations(rawOperations)); } finally { this._eventEmitter.endDeferredEmit(); this._onDidChangeDecorations.endDeferredEmit(); } } - private _applyEdits(rawOperations: model.IIdentifiedSingleEditOperation[]): model.IIdentifiedSingleEditOperation[] { - for (let i = 0, len = rawOperations.length; i < len; i++) { - rawOperations[i].range = this.validateRange(rawOperations[i].range); - } + private _applyEdits(rawOperations: model.ValidAnnotatedEditOperation[]): model.IValidEditOperation[] { const oldLineCount = this._buffer.getLineCount(); const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index aa0a70e307..2e50a75ebe 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1324,7 +1324,7 @@ export interface WorkspaceEditMetadata { needsConfirmation: boolean; label: string; description?: string; - iconPath?: { id: string } | { light: URI, dark: URI }; + iconPath?: { id: string } | URI | { light: URI, dark: URI }; } export interface WorkspaceFileEditOptions { diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 7b460d7737..8b1eab6448 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; -import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model'; +import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions, IValidEditOperation } from 'vs/editor/common/model'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { IModelLanguageChangedEvent, IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { LanguageIdentifier, DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokensLegend, SemanticTokens, SemanticTokensEdits, TokenMetadata, FontStyle, MetadataConsts } from 'vs/editor/common/modes'; @@ -305,7 +305,7 @@ export class ModelServiceImpl extends Disposable implements IModelService { model.pushEditOperations( [], ModelServiceImpl._computeEdits(model, textBuffer), - (inverseEditOperations: IIdentifiedSingleEditOperation[]) => [] + (inverseEditOperations: IValidEditOperation[]) => [] ); model.pushStackElement(); } diff --git a/src/vs/editor/contrib/comment/blockCommentCommand.ts b/src/vs/editor/contrib/comment/blockCommentCommand.ts index dc6fe03a21..9162b29ee0 100644 --- a/src/vs/editor/contrib/comment/blockCommentCommand.ts +++ b/src/vs/editor/contrib/comment/blockCommentCommand.ts @@ -9,7 +9,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon'; -import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; +import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; export class BlockCommentCommand implements ICommand { diff --git a/src/vs/editor/contrib/comment/lineCommentCommand.ts b/src/vs/editor/contrib/comment/lineCommentCommand.ts index 6f3ea597b2..da116ec74d 100644 --- a/src/vs/editor/contrib/comment/lineCommentCommand.ts +++ b/src/vs/editor/contrib/comment/lineCommentCommand.ts @@ -205,7 +205,7 @@ export class LineCommentCommand implements ICommand { for (let i = 0, len = ops.length; i < len; i++) { builder.addEditOperation(ops[i].range, ops[i].text); - if (ops[i].range.isEmpty() && ops[i].range.getStartPosition().equals(cursorPosition)) { + if (Range.isEmpty(ops[i].range) && Range.getStartPosition(ops[i].range).equals(cursorPosition)) { const lineContent = model.getLineContent(cursorPosition.lineNumber); if (lineContent.length + 1 === cursorPosition.column) { this._deltaColumn = (ops[i].text || '').length; diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index b1b4f8f6de..cfd2349263 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -25,6 +25,10 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import * as resources from 'vs/base/common/resources'; +import * as strings from 'vs/base/common/strings'; function getHoverMessage(link: Link, useMetaKey: boolean): MarkdownString { const executeCmd = link.url && /^command:/i.test(link.url.toString()); @@ -291,7 +295,29 @@ class LinkDetector implements IEditorContribution { const { link } = occurrence; link.resolve(CancellationToken.None).then(uri => { - // open the uri + + // Support for relative file URIs of the shape file://./relativeFile.txt or file:///./relativeFile.txt + if (typeof uri === 'string' && this.editor.hasModel()) { + const modelUri = this.editor.getModel().uri; + if (modelUri.scheme === Schemas.file && strings.startsWith(uri, 'file:')) { + const parsedUri = URI.parse(uri); + if (parsedUri.scheme === Schemas.file) { + const fsPath = resources.originalFSPath(parsedUri); + + let relativePath: string | null = null; + if (strings.startsWith(fsPath, '/./')) { + relativePath = `.${fsPath.substr(1)}`; + } else if (strings.startsWith(fsPath, '//./')) { + relativePath = `.${fsPath.substr(2)}`; + } + + if (relativePath) { + uri = resources.joinPath(modelUri, relativePath); + } + } + } + } + return this.openerService.open(uri, { openToSide, fromUserGesture }); }, err => { diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index da171faea1..81dcc5e766 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -26,7 +26,7 @@ import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService import { ITextResourceConfigurationService, ITextResourcePropertiesService, ITextResourceConfigurationChangeEvent } from 'vs/editor/common/services/textResourceConfigurationService'; import { CommandsRegistry, ICommand, ICommandEvent, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationChangeEvent, IConfigurationData, IConfigurationOverrides, IConfigurationService, IConfigurationModel, IConfigurationValue, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { Configuration, ConfigurationModel, DefaultConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; +import { Configuration, ConfigurationModel, DefaultConfigurationModel, ConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IShowResult } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -448,10 +448,6 @@ export class SimpleConfigurationService implements IConfigurationService { this._configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel()); } - private configuration(): Configuration { - return this._configuration; - } - getValue(): T; getValue(section: string): T; getValue(overrides: IConfigurationOverrides): T; @@ -459,20 +455,43 @@ export class SimpleConfigurationService implements IConfigurationService { getValue(arg1?: any, arg2?: any): any { const section = typeof arg1 === 'string' ? arg1 : undefined; const overrides = isConfigurationOverrides(arg1) ? arg1 : isConfigurationOverrides(arg2) ? arg2 : {}; - return this.configuration().getValue(section, overrides, undefined); + return this._configuration.getValue(section, overrides, undefined); } - public updateValue(key: string, value: any, arg3?: any, arg4?: any): Promise { - this.configuration().updateValue(key, value); + public updateValues(values: [string, any][]): Promise { + const previous = { data: this._configuration.toData() }; + + let changedKeys: string[] = []; + + for (const entry of values) { + const [key, value] = entry; + if (this.getValue(key) === value) { + continue; + } + this._configuration.updateValue(key, value); + changedKeys.push(key); + } + + if (changedKeys.length > 0) { + const configurationChangeEvent = new ConfigurationChangeEvent({ keys: changedKeys, overrides: [] }, previous, this._configuration); + configurationChangeEvent.source = ConfigurationTarget.MEMORY; + configurationChangeEvent.sourceConfig = null; + this._onDidChangeConfiguration.fire(configurationChangeEvent); + } + return Promise.resolve(); } + public updateValue(key: string, value: any, arg3?: any, arg4?: any): Promise { + return this.updateValues([[key, value]]); + } + public inspect(key: string, options: IConfigurationOverrides = {}): IConfigurationValue { - return this.configuration().inspect(key, options, undefined); + return this._configuration.inspect(key, options, undefined); } public keys() { - return this.configuration().keys(undefined); + return this._configuration.keys(undefined); } public reloadConfiguration(): Promise { @@ -622,14 +641,18 @@ export function applyConfigurationValues(configurationService: IConfigurationSer if (!(configurationService instanceof SimpleConfigurationService)) { return; } + let toUpdate: [string, any][] = []; Object.keys(source).forEach((key) => { if (isEditorConfigurationKey(key)) { - configurationService.updateValue(`editor.${key}`, source[key]); + toUpdate.push([`editor.${key}`, source[key]]); } if (isDiffEditor && isDiffEditorConfigurationKey(key)) { - configurationService.updateValue(`diffEditor.${key}`, source[key]); + toUpdate.push([`diffEditor.${key}`, source[key]]); } }); + if (toUpdate.length > 0) { + configurationService.updateValues(toUpdate); + } } export class SimpleBulkEditService implements IBulkEditService { diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index dca8eec3b7..f4d4d87b94 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -23,7 +23,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; -import { IInstantiationService, optional, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -114,6 +114,11 @@ export interface IGlobalEditorOptions { * Defaults to true. */ wordBasedSuggestions?: boolean; + /** + * Controls whether the semanticHighlighting is shown for the languages that support it. + * Defaults to true. + */ + 'semanticHighlighting.enabled'?: boolean; /** * Keep peek editors open even when double clicking their content or when hitting `Escape`. * Defaults to false. @@ -443,7 +448,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon @IConfigurationService configurationService: IConfigurationService, @IContextMenuService contextMenuService: IContextMenuService, @IEditorProgressService editorProgressService: IEditorProgressService, - @optional(IClipboardService) clipboardService: IClipboardService | null, + @IClipboardService clipboardService: IClipboardService, ) { applyConfigurationValues(configurationService, options, true); const themeDomRegistration = (themeService).registerEditorContainer(domElement); diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 5bf328fdc1..9730b7d023 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -39,6 +39,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { clearAllFontInfos } from 'vs/editor/browser/config/configuration'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; type Omit = Pick>; @@ -122,7 +123,7 @@ export function createDiffEditor(domElement: HTMLElement, options?: IDiffEditorC services.get(IConfigurationService), services.get(IContextMenuService), services.get(IEditorProgressService), - null + services.get(IClipboardService) ); }); } diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 8dd5d71888..0f3a925854 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -48,6 +48,8 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; import { AccessibilityService } from 'vs/platform/accessibility/common/accessibilityService'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { BrowserClipboardService } from 'vs/platform/clipboard/browser/clipboardService'; export interface IEditorOverrideServices { [index: string]: any; @@ -204,6 +206,8 @@ export class DynamicStandaloneServices extends Disposable { let contextViewService = ensure(IContextViewService, () => this._register(new ContextViewService(layoutService))); + ensure(IClipboardService, () => new BrowserClipboardService()); + ensure(IContextMenuService, () => { const contextMenuService = new ContextMenuService(telemetryService, notificationService, contextViewService, keybindingService, themeService); contextMenuService.configure({ blockMouse: false }); // we do not want that in the standalone editor diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index b5649dfac9..8ed9468ec1 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -2880,6 +2880,33 @@ suite('Editor Controller - Cursor Configuration', () => { model.dispose(); }); + + test('issue #90973: Undo brings back model alternative version', () => { + let model = createTextModel( + [ + '' + ].join('\n'), + { + insertSpaces: false, + } + ); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + const beforeVersion = model.getVersionId(); + const beforeAltVersion = model.getAlternativeVersionId(); + cursorCommand(cursor, H.Type, { text: 'Hello' }, 'keyboard'); + cursorCommand(cursor, H.Undo, {}); + const afterVersion = model.getVersionId(); + const afterAltVersion = model.getAlternativeVersionId(); + + assert.notEqual(beforeVersion, afterVersion); + assert.equal(beforeAltVersion, afterAltVersion); + }); + + model.dispose(); + }); + + }); suite('Editor Controller - Indentation Rules', () => { diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index 08b1fd60ce..85bf50c4e9 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; +import { IRange } from 'vs/editor/common/core/range'; +import { Selection, ISelection } from 'vs/editor/common/core/selection'; import { ICommand, Handler, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; @@ -50,7 +50,7 @@ export function testCommand( export function getEditOperation(model: ITextModel, command: ICommand): IIdentifiedSingleEditOperation[] { let operations: IIdentifiedSingleEditOperation[] = []; let editOperationBuilder: IEditOperationBuilder = { - addEditOperation: (range: Range, text: string, forceMoveMarkers: boolean = false) => { + addEditOperation: (range: IRange, text: string, forceMoveMarkers: boolean = false) => { operations.push({ range: range, text: text, @@ -58,7 +58,7 @@ export function getEditOperation(model: ITextModel, command: ICommand): IIdentif }); }, - addTrackedEditOperation: (range: Range, text: string, forceMoveMarkers: boolean = false) => { + addTrackedEditOperation: (range: IRange, text: string, forceMoveMarkers: boolean = false) => { operations.push({ range: range, text: text, @@ -67,7 +67,7 @@ export function getEditOperation(model: ITextModel, command: ICommand): IIdentif }, - trackSelection: (selection: Selection) => { + trackSelection: (selection: ISelection) => { return ''; } }; diff --git a/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts b/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts index be8030eafc..056b6a7b97 100644 --- a/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts +++ b/src/vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils.ts @@ -5,7 +5,7 @@ import { CharCode } from 'vs/base/common/charCode'; import { Range } from 'vs/editor/common/core/range'; -import { DefaultEndOfLine, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferBuilder } from 'vs/editor/common/model'; +import { DefaultEndOfLine, ITextBuffer, ITextBufferBuilder, ValidAnnotatedEditOperation } from 'vs/editor/common/model'; export function getRandomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; @@ -31,7 +31,7 @@ export function getRandomString(minLength: number, maxLength: number): string { return r; } -export function generateRandomEdits(chunks: string[], editCnt: number): IIdentifiedSingleEditOperation[] { +export function generateRandomEdits(chunks: string[], editCnt: number): ValidAnnotatedEditOperation[] { let lines: string[] = []; for (const chunk of chunks) { let newLines = chunk.split(/\r\n|\r|\n/); @@ -43,7 +43,7 @@ export function generateRandomEdits(chunks: string[], editCnt: number): IIdentif } } - let ops: IIdentifiedSingleEditOperation[] = []; + let ops: ValidAnnotatedEditOperation[] = []; for (let i = 0; i < editCnt; i++) { let line = getRandomInt(1, lines.length); @@ -54,17 +54,14 @@ export function generateRandomEdits(chunks: string[], editCnt: number): IIdentif text = getRandomString(5, 10); } - ops.push({ - text: text, - range: new Range(line, startColumn, line, endColumn) - }); + ops.push(new ValidAnnotatedEditOperation(null, new Range(line, startColumn, line, endColumn), text, false, false, false)); lines[line - 1] = lines[line - 1].substring(0, startColumn - 1) + text + lines[line - 1].substring(endColumn - 1); } return ops; } -export function generateSequentialInserts(chunks: string[], editCnt: number): IIdentifiedSingleEditOperation[] { +export function generateSequentialInserts(chunks: string[], editCnt: number): ValidAnnotatedEditOperation[] { let lines: string[] = []; for (const chunk of chunks) { let newLines = chunk.split(/\r\n|\r|\n/); @@ -76,7 +73,7 @@ export function generateSequentialInserts(chunks: string[], editCnt: number): II } } - let ops: IIdentifiedSingleEditOperation[] = []; + let ops: ValidAnnotatedEditOperation[] = []; for (let i = 0; i < editCnt; i++) { let line = lines.length; @@ -90,16 +87,13 @@ export function generateSequentialInserts(chunks: string[], editCnt: number): II lines[line - 1] += text; } - ops.push({ - text: text, - range: new Range(line, column, line, column) - }); + ops.push(new ValidAnnotatedEditOperation(null, new Range(line, column, line, column), text, false, false, false)); } return ops; } -export function generateRandomReplaces(chunks: string[], editCnt: number, searchStringLen: number, replaceStringLen: number): IIdentifiedSingleEditOperation[] { +export function generateRandomReplaces(chunks: string[], editCnt: number, searchStringLen: number, replaceStringLen: number): ValidAnnotatedEditOperation[] { let lines: string[] = []; for (const chunk of chunks) { let newLines = chunk.split(/\r\n|\r|\n/); @@ -111,7 +105,7 @@ export function generateRandomReplaces(chunks: string[], editCnt: number, search } } - let ops: IIdentifiedSingleEditOperation[] = []; + let ops: ValidAnnotatedEditOperation[] = []; let chunkSize = Math.max(1, Math.floor(lines.length / editCnt)); let chunkCnt = Math.floor(lines.length / chunkSize); let replaceString = getRandomString(replaceStringLen, replaceStringLen); @@ -125,10 +119,7 @@ export function generateRandomReplaces(chunks: string[], editCnt: number, search let startColumn = getRandomInt(1, maxColumn); let endColumn = Math.min(maxColumn, startColumn + searchStringLen); - ops.push({ - text: replaceString, - range: new Range(line, startColumn, line, endColumn) - }); + ops.push(new ValidAnnotatedEditOperation(null, new Range(line, startColumn, line, endColumn), replaceString, false, false, false)); previousChunksLength = endLine; } @@ -166,4 +157,4 @@ export function generateRandomChunkWithLF(minLength: number, maxLength: number): } } return r; -} \ No newline at end of file +} diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e982ce6248..430b27fd45 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -638,10 +638,18 @@ declare namespace monaco { * Return the end position (which will be after or equal to the start position) */ getEndPosition(): Position; + /** + * Return the end position (which will be after or equal to the start position) + */ + static getEndPosition(range: IRange): Position; /** * Return the start position (which will be before or equal to the end position) */ getStartPosition(): Position; + /** + * Return the start position (which will be before or equal to the end position) + */ + static getStartPosition(range: IRange): Position; /** * Transform to a user presentable string representation. */ @@ -1098,6 +1106,11 @@ declare namespace monaco.editor { * Defaults to true. */ wordBasedSuggestions?: boolean; + /** + * Controls whether the semanticHighlighting is shown for the languages that support it. + * Defaults to true. + */ + 'semanticHighlighting.enabled'?: boolean; /** * Keep peek editors open even when double clicking their content or when hitting `Escape`. * Defaults to false. @@ -1508,7 +1521,7 @@ declare namespace monaco.editor { /** * The range to replace. This can be empty to emulate a simple insert. */ - range: Range; + range: IRange; /** * The text to replace with. This can be null to emulate a simple delete. */ @@ -1520,6 +1533,22 @@ declare namespace monaco.editor { forceMoveMarkers?: boolean; } + export interface IValidEditOperation { + /** + * The range to replace. This can be empty to emulate a simple insert. + */ + range: Range; + /** + * The text to replace with. This can be null to emulate a simple delete. + */ + text: string | null; + /** + * This indicates that this operation has "insert" semantics. + * i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved. + */ + forceMoveMarkers: boolean; + } + /** * A callback that can compute the cursor state after applying a series of edit operations. */ @@ -1527,7 +1556,7 @@ declare namespace monaco.editor { /** * A callback that can compute the resulting cursors state after some edit operations have been executed. */ - (inverseEditOperations: IIdentifiedSingleEditOperation[]): Selection[] | null; + (inverseEditOperations: IValidEditOperation[]): Selection[] | null; } export class TextModelResolvedOptions { @@ -1867,7 +1896,7 @@ declare namespace monaco.editor { * @param operations The edit operations. * @return The inverse edit operations, that, when applied, will bring the model back to the previous state. */ - applyEdits(operations: IIdentifiedSingleEditOperation[]): IIdentifiedSingleEditOperation[]; + applyEdits(operations: IIdentifiedSingleEditOperation[]): IValidEditOperation[]; /** * Change the end of line sequence without recording in the undo stack. * This can have dire consequences on the undo stack! See @pushEOL for the preferred way. @@ -1919,14 +1948,14 @@ declare namespace monaco.editor { * @param range The range to replace (delete). May be empty to represent a simple insert. * @param text The text to replace with. May be null to represent a simple delete. */ - addEditOperation(range: Range, text: string | null, forceMoveMarkers?: boolean): void; + addEditOperation(range: IRange, text: string | null, forceMoveMarkers?: boolean): void; /** * Add a new edit operation (a replace operation). * The inverse edits will be accessible in `ICursorStateComputerData.getInverseEditOperations()` * @param range The range to replace (delete). May be empty to represent a simple insert. * @param text The text to replace with. May be null to represent a simple delete. */ - addTrackedEditOperation(range: Range, text: string | null, forceMoveMarkers?: boolean): void; + addTrackedEditOperation(range: IRange, text: string | null, forceMoveMarkers?: boolean): void; /** * Track `selection` when applying edit operations. * A best effort will be made to not grow/expand the selection. @@ -1946,7 +1975,7 @@ declare namespace monaco.editor { /** * Get the inverse edit operations of the added edit operations. */ - getInverseEditOperations(): IIdentifiedSingleEditOperation[]; + getInverseEditOperations(): IValidEditOperation[]; /** * Get a previously tracked selection. * @param id The unique identifier returned by `trackSelection`. @@ -6043,7 +6072,7 @@ declare namespace monaco.languages { description?: string; iconPath?: { id: string; - } | { + } | Uri | { light: Uri; dark: Uri; }; diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts new file mode 100644 index 0000000000..b5febb04ec --- /dev/null +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { URI } from 'vs/base/common/uri'; + +export class BrowserClipboardService implements IClipboardService { + + _serviceBrand: undefined; + + private _internalResourcesClipboard: URI[] | undefined; + + async writeText(text: string, type?: string): Promise { + if (type) { + return; // TODO@sbatten + } + + if (navigator.clipboard && navigator.clipboard.writeText) { + return navigator.clipboard.writeText(text); + } else { + const activeElement = document.activeElement; + const newTextarea = document.createElement('textarea'); + newTextarea.className = 'clipboard-copy'; + newTextarea.style.visibility = 'false'; + newTextarea.style.height = '1px'; + newTextarea.style.width = '1px'; + newTextarea.setAttribute('aria-hidden', 'true'); + newTextarea.style.position = 'absolute'; + newTextarea.style.top = '-1000'; + newTextarea.style.left = '-1000'; + document.body.appendChild(newTextarea); + newTextarea.value = text; + newTextarea.focus(); + newTextarea.select(); + document.execCommand('copy'); + activeElement.focus(); + document.body.removeChild(newTextarea); + } + return; + } + + async readText(type?: string): Promise { + if (type) { + return ''; // TODO@sbatten + } + + return navigator.clipboard.readText(); + } + + readTextSync(): string | undefined { + return undefined; + } + + readFindText(): string { + // @ts-ignore + return undefined; + } + + writeFindText(text: string): void { } + + writeResources(resources: URI[]): void { + this._internalResourcesClipboard = resources; + } + + readResources(): URI[] { + return this._internalResourcesClipboard || []; + } + + hasResources(): boolean { + return this._internalResourcesClipboard !== undefined && this._internalResourcesClipboard.length > 0; + } +} diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index ec3f5cc5b8..f9c125ee5c 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -49,6 +49,7 @@ export interface IProductConfiguration { readonly extensionTips?: { [id: string]: string; }; readonly extensionImportantTips?: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; }; readonly exeBasedExtensionTips?: { [id: string]: IExeBasedExtensionTip; }; + readonly remoteExtensionTips?: { [remoteName: string]: IRemoteExtensionTip; }; readonly extensionKeywords?: { [extension: string]: readonly string[]; }; readonly keymapExtensionTips?: readonly string[]; @@ -118,6 +119,11 @@ export interface IExeBasedExtensionTip { exeFriendlyName?: string; } +export interface IRemoteExtensionTip { + friendlyName: string; + extensionId: string; +} + export interface ISurveyData { surveyId: string; surveyUrl: string; diff --git a/src/vs/platform/remote/common/remoteHosts.ts b/src/vs/platform/remote/common/remoteHosts.ts index d6e2fe7e8b..4d6b3e86cf 100644 --- a/src/vs/platform/remote/common/remoteHosts.ts +++ b/src/vs/platform/remote/common/remoteHosts.ts @@ -12,6 +12,9 @@ export function getRemoteAuthority(uri: URI): string | undefined { return uri.scheme === REMOTE_HOST_SCHEME ? uri.authority : undefined; } +export function getRemoteName(authority: string): string; +export function getRemoteName(authority: undefined): undefined; +export function getRemoteName(authority: string | undefined): string | undefined; export function getRemoteName(authority: string | undefined): string | undefined { if (!authority) { return undefined; @@ -22,4 +25,4 @@ export function getRemoteName(authority: string | undefined): string | undefined return authority; } return authority.substr(0, pos); -} \ No newline at end of file +} diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index d0b8560999..19f029e775 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -16,11 +16,30 @@ import { Emitter, Event } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ParseError, parse } from 'vs/base/common/json'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { localize } from 'vs/nls'; -type SyncConflictsClassification = { +type SyncSourceClassification = { source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; +export interface IRemoteUserData { + ref: string; + syncData: ISyncData | null; +} + +export interface ISyncData { + version: number; + content: string; +} + +function isSyncData(thing: any): thing is ISyncData { + return thing + && (thing.version && typeof thing.version === 'number') + && (thing.content && typeof thing.content === 'string') + && Object.keys(thing).length === 2; +} + export abstract class AbstractSynchroniser extends Disposable { protected readonly syncFolder: URI; @@ -58,11 +77,11 @@ export abstract class AbstractSynchroniser extends Disposable { this._onDidChangStatus.fire(status); if (status === SyncStatus.HasConflicts) { // Log to telemetry when there is a sync conflict - this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsDetected', { source: this.source }); + this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsDetected', { source: this.source }); } if (oldStatus === SyncStatus.HasConflicts && status === SyncStatus.Idle) { // Log to telemetry when conflicts are resolved - this.telemetryService.publicLog2<{ source: string }, SyncConflictsClassification>('sync/conflictsResolved', { source: this.source }); + this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/conflictsResolved', { source: this.source }); } } } @@ -88,6 +107,13 @@ export abstract class AbstractSynchroniser extends Disposable { const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = ref && lastSyncUserData && lastSyncUserData.ref === ref ? lastSyncUserData : await this.getRemoteUserData(lastSyncUserData); + + if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { + // current version is not compatible with cloud version + this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.source }); + throw new UserDataSyncError(localize('incompatible', "Cannot sync {0} as its version {1} is not compatible with cloud {2}", this.source, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.source); + } + return this.doSync(remoteUserData, lastSyncUserData); } @@ -98,8 +124,8 @@ export abstract class AbstractSynchroniser extends Disposable { async getRemoteContent(): Promise { const lastSyncData = await this.getLastSyncUserData(); - const remoteUserData = await this.getRemoteUserData(lastSyncData); - return remoteUserData.content; + const { syncData } = await this.getRemoteUserData(lastSyncData); + return syncData ? syncData.content : null; } async resetLocal(): Promise { @@ -108,25 +134,56 @@ export abstract class AbstractSynchroniser extends Disposable { } catch (e) { /* ignore */ } } - protected async getLastSyncUserData(): Promise { + protected async getLastSyncUserData(): Promise { try { const content = await this.fileService.readFile(this.lastSyncResource); - return JSON.parse(content.value.toString()); + const parsed = JSON.parse(content.value.toString()); + let syncData: ISyncData = JSON.parse(parsed.content); + + // Migration from old content to sync data + if (!isSyncData(syncData)) { + syncData = { version: this.version, content: parsed.content }; + } + + return { ...parsed, ...{ syncData, content: undefined } }; } catch (error) { - return null; + if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { + // log error always except when file does not exist + this.logService.error(error); + } } + return null; } - protected async updateLastSyncUserData(lastSyncUserData: T): Promise { + protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary = {}): Promise { + const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: JSON.stringify(lastSyncRemoteUserData.syncData), ...additionalProps }; await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); } - protected async getRemoteUserData(lastSyncData: IUserData | null): Promise { - return this.userDataSyncStoreService.read(this.resourceKey, lastSyncData, this.source); + protected async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise { + const lastSyncUserData: IUserData | null = lastSyncData ? { ref: lastSyncData.ref, content: lastSyncData.syncData ? JSON.stringify(lastSyncData.syncData) : null } : null; + const { ref, content } = await this.userDataSyncStoreService.read(this.resourceKey, lastSyncUserData, this.source); + let syncData: ISyncData | null = null; + if (content !== null) { + try { + syncData = JSON.parse(content); + + // Migration from old content to sync data + if (!isSyncData(syncData)) { + syncData = { version: this.version, content }; + } + + } catch (e) { + this.logService.error(e); + } + } + return { ref, syncData }; } - protected async updateRemoteUserData(content: string, ref: string | null): Promise { - return this.userDataSyncStoreService.write(this.resourceKey, content, ref, this.source); + protected async updateRemoteUserData(content: string, ref: string | null): Promise { + const syncData: ISyncData = { version: this.version, content }; + ref = await this.userDataSyncStoreService.write(this.resourceKey, JSON.stringify(syncData), ref, this.source); + return { ref, syncData }; } protected async backupLocal(content: VSBuffer): Promise { @@ -145,13 +202,14 @@ export abstract class AbstractSynchroniser extends Disposable { } abstract readonly resourceKey: ResourceKey; - protected abstract doSync(remoteUserData: IUserData, lastSyncUserData: IUserData | null): Promise; + protected abstract readonly version: number; + protected abstract doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; } export interface IFileSyncPreviewResult { readonly fileContent: IFileContent | null; - readonly remoteUserData: IUserData; - readonly lastSyncUserData: IUserData | null; + readonly remoteUserData: IRemoteUserData; + readonly lastSyncUserData: IRemoteUserData | null; readonly content: string | null; readonly hasLocalChanged: boolean; readonly hasRemoteChanged: boolean; @@ -190,7 +248,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { if (preview) { if (this.syncPreviewResultPromise) { const result = await this.syncPreviewResultPromise; - return result.remoteUserData ? result.remoteUserData.content : null; + return result.remoteUserData && result.remoteUserData.syncData ? result.remoteUserData.syncData.content : null; } } return super.getRemoteContent(); diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 4878e727d8..64111c5c20 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -14,7 +14,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { localize } from 'vs/nls'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; interface ISyncPreviewResult { @@ -22,18 +22,19 @@ interface ISyncPreviewResult { readonly removed: IExtensionIdentifier[]; readonly updated: ISyncExtension[]; readonly remote: ISyncExtension[] | null; - readonly remoteUserData: IUserData; + readonly remoteUserData: IRemoteUserData; readonly skippedExtensions: ISyncExtension[]; readonly lastSyncUserData: ILastSyncUserData | null; } -interface ILastSyncUserData extends IUserData { +interface ILastSyncUserData extends IRemoteUserData { skippedExtensions: ISyncExtension[] | undefined; } export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { readonly resourceKey: ResourceKey = 'extensions'; + protected readonly version: number = 1; constructor( @IEnvironmentService environmentService: IEnvironmentService, @@ -72,9 +73,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); - if (remoteUserData.content !== null) { + if (remoteUserData.syncData !== null) { const localExtensions = await this.getLocalExtensions(); - const remoteExtensions: ISyncExtension[] = JSON.parse(remoteUserData.content); + const remoteExtensions: ISyncExtension[] = JSON.parse(remoteUserData.syncData.content); const { added, updated, remote } = merge(localExtensions, remoteExtensions, [], [], this.getIgnoredExtensions()); await this.apply({ added, removed: [], updated, remote, remoteUserData, skippedExtensions: [], lastSyncUserData }); } @@ -145,7 +146,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return null; } - protected async doSync(remoteUserData: IUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { try { const previewResult = await this.getPreview(remoteUserData, lastSyncUserData); await this.apply(previewResult); @@ -163,9 +164,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse this.setStatus(SyncStatus.Idle); } - private async getPreview(remoteUserData: IUserData, lastSyncUserData: ILastSyncUserData | null): Promise { - const remoteExtensions: ISyncExtension[] = remoteUserData.content ? JSON.parse(remoteUserData.content) : null; - const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? JSON.parse(lastSyncUserData.content!) : null; + private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + const remoteExtensions: ISyncExtension[] = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; + const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? JSON.parse(lastSyncUserData.syncData!.content) : null; const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : []; const localExtensions = await this.getLocalExtensions(); @@ -201,15 +202,14 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse // update remote this.logService.trace('Extensions: Updating remote extensions...'); const content = JSON.stringify(remote); - const ref = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); - remoteUserData = { ref, content }; + remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); this.logService.info('Extensions: Updated remote extensions'); } if (lastSyncUserData?.ref !== remoteUserData.ref) { // update last sync this.logService.trace('Extensions: Updating last synchronized extensions...'); - await this.updateLastSyncUserData({ ...remoteUserData, skippedExtensions }); + await this.updateLastSyncUserData(remoteUserData, { skippedExtensions }); this.logService.info('Extensions: Updated last synchronized extensions'); } } diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 1b299bd0af..9da7039d1b 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserData, UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -13,7 +13,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { edit } from 'vs/platform/userDataSync/common/content'; import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; import { parse } from 'vs/base/common/json'; -import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const argvProperties: string[] = ['locale']; @@ -21,13 +21,14 @@ const argvProperties: string[] = ['locale']; interface ISyncPreviewResult { readonly local: IGlobalState | undefined; readonly remote: IGlobalState | undefined; - readonly remoteUserData: IUserData; - readonly lastSyncUserData: IUserData | null; + readonly remoteUserData: IRemoteUserData; + readonly lastSyncUserData: IRemoteUserData | null; } export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { readonly resourceKey: ResourceKey = 'globalState'; + protected readonly version: number = 1; constructor( @IFileService fileService: IFileService, @@ -57,8 +58,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); - if (remoteUserData.content !== null) { - const local: IGlobalState = JSON.parse(remoteUserData.content); + if (remoteUserData.syncData !== null) { + const local: IGlobalState = JSON.parse(remoteUserData.syncData.content); await this.apply({ local, remote: undefined, remoteUserData, lastSyncUserData }); } @@ -119,7 +120,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return null; } - protected async doSync(remoteUserData: IUserData, lastSyncUserData: IUserData | null): Promise { + protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { try { const result = await this.getPreview(remoteUserData, lastSyncUserData); await this.apply(result); @@ -137,9 +138,9 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } } - private async getPreview(remoteUserData: IUserData, lastSyncUserData: IUserData | null, ): Promise { - const remoteGlobalState: IGlobalState = remoteUserData.content ? JSON.parse(remoteUserData.content) : null; - const lastSyncGlobalState = lastSyncUserData && lastSyncUserData.content ? JSON.parse(lastSyncUserData.content) : null; + private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, ): Promise { + const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; + const lastSyncGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; const localGloablState = await this.getLocalGlobalState(); @@ -173,9 +174,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs // update remote this.logService.trace('UI State: Updating remote ui state...'); const content = JSON.stringify(remote); - const ref = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); + remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); this.logService.info('UI State: Updated remote ui state'); - remoteUserData = { ref, content }; } if (lastSyncUserData?.ref !== remoteUserData.ref) { diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index fec6c1ea4b..e0d400083b 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource, IUserDataSynchroniser, IUserData, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource, IUserDataSynchroniser, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse } from 'vs/base/common/json'; @@ -16,7 +16,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { isUndefined } from 'vs/base/common/types'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; @@ -31,6 +31,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem readonly resourceKey: ResourceKey = 'keybindings'; protected get conflictsPreviewResource(): URI { return this.environmentService.keybindingsSyncPreviewResource; } + protected readonly version: number = 1; constructor( @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @@ -59,7 +60,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); - const content = remoteUserData.content !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null; + const content = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; if (content !== null) { const fileContent = await this.getLocalFileContent(); @@ -160,7 +161,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null; } - protected async doSync(remoteUserData: IUserData, lastSyncUserData: IUserData | null): Promise { + protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { try { const result = await this.getPreview(remoteUserData, lastSyncUserData); if (result.hasConflicts) { @@ -213,9 +214,8 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem if (hasRemoteChanged) { this.logService.trace('Keybindings: Updating remote keybindings...'); - const remoteContents = this.updateSyncContent(content, remoteUserData.content); - const ref = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref); - remoteUserData = { ref, content: remoteContents }; + const remoteContents = this.updateSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null); + remoteUserData = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref); this.logService.info('Keybindings: Updated remote keybindings'); } @@ -230,23 +230,23 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== null || fileContent !== null)) { this.logService.trace('Keybindings: Updating last synchronized keybindings...'); const lastSyncContent = this.updateSyncContent(content !== null ? content : fileContent!.value.toString(), null); - await this.updateLastSyncUserData({ ref: remoteUserData.ref, content: lastSyncContent }); + await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: { version: remoteUserData.syncData!.version, content: lastSyncContent } }); this.logService.info('Keybindings: Updated last synchronized keybindings'); } this.syncPreviewResultPromise = null; } - private getPreview(remoteUserData: IUserData, lastSyncUserData: IUserData | null): Promise { + private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { if (!this.syncPreviewResultPromise) { this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token)); } return this.syncPreviewResultPromise; } - private async generatePreview(remoteUserData: IUserData, lastSyncUserData: IUserData | null, token: CancellationToken): Promise { - const remoteContent = remoteUserData.content ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null; - const lastSyncContent = lastSyncUserData && lastSyncUserData.content ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.content) : null; + private async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; + const lastSyncContent = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null; // Get file content last to get the latest const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index 5c9135a386..25eecaa2c0 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -576,15 +576,17 @@ function parseSettings(content: string): INode[] { if (hierarchyLevel === 0) { if (sep === ',') { const node = nodes.pop(); - nodes.push({ - startOffset: node!.startOffset, - endOffset: node!.endOffset, - value: node!.value, - setting: { - key: node!.setting!.key, - hasCommaSeparator: true - } - }); + if (node) { + nodes.push({ + startOffset: node.startOffset, + endOffset: node.endOffset, + value: node.value, + setting: { + key: node.setting!.key, + hasCommaSeparator: true + } + }); + } } } }, diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 9d77847658..bb5edb733a 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource, IUserData, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse } from 'vs/base/common/json'; import { localize } from 'vs/nls'; @@ -18,15 +18,26 @@ import * as arrays from 'vs/base/common/arrays'; import * as objects from 'vs/base/common/objects'; import { isEmptyObject } from 'vs/base/common/types'; import { edit } from 'vs/platform/userDataSync/common/content'; -import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; +interface ISettingsSyncContent { + settings: string; +} + +function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent { + return thing + && (thing.settings && typeof thing.settings === 'string') + && Object.keys(thing).length === 1; +} + export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements ISettingsSyncService { _serviceBrand: any; readonly resourceKey: ResourceKey = 'settings'; + protected readonly version: number = 1; protected get conflictsPreviewResource(): URI { return this.environmentService.settingsSyncPreviewResource; } private _conflicts: IConflictSetting[] = []; @@ -77,12 +88,13 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); + const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - if (remoteUserData.content !== null) { + if (remoteSettingsSyncContent !== null) { const fileContent = await this.getLocalFileContent(); const formatUtils = await this.getFormattingOptions(); // Update ignored settings from local file content - const content = updateIgnoredSettings(remoteUserData.content, fileContent ? fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils); + const content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', getIgnoredSettings(this.configurationService), formatUtils); this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ fileContent, remoteUserData, @@ -173,6 +185,10 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement async getRemoteContent(preview?: boolean): Promise { let content = await super.getRemoteContent(preview); + if (content !== null) { + const settingsSyncContent = this.parseSettingsSyncContent(content); + content = settingsSyncContent ? settingsSyncContent.settings : null; + } if (preview && content !== null) { const formatUtils = await this.getFormattingOptions(); // remove ignored settings from the remote content for preview @@ -202,7 +218,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement } } - protected async doSync(remoteUserData: IUserData, lastSyncUserData: IUserData | null, resolvedConflicts: { key: string, value: any | undefined }[] = []): Promise { + protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any | undefined }[] = []): Promise { try { const result = await this.getPreview(remoteUserData, lastSyncUserData, resolvedConflicts); if (result.hasConflicts) { @@ -256,11 +272,11 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement if (hasRemoteChanged) { const formatUtils = await this.getFormattingOptions(); // Update ignored settings from remote - content = updateIgnoredSettings(content, remoteUserData.content || '{}', getIgnoredSettings(this.configurationService, content), formatUtils); + const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); + content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', getIgnoredSettings(this.configurationService, content), formatUtils); this.logService.trace('Settings: Updating remote settings...'); - const ref = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); + remoteUserData = await this.updateRemoteUserData(JSON.stringify({ settings: content }), forcePush ? null : remoteUserData.ref); this.logService.info('Settings: Updated remote settings'); - remoteUserData = { ref, content }; } // Delete the preview @@ -280,16 +296,18 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement this.syncPreviewResultPromise = null; } - private getPreview(remoteUserData: IUserData, lastSyncUserData: IUserData | null, resolvedConflicts: { key: string, value: any }[]): Promise { + private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[]): Promise { if (!this.syncPreviewResultPromise) { this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, resolvedConflicts, token)); } return this.syncPreviewResultPromise; } - protected async generatePreview(remoteUserData: IUserData, lastSyncUserData: IUserData | null, resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise { + protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise { const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); + const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); + const lastSettingsSyncContent = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null; let content: string | null = null; let hasLocalChanged: boolean = false; @@ -297,7 +315,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement let hasConflicts: boolean = false; let conflictSettings: IConflictSetting[] = []; - if (remoteUserData.content) { + if (remoteSettingsSyncContent) { const localContent: string = fileContent ? fileContent.value.toString() : '{}'; // No action when there are errors @@ -307,7 +325,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement else { this.logService.trace('Settings: Merging remote settings with local settings...'); - const result = merge(localContent, remoteUserData.content, lastSyncUserData ? lastSyncUserData.content : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formattingOptions); + const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, getIgnoredSettings(this.configurationService), resolvedConflicts, formattingOptions); content = result.localContent || result.remoteContent; hasLocalChanged = result.localContent !== null; hasRemoteChanged = result.remoteContent !== null; @@ -333,4 +351,17 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts }; } + private getSettingsSyncContent(remoteUserData: IRemoteUserData): ISettingsSyncContent | null { + return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null; + } + + private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { + try { + const parsed = JSON.parse(syncContent); + return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent }; + } catch (e) { + this.logService.error(e); + } + return null; + } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 26f86b2118..7bf0b3c1e0 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -5,7 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; -import { IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, allSettings } from 'vs/platform/configuration/common/configurationRegistry'; @@ -83,7 +83,12 @@ export function registerConfiguration(): IDisposable { }, 'sync.ignoredExtensions': { 'type': 'array', - description: localize('sync.ignoredExtensions', "Configure extensions to be ignored while synchronizing."), + 'description': localize('sync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."), + items: { + type: 'string', + pattern: EXTENSION_IDENTIFIER_PATTERN, + errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") + }, 'default': [], 'scope': ConfigurationScope.APPLICATION, uniqueItems: true @@ -171,6 +176,7 @@ export enum UserDataSyncErrorCode { // Local Errors LocalPreconditionFailed = 'LocalPreconditionFailed', LocalInvalidContent = 'LocalInvalidContent', + Incompatible = 'Incompatible', Unknown = 'Unknown', } @@ -335,6 +341,7 @@ export interface ISettingsSyncService extends IUserDataSynchroniser { //#endregion export const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); +export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey('syncEnabled', false); export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync'; export function toRemoteContentResource(source: SyncSource): URI { diff --git a/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts b/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts index d934965836..6bfa779916 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts @@ -7,6 +7,11 @@ import { IUserDataSyncEnablementService, ResourceKey, ALL_RESOURCE_KEYS } from ' import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +type SyncEnablementClassification = { + enabled?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; +}; const enablementKey = 'sync.enable'; function getEnablementKey(resourceKey: ResourceKey) { return `${enablementKey}.${resourceKey}`; } @@ -22,7 +27,8 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa readonly onDidChangeResourceEnablement: Event<[ResourceKey, boolean]> = this._onDidChangeResourceEnablement.event; constructor( - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); @@ -34,6 +40,7 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa setEnablement(enabled: boolean): void { if (this.isEnabled() !== enabled) { + this.telemetryService.publicLog2<{ enabled: boolean }, SyncEnablementClassification>(enablementKey, { enabled }); this.storageService.store(enablementKey, enabled, StorageScope.GLOBAL); } } @@ -44,7 +51,9 @@ export class UserDataSyncEnablementService extends Disposable implements IUserDa setResourceEnablement(resourceKey: ResourceKey, enabled: boolean): void { if (this.isResourceEnabled(resourceKey) !== enabled) { - this.storageService.store(getEnablementKey(resourceKey), enabled, StorageScope.GLOBAL); + const resourceEnablementKey = getEnablementKey(resourceKey); + this.telemetryService.publicLog2<{ enabled: boolean }, SyncEnablementClassification>(resourceEnablementKey, { enabled }); + this.storageService.store(resourceEnablementKey, enabled, StorageScope.GLOBAL); } } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 7616d87ab5..72e2e0b37b 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataAuthTokenService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -49,7 +49,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ @IInstantiationService private readonly instantiationService: IInstantiationService, @ISettingsSyncService private readonly settingsSynchroniser: ISettingsSyncService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, - @IUserDataAuthTokenService private readonly userDataAuthTokenService: IUserDataAuthTokenService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IStorageService private readonly storageService: IStorageService, ) { @@ -62,7 +61,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (this.userDataSyncStoreService.userDataSyncStore) { this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus())); - this._register(this.userDataAuthTokenService.onDidChangeToken(e => this.onDidChangeAuthTokenStatus(e))); } this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal)); @@ -140,6 +138,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } async stop(): Promise { + await this.checkEnablement(); if (this.status === SyncStatus.Idle) { return; } @@ -201,7 +200,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } private async hasPreviouslySynced(): Promise { - await this.checkEnablement(); for (const synchroniser of this.synchronisers) { if (await synchroniser.hasPreviouslySynced()) { return true; @@ -211,7 +209,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } private async hasLocalData(): Promise { - await this.checkEnablement(); for (const synchroniser of this.synchronisers) { if (await synchroniser.hasLocalData()) { return true; @@ -288,14 +285,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (!this.userDataSyncStoreService.userDataSyncStore) { throw new Error('Not enabled'); } - if (!(await this.userDataAuthTokenService.getToken())) { - throw new UserDataSyncError('Not Authenticated. Please sign in to start sync.', UserDataSyncErrorCode.Unauthorized); - } } - private onDidChangeAuthTokenStatus(token: string | undefined): void { - if (!token) { - this.stop(); - } - } } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index a5749a5bb8..401c2e17f7 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -458,7 +458,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic let workspacesToRestore: IWorkspacePathToOpen[] = []; if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath && !openConfig.cli['disable-restore-windows']) { let foldersToRestore = this.backupMainService.getFolderBackupPaths(); - foldersToAdd.push(...foldersToRestore.map(f => ({ folderUri: f, remoteAuhority: getRemoteAuthority(f), isRestored: true }))); + foldersToOpen.push(...foldersToRestore.map(f => ({ folderUri: f, remoteAuhority: getRemoteAuthority(f) }))); // collect from workspaces with hot-exit backups and from previous window session workspacesToRestore = [...this.backupMainService.getWorkspaceBackups(), ...this.workspacesMainService.getUntitledWorkspacesSync()]; diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index b508bdc40e..86c58351cc 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2465,6 +2465,53 @@ declare module 'vscode' { provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } + /** + * An EvaluatableExpression represents an expression in a document that can be evaluated by an active debugger or runtime. + * The result of this evaluation is shown in a tooltip-like widget. + * If only a range is specified, the expression will be extracted from the underlying document. + * An optional expression can be used to override the extracted expression. + * In this case the range is still used to highlight the range in the document. + */ + export class EvaluatableExpression { + + /* + * The range is used to extract the evaluatable expression from the underlying document and to highlight it. + */ + readonly range: Range; + + /* + * If specified the expression overrides the extracted expression. + */ + readonly expression?: string; + + /** + * Creates a new evaluatable expression object. + * + * @param range The range in the underlying document from which the evaluatable expression is extracted. + * @param expression If specified overrides the extracted expression. + */ + constructor(range: Range, expression?: string); + } + + /** + * The evaluatable expression provider interface defines the contract between extensions and + * the debug hover. + */ + export interface EvaluatableExpressionProvider { + + /** + * Provide an evaluatable expression for the given document and position. + * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression. + * + * @param document The document in which the debug hover is opened. + * @param position The position in the document where the debug hover is opened. + * @param token A cancellation token. + * @return An EvaluatableExpression or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideEvaluatableExpression(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + } + /** * A document highlight kind. */ @@ -9080,6 +9127,17 @@ declare module 'vscode' { */ export function registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Disposable; + /** + * Register a provider that locates evaluatable expressions in text documents. + * + * If multiple providers are registered for a language an arbitrary provider will be used. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An evaluatable expression provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerEvaluatableExpressionProvider(selector: DocumentSelector, provider: EvaluatableExpressionProvider): Disposable; + /** * Register a document highlight provider. * diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 4cfba2c344..848593501a 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -876,67 +876,7 @@ declare module 'vscode' { //#endregion - //#region locate evaluatable expressions for debug hover: https://github.com/microsoft/vscode/issues/89084 - - /** - * An EvaluatableExpression represents an expression in a document that can be evaluated by an active debugger or runtime. - * The result of this evaluation is shown in a tooltip-like widget. - * If only a range is specified, the expression will be extracted from the underlying document. - * An optional expression can be used to override the extracted expression. - * In this case the range is still used to highlight the range in the document. - */ - export class EvaluatableExpression { - /* - * The range is used to extract the evaluatable expression from the underlying document and to highlight it. - */ - readonly range: Range; - /* - * If specified the expression overrides the extracted expression. - */ - readonly expression?: string; - - /** - * Creates a new evaluatable expression object. - * - * @param range The range in the underlying document from which the evaluatable expression is extracted. - * @param expression If specified overrides the extracted expression. - */ - constructor(range: Range, expression?: string); - } - - /** - * The evaluatable expression provider interface defines the contract between extensions and - * the debug hover. - */ - export interface EvaluatableExpressionProvider { - - /** - * Provide an evaluatable expression for the given document and position. - * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression. - * - * @param document The document in which the command was invoked. - * @param position The position where the command was invoked. - * @param token A cancellation token. - * @return An EvaluatableExpression or a thenable that resolves to such. The lack of a result can be - * signaled by returning `undefined` or `null`. - */ - provideEvaluatableExpression(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; - } - - export namespace languages { - /** - * Register a provider that locates evaluatable expressions in text documents. - * - * If multiple providers are registered for a language an arbitrary provider will be used. - * - * @param selector A selector that defines the documents this provider is applicable to. - * @param provider An evaluatable expression provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. - */ - export function registerEvaluatableExpressionProvider(selector: DocumentSelector, provider: EvaluatableExpressionProvider): Disposable; - } - - // deprecated + //#region deprecated debug API export interface DebugConfigurationProvider { /** @@ -1734,4 +1674,33 @@ declare module 'vscode' { } //#endregion + + //#region Dialog title: https://github.com/microsoft/vscode/issues/82871 + + /** + * Options to configure the behaviour of a file open dialog. + * + * * Note 1: A dialog can select files, folders, or both. This is not true for Windows + * which enforces to open either files or folder, but *not both*. + * * Note 2: Explicitly setting `canSelectFiles` and `canSelectFolders` to `false` is futile + * and the editor then silently adjusts the options to select files. + */ + export interface OpenDialogOptions { + /** + * Dialog title + */ + title?: string; + } + + /** + * Options to configure the behaviour of a file save dialog. + */ + export interface SaveDialogOptions { + /** + * Dialog title + */ + title?: string; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadDialogs.ts b/src/vs/workbench/api/browser/mainThreadDialogs.ts index 27e06cc209..8bc0857665 100644 --- a/src/vs/workbench/api/browser/mainThreadDialogs.ts +++ b/src/vs/workbench/api/browser/mainThreadDialogs.ts @@ -37,7 +37,8 @@ export class MainThreadDialogs implements MainThreadDiaglogsShape { canSelectFiles: options.canSelectFiles || (!options.canSelectFiles && !options.canSelectFolders), canSelectFolders: options.canSelectFolders, canSelectMany: options.canSelectMany, - defaultUri: options.defaultUri ? URI.revive(options.defaultUri) : undefined + defaultUri: options.defaultUri ? URI.revive(options.defaultUri) : undefined, + title: options.title || undefined }; if (options.filters) { result.filters = []; @@ -49,7 +50,8 @@ export class MainThreadDialogs implements MainThreadDiaglogsShape { private static _convertSaveOptions(options: MainThreadDialogSaveOptions): ISaveDialogOptions { const result: ISaveDialogOptions = { defaultUri: options.defaultUri ? URI.revive(options.defaultUri) : undefined, - saveLabel: options.saveLabel || undefined + saveLabel: options.saveLabel || undefined, + title: options.title || undefined }; if (options.filters) { result.filters = []; diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 9e9b190442..657a31291d 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -10,7 +10,7 @@ import { RenderLineNumbersType, TextEditorCursorStyle, cursorStyleToString, Edit import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { IDecorationOptions, ScrollType } from 'vs/editor/common/editorCommon'; -import { IIdentifiedSingleEditOperation, ISingleEditOperation, ITextModel, ITextModelUpdateOptions } from 'vs/editor/common/model'; +import { ISingleEditOperation, ITextModel, ITextModelUpdateOptions, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, IUndoStopOptions, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol'; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 38157a142a..e134b58a52 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -50,6 +50,7 @@ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtens import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineCursor, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; +import { revive } from 'vs/base/common/marshalling'; // {{SQL CARBON EDIT}} import { ITreeItem as sqlITreeItem } from 'sql/workbench/common/views'; @@ -177,12 +178,14 @@ export interface MainThreadDialogOpenOptions { canSelectFolders?: boolean; canSelectMany?: boolean; filters?: { [name: string]: string[]; }; + title?: string; } export interface MainThreadDialogSaveOptions { defaultUri?: UriComponents; saveLabel?: string; filters?: { [name: string]: string[]; }; + title?: string; } export interface MainThreadDiaglogsShape extends IDisposable { @@ -1098,18 +1101,25 @@ export interface IWorkspaceSymbolsDto extends IdObject { symbols: IWorkspaceSymbolDto[]; } +export interface IWorkspaceEditEntryMetadataDto { + needsConfirmation: boolean; + label: string; + description?: string; + iconPath?: { id: string } | UriComponents | { light: UriComponents, dark: UriComponents }; +} + export interface IWorkspaceFileEditDto { oldUri?: UriComponents; newUri?: UriComponents; options?: modes.WorkspaceFileEditOptions - metadata?: modes.WorkspaceEditMetadata; + metadata?: IWorkspaceEditEntryMetadataDto; } export interface IWorkspaceTextEditDto { resource: UriComponents; edit: modes.TextEdit; modelVersionId?: number; - metadata?: modes.WorkspaceEditMetadata; + metadata?: IWorkspaceEditEntryMetadataDto; } export interface IWorkspaceEditDto { @@ -1128,6 +1138,9 @@ export function reviveWorkspaceEditDto(data: IWorkspaceEditDto | undefined): mod (edit).newUri = URI.revive((edit).newUri); (edit).oldUri = URI.revive((edit).oldUri); } + if (edit.metadata && edit.metadata.iconPath) { + edit.metadata = revive(edit.metadata); + } } } return data; diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index ba6418a543..1be2891d59 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -736,6 +736,35 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'list.toggleSelection', + weight: KeybindingWeight.WorkbenchContrib, + when: WorkbenchListFocusContextKey, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, + handler: (accessor) => { + const widget = accessor.get(IListService).lastFocusedList; + + if (!widget || isLegacyTree(widget)) { + return; + } + + const focus = widget.getFocus(); + + if (focus.length === 0) { + return; + } + + const selection = widget.getSelection(); + const index = selection.indexOf(focus[0]); + + if (index > -1) { + widget.setSelection([...selection.slice(0, index), ...selection.slice(index + 1)]); + } else { + widget.setSelection([...selection, focus[0]]); + } + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.toggleExpand', weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index c0ec047fc4..0688cca89a 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -28,7 +28,6 @@ import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/commo import { withNullAsUndefined } from 'vs/base/common/types'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { isStandalone } from 'vs/base/browser/browser'; -import { IModelService } from 'vs/editor/common/services/modelService'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; export interface IDraggedResource { @@ -343,7 +342,6 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: // Editors: enables cross window DND of tabs into the editor area const textFileService = accessor.get(ITextFileService); const editorService = accessor.get(IEditorService); - const modelService = accessor.get(IModelService); const draggedEditors: ISerializedDraggedEditor[] = []; files.forEach(file => { @@ -374,11 +372,8 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: // If the resource is dirty or untitled, send over its content // to restore dirty state. Get that from the text model directly let content: string | undefined = undefined; - if (textFileService.isDirty(file.resource)) { - const textModel = modelService.getModel(file.resource); - if (textModel) { - content = textModel.getValue(); - } + if (model?.isDirty()) { + content = model.textEditorModel.getValue(); } // Add as dragged editor diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 972315c9cf..cfdc31402c 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -148,8 +148,8 @@ export class ResourceLabels extends Disposable { })); // notify when untitled labels change - this.textFileService.untitled.onDidChangeLabel(resource => { - this._widgets.forEach(widget => widget.notifyUntitledLabelChange(resource)); + this.textFileService.untitled.onDidChangeLabel(model => { + this._widgets.forEach(widget => widget.notifyUntitledLabelChange(model.resource)); }); } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index c79b910756..3e4eb4a747 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -122,8 +122,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private workbenchGrid!: SerializableGrid; - private editorWidgetSet = new Set(); - private disposed: boolean | undefined; private titleBarPartView!: ISerializableView; @@ -198,7 +196,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi wasSideBarVisible: false, wasPanelVisible: false, transitionDisposables: new DisposableStore(), - setNotificationsFilter: false + setNotificationsFilter: false, + editorWidgetSet: new Set() }, }; @@ -708,15 +707,21 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi editor.updateOptions({ lineNumbers }); }; + const editorWidgetSet = this.state.zenMode.editorWidgetSet; if (!lineNumbers) { // Reset line numbers on all editors visible and non-visible - for (const editor of this.editorWidgetSet) { + for (const editor of editorWidgetSet) { setEditorLineNumbers(editor); } - this.editorWidgetSet.clear(); + editorWidgetSet.clear(); } else { this.editorService.visibleTextEditorWidgets.forEach(editor => { - this.editorWidgetSet.add(editor); + if (!editorWidgetSet.has(editor)) { + editorWidgetSet.add(editor); + this.state.zenMode.transitionDisposables.add(editor.onDidDispose(() => { + editorWidgetSet.delete(editor); + })); + } setEditorLineNumbers(editor); }); } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index cfe8466964..f5c0169ec9 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -470,7 +470,7 @@ export class BreadcrumbsControl { this._ckBreadcrumbsActive.set(value); } - private _revealInEditor(event: IBreadcrumbsItemEvent, element: any, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): void { + private _revealInEditor(event: IBreadcrumbsItemEvent, element: BreadcrumbElement, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): void { if (element instanceof FileElement) { if (element.kind === FileKind.FILE) { // open file in any editor diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index b27304aabc..cd8242dd6f 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -71,10 +71,8 @@ export class BaseSplitEditorAction extends Action { })); } - run(context?: IEditorIdentifier): Promise { + async run(context?: IEditorIdentifier): Promise { splitEditor(this.editorGroupService, this.direction, context); - - return Promise.resolve(true); } } @@ -183,7 +181,7 @@ export class JoinTwoGroupsAction extends Action { super(id, label); } - run(context?: IEditorIdentifier): Promise { + async run(context?: IEditorIdentifier): Promise { let sourceGroup: IEditorGroup | undefined; if (context && typeof context.groupId === 'number') { sourceGroup = this.editorGroupService.getGroup(context.groupId); @@ -198,12 +196,10 @@ export class JoinTwoGroupsAction extends Action { if (targetGroup && sourceGroup !== targetGroup) { this.editorGroupService.mergeGroup(sourceGroup, targetGroup); - return Promise.resolve(true); + break; } } } - - return Promise.resolve(true); } } @@ -220,10 +216,8 @@ export class JoinAllGroupsAction extends Action { super(id, label); } - run(context?: IEditorIdentifier): Promise { + async run(context?: IEditorIdentifier): Promise { mergeAllGroups(this.editorGroupService); - - return Promise.resolve(true); } } @@ -240,11 +234,9 @@ export class NavigateBetweenGroupsAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const nextGroup = this.editorGroupService.findGroup({ location: GroupLocation.NEXT }, this.editorGroupService.activeGroup, true); nextGroup.focus(); - - return Promise.resolve(true); } } @@ -261,10 +253,8 @@ export class FocusActiveGroupAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.editorGroupService.activeGroup.focus(); - - return Promise.resolve(true); } } @@ -279,13 +269,11 @@ export abstract class BaseFocusGroupAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const group = this.editorGroupService.findGroup(this.scope, this.editorGroupService.activeGroup, true); if (group) { group.focus(); } - - return Promise.resolve(true); } } @@ -421,7 +409,7 @@ export class OpenToSideFromQuickOpenAction extends Action { this.class = (preferredDirection === GroupDirection.RIGHT) ? 'codicon-split-horizontal' : 'codicon-split-vertical'; } - run(context: any): Promise { + async run(context: any): Promise { const entry = toEditorQuickOpenEntry(context); if (entry) { const input = entry.getInput(); @@ -436,8 +424,6 @@ export class OpenToSideFromQuickOpenAction extends Action { return this.editorService.openEditor(resourceInput, SIDE_GROUP); } } - - return Promise.resolve(false); } } @@ -490,7 +476,7 @@ export class CloseOneEditorAction extends Action { super(id, label, 'codicon-close'); } - run(context?: IEditorCommandsContext): Promise { + async run(context?: IEditorCommandsContext): Promise { let group: IEditorGroup | undefined; let editorIndex: number | undefined; if (context) { @@ -517,8 +503,6 @@ export class CloseOneEditorAction extends Action { if (group.activeEditor) { return group.closeEditor(group.activeEditor); } - - return Promise.resolve(false); } } @@ -554,8 +538,6 @@ export class RevertAndCloseEditorAction extends Action { group.closeEditor(editor); } - - return true; } } @@ -573,13 +555,11 @@ export class CloseLeftEditorsInGroupAction extends Action { super(id, label); } - run(context?: IEditorIdentifier): Promise { + async run(context?: IEditorIdentifier): Promise { const { group, editor } = getTarget(this.editorService, this.editorGroupService, context); if (group && editor) { return group.closeEditors({ direction: CloseDirection.LEFT, except: editor }); } - - return Promise.resolve(false); } } @@ -736,9 +716,9 @@ export class CloseEditorsInOtherGroupsAction extends Action { run(context?: IEditorIdentifier): Promise { const groupToSkip = context ? this.editorGroupService.getGroup(context.groupId) : this.editorGroupService.activeGroup; - return Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(g => { + return Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(async g => { if (groupToSkip && g.id === groupToSkip.id) { - return Promise.resolve(); + return; } return g.closeAllEditors(); @@ -760,13 +740,11 @@ export class CloseEditorInAllGroupsAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const activeEditor = this.editorService.activeEditor; if (activeEditor) { return Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(g => g.closeEditor(activeEditor))); } - - return Promise.resolve(); } } @@ -781,7 +759,7 @@ export class BaseMoveGroupAction extends Action { super(id, label); } - run(context?: IEditorIdentifier): Promise { + async run(context?: IEditorIdentifier): Promise { let sourceGroup: IEditorGroup | undefined; if (context && typeof context.groupId === 'number') { sourceGroup = this.editorGroupService.getGroup(context.groupId); @@ -795,8 +773,6 @@ export class BaseMoveGroupAction extends Action { this.editorGroupService.moveGroup(sourceGroup, targetGroup, this.direction); } } - - return Promise.resolve(true); } private findTargetGroup(sourceGroup: IEditorGroup): IEditorGroup | undefined { @@ -892,10 +868,8 @@ export class MinimizeOtherGroupsAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.editorGroupService.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); - - return Promise.resolve(false); } } @@ -908,10 +882,8 @@ export class ResetGroupSizesAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.editorGroupService.arrangeGroups(GroupsArrangement.EVEN); - - return Promise.resolve(false); } } @@ -924,10 +896,8 @@ export class ToggleGroupSizesAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.editorGroupService.arrangeGroups(GroupsArrangement.TOGGLE); - - return Promise.resolve(false); } } @@ -946,13 +916,11 @@ export class MaximizeGroupAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { if (this.editorService.activeEditor) { this.editorGroupService.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); this.layoutService.setSideBarHidden(true); } - - return Promise.resolve(false); } } @@ -967,23 +935,21 @@ export abstract class BaseNavigateEditorAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const result = this.navigate(); if (!result) { - return Promise.resolve(false); + return; } const { groupId, editor } = result; if (!editor) { - return Promise.resolve(false); + return; } const group = this.editorGroupService.getGroup(groupId); if (group) { return group.openEditor(editor); } - - return Promise.resolve(); } protected abstract navigate(): IEditorIdentifier | undefined; @@ -1158,10 +1124,8 @@ export class NavigateForwardAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.historyService.forward(); - - return Promise.resolve(); } } @@ -1174,10 +1138,8 @@ export class NavigateBackwardsAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.historyService.back(); - - return Promise.resolve(); } } @@ -1190,10 +1152,8 @@ export class NavigateToLastEditLocationAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.historyService.openLastEditLocation(); - - return Promise.resolve(); } } @@ -1206,10 +1166,8 @@ export class NavigateLastAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.historyService.last(); - - return Promise.resolve(); } } @@ -1226,10 +1184,8 @@ export class ReopenClosedEditorAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.historyService.reopenLastClosedEditor(); - - return Promise.resolve(false); } } @@ -1247,15 +1203,13 @@ export class ClearRecentFilesAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { // Clear global recently opened this.workspacesService.clearRecentlyOpened(); // Clear workspace specific recently opened this.historyService.clearRecentlyOpened(); - - return Promise.resolve(false); } } @@ -1313,12 +1267,10 @@ export class BaseQuickOpenEditorAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const keybindings = this.keybindingService.lookupKeybindings(this.id); this.quickOpenService.show(this.prefix, { quickNavigateConfiguration: { keybindings } }); - - return Promise.resolve(true); } } @@ -1396,12 +1348,10 @@ export class QuickOpenPreviousEditorFromHistoryAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const keybindings = this.keybindingService.lookupKeybindings(this.id); this.quickOpenService.show(undefined, { quickNavigateConfiguration: { keybindings } }); - - return Promise.resolve(true); } } @@ -1418,10 +1368,8 @@ export class OpenNextRecentlyUsedEditorAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.historyService.openNextRecentlyUsedEditor(); - - return Promise.resolve(); } } @@ -1438,10 +1386,8 @@ export class OpenPreviousRecentlyUsedEditorAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.historyService.openPreviouslyUsedEditor(); - - return Promise.resolve(); } } @@ -1459,10 +1405,8 @@ export class OpenNextRecentlyUsedEditorInGroupAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.historyService.openNextRecentlyUsedEditor(this.editorGroupsService.activeGroup.id); - - return Promise.resolve(); } } @@ -1480,10 +1424,8 @@ export class OpenPreviousRecentlyUsedEditorInGroupAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.historyService.openPreviouslyUsedEditor(this.editorGroupsService.activeGroup.id); - - return Promise.resolve(); } } @@ -1500,12 +1442,10 @@ export class ClearEditorHistoryAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { // Editor history this.historyService.clear(); - - return Promise.resolve(true); } } @@ -1772,10 +1712,8 @@ export class BaseCreateEditorGroupAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { this.editorGroupService.addGroup(this.editorGroupService.activeGroup, this.direction, { activate: true }); - - return Promise.resolve(true); } } diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index e8c39396e8..f232e58312 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -317,8 +317,8 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private registerListeners(): void { this._register(this.editorService.onDidActiveEditorChange(() => this.updateStatusBar())); - this._register(this.textFileService.untitled.onDidChangeEncoding(r => this.onResourceEncodingChange(r))); - this._register(this.textFileService.files.onDidChangeEncoding(m => this.onResourceEncodingChange((m.resource)))); + this._register(this.textFileService.untitled.onDidChangeEncoding(model => this.onResourceEncodingChange(model.resource))); + this._register(this.textFileService.files.onDidChangeEncoding(model => this.onResourceEncodingChange((model.resource)))); this._register(TabFocus.onDidChangeTabFocus(e => this.onTabFocusModeChange())); } diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 9e96a1c7ab..1732dc209f 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -30,7 +30,6 @@ import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/commo import { CancellationToken } from 'vs/base/common/cancellation'; import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorActivation, IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; /** * The text editor that leverages the diff text editor for the editing experience. @@ -51,8 +50,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService, @IEditorService editorService: IEditorService, @IThemeService themeService: IThemeService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IClipboardService private clipboardService: IClipboardService + @IEditorGroupsService editorGroupService: IEditorGroupsService ) { super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService); } @@ -75,10 +73,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { } createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): IDiffEditor { - if (this.reverseColor) { // {{SQL CARBON EDIT}} - (configuration as IDiffEditorOptions).reverse = true; - } - return this.instantiationService.createInstance(DiffEditorWidget as any, parent, configuration, this.clipboardService); // {{SQL CARBON EDIT}} strict-null-check...i guess? + if (this.reverseColor) { (configuration as IDiffEditorOptions).reverse = true; } // {{SQL CARBON EDIT}} + return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration); } async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index a326663bc3..58b7d4b50b 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -12,7 +12,7 @@ import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass } from 'vs/base/browser/dom'; import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { firstIndex } from 'vs/base/common/arrays'; -import { IAction, IActionRunner, ActionRunner } from 'vs/base/common/actions'; +import { IAction } from 'vs/base/common/actions'; import { IActionViewItem, ActionsOrientation, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { Registry } from 'vs/platform/registry/common/platform'; import { prepareActions } from 'vs/workbench/browser/actions'; @@ -51,7 +51,6 @@ export interface IPaneColors extends IColorMapping { } export interface IViewPaneOptions extends IPaneOptions { - actionRunner?: IActionRunner; id: string; title: string; showActionsAlways?: boolean; @@ -169,7 +168,6 @@ export abstract class ViewPane extends Pane implements IView { private readonly menuActions: ViewMenuActions; - protected actionRunner?: IActionRunner; private toolbar?: ToolBar; private readonly showActionsAlways: boolean = false; private headerContainer?: HTMLElement; @@ -196,7 +194,6 @@ export abstract class ViewPane extends Pane implements IView { this.id = options.id; this.title = options.title; - this.actionRunner = options.actionRunner; this.showActionsAlways = !!options.showActionsAlways; this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); @@ -262,7 +259,6 @@ export abstract class ViewPane extends Pane implements IView { actionViewItemProvider: action => this.getActionViewItem(action), ariaLabel: nls.localize('viewToolbarAriaLabel', "{0} actions", this.title), getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id), - actionRunner: this.actionRunner }); this._register(this.toolbar); @@ -311,7 +307,9 @@ export abstract class ViewPane extends Pane implements IView { } focus(): void { - if (this.element) { + if (this.shouldShowWelcome()) { + this.viewWelcomeContainer.focus(); + } else if (this.element) { this.element.focus(); this._onDidFocus.fire(); } @@ -453,8 +451,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { private didLayout = false; private dimension: Dimension | undefined; - protected actionRunner: IActionRunner | undefined; - private readonly visibleViewsCountFromCache: number | undefined; private readonly visibleViewsStorageId: string; protected readonly viewsModel: PersistentContributableViewsModel; @@ -800,7 +796,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { { id: viewDescriptor.id, title: viewDescriptor.name, - actionRunner: this.getActionRunner(), expanded: !collapsed, minimumBodySize: this.viewDescriptorService.getViewContainerLocation(this.viewContainer) === ViewContainerLocation.Panel ? 0 : 120 }); @@ -831,14 +826,6 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return panes; } - getActionRunner(): IActionRunner { - if (!this.actionRunner) { - this.actionRunner = new ActionRunner(); - } - - return this.actionRunner; - } - private onDidRemoveViewDescriptors(removed: IViewDescriptorRef[]): void { removed = removed.sort((a, b) => b.index - a.index); const panesToRemove: ViewPane[] = []; diff --git a/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index 952714c66c..7e29fb30d7 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/src/vs/workbench/common/contributions.ts @@ -118,7 +118,7 @@ class WorkbenchContributionsRegistry implements IWorkbenchContributionsRegistry try { instantiationService.createInstance(ctor); } catch (error) { - console.error(`Unable to instantiate workbench contribution ${(ctor as any).name}.`, error); + console.error(`Unable to instantiate workbench contribution ${ctor.name}.`, error); } } } diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 0b7f657b81..1ded1b43a3 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -165,7 +165,7 @@ export interface IFileInputFactory { createFileInput(resource: URI, encoding: string | undefined, mode: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; - isFileInput(obj: any): obj is IFileEditorInput; + isFileInput(obj: unknown): obj is IFileEditorInput; } export interface IEditorInputFactoryRegistry { diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 57cc301997..6d18b1f2f5 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -46,8 +46,8 @@ export interface ISerializedEditorGroup { preview?: number; } -export function isSerializedEditorGroup(obj?: any): obj is ISerializedEditorGroup { - const group: ISerializedEditorGroup = obj; +export function isSerializedEditorGroup(obj?: unknown): obj is ISerializedEditorGroup { + const group = obj as ISerializedEditorGroup; return obj && typeof obj === 'object' && Array.isArray(group.editors) && Array.isArray(group.mru); } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts index e574f4ff66..dffcd241b2 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts @@ -25,6 +25,7 @@ import { basename } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { WorkspaceFileEdit } from 'vs/editor/common/modes'; import { compare } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; // --- VIEW MODEL @@ -420,6 +421,12 @@ export class CategoryElementRenderer implements ITreeRenderer ({ label: editorDescriptor.displayName, id: editorDescriptor.id, description: editorDescriptor.id === currentlyOpenedEditorType ? nls.localize('openWithCurrentlyActive', "Currently Active") - : undefined + : undefined, + buttons: resourceExt ? [{ + iconClass: 'codicon-settings-gear', + tooltip: nls.localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", resourceExt) + }] : undefined })); - const pick = await this.quickInputService.pick(items, { - placeHolder: nls.localize('promptOpenWith.placeHolder', "Select editor to use for '{0}'...", basename(resource)), + + const picker = this.quickInputService.createQuickPick(); + picker.items = items; + picker.placeholder = nls.localize('promptOpenWith.placeHolder', "Select editor to use for '{0}'...", basename(resource)); + + const pick = await new Promise(resolve => { + picker.onDidAccept(() => { + resolve(picker.selectedItems.length === 1 ? picker.selectedItems[0].id : undefined); + picker.dispose(); + }); + picker.onDidTriggerItemButton(e => { + const pick = e.item.id; + resolve(pick); // open the view + picker.dispose(); + + // And persist the setting + if (pick) { + const newAssociation: CustomEditorAssociation = { viewType: pick, filenamePattern: '*' + resourceExt }; + const currentAssociations = [...this.configurationService.getValue(customEditorsAssociationsKey)] || []; + + // First try updating existing association + for (let i = 0; i < currentAssociations.length; ++i) { + const existing = currentAssociations[i]; + if (existing.filenamePattern === newAssociation.filenamePattern) { + currentAssociations.splice(i, 1, newAssociation); + this.configurationService.updateValue(customEditorsAssociationsKey, currentAssociations); + return; + } + } + + // Otherwise, create a new one + currentAssociations.unshift(newAssociation); + this.configurationService.updateValue(customEditorsAssociationsKey, currentAssociations); + } + }); + picker.show(); }); - if (!pick || !pick.id) { + if (!pick) { return undefined; // {{SQL CARBON EDIT}} strict-null-check } - return this.openWith(resource, pick.id, options, group); + + return this.openWith(resource, pick, options, group); } public openWith( @@ -312,7 +353,11 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ export const customEditorsAssociationsKey = 'workbench.experimental.editorAssociations'; -export type CustomEditorsAssociations = readonly (CustomEditorSelector & { readonly viewType: string; })[]; +export type CustomEditorAssociation = CustomEditorSelector & { + readonly viewType: string; +}; + +export type CustomEditorsAssociations = readonly CustomEditorAssociation[]; export class CustomEditorContribution extends Disposable implements IWorkbenchContribution { constructor( diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index cf35bfe141..d41f59c1e0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -915,14 +915,13 @@ export class DebugSession implements IDebugSession { // Disconnects and clears state. Session can be initialized again for a new connection. private shutdown(): void { dispose(this.rawListeners); + if (this.raw) { + this.raw.disconnect(); + this.raw.dispose(); + this.raw = undefined; + } this.fetchThreadsScheduler = undefined; this.model.clearThreads(this.getId(), true); - if (this.raw) { - const raw = this.raw; - this.raw = undefined; - raw.disconnect(); - raw.dispose(); - } this._onDidChangeState.fire(); } diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 20a21600d2..c70090a612 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -13,31 +13,6 @@ height: 100%; } -.debug-pane .debug-start-view { - padding: 0 20px 0 20px; -} - -.debug-pane .debug-start-view .monaco-button, -.debug-pane .debug-start-view .section { - margin-top: 20px; -} - -.debug-pane .debug-start-view .top-section { - margin-top: 10px; -} - -.debug-pane .debug-start-view .monaco-button { - max-width: 260px; - margin-left: auto; - margin-right: auto; - display: block; -} - -.debug-pane .debug-start-view .click { - cursor: pointer; - color: #007ACC; -} - .monaco-workbench .debug-action.notification:after { content: ''; width: 6px; diff --git a/src/vs/workbench/contrib/debug/browser/startView.ts b/src/vs/workbench/contrib/debug/browser/startView.ts index f78e8158fc..f36a7d22ce 100644 --- a/src/vs/workbench/contrib/debug/browser/startView.ts +++ b/src/vs/workbench/contrib/debug/browser/startView.ts @@ -3,64 +3,33 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from 'vs/base/browser/dom'; -import { Button } from 'vs/base/browser/ui/button/button'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { StartAction, ConfigureAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { equals } from 'vs/base/common/arrays'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IViewDescriptorService, IViewsRegistry, Extensions } from 'vs/workbench/common/views'; +import { Registry } from 'vs/platform/registry/common/platform'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -const $ = dom.$; +import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { OpenFolderAction, OpenFileAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; +import { isMacintosh } from 'vs/base/common/platform'; -interface DebugStartMetrics { - debuggers?: string[]; -} -type DebugStartMetricsClassification = { - debuggers?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; -}; - -function createClickElement(textContent: string, action: () => any): HTMLSpanElement { - const clickElement = $('span.click'); - clickElement.textContent = textContent; - clickElement.onclick = action; - clickElement.tabIndex = 0; - clickElement.onkeyup = (e) => { - const keyboardEvent = new StandardKeyboardEvent(e); - if (keyboardEvent.keyCode === KeyCode.Enter || (keyboardEvent.keyCode === KeyCode.Space)) { - action(); - } - }; - - return clickElement; -} +const CONTEXT_DEBUGGER_INTERESTED = new RawContextKey('debuggerInterested', false); export class StartView extends ViewPane { static ID = 'workbench.debug.startView'; static LABEL = localize('start', "Start"); - private debugButton!: Button; - private firstMessageContainer!: HTMLElement; - private secondMessageContainer!: HTMLElement; - private clickElement: HTMLElement | undefined; - private debuggerLabels: string[] | undefined = undefined; + private debuggerInterestedContext: IContextKey; constructor( options: IViewletViewOptions, @@ -69,125 +38,45 @@ export class StartView extends ViewPane { @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService private readonly commandService: ICommandService, @IDebugService private readonly debugService: IDebugService, @IEditorService private readonly editorService: IEditorService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IFileDialogService private readonly dialogService: IFileDialogService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @IOpenerService openerService: IOpenerService, ) { super({ ...(options as IViewPaneOptions), ariaHeaderLabel: localize('debugStart', "Debug Start Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); - this._register(editorService.onDidActiveEditorChange(() => this.updateView())); - this._register(this.debugService.getConfigurationManager().onDidRegisterDebugger(() => this.updateView())); + + this.debuggerInterestedContext = CONTEXT_DEBUGGER_INTERESTED.bindTo(contextKeyService); + const setContextKey = () => { + const activeEditor = this.editorService.activeTextEditorWidget; + const debuggerLabels = this.debugService.getConfigurationManager().getDebuggerLabelsForEditor(activeEditor); + this.debuggerInterestedContext.set(debuggerLabels.length > 0); + }; + this._register(editorService.onDidActiveEditorChange(setContextKey)); + this._register(this.debugService.getConfigurationManager().onDidRegisterDebugger(setContextKey)); } - private updateView(): void { - const activeEditor = this.editorService.activeTextEditorWidget; - const debuggerLabels = this.debugService.getConfigurationManager().getDebuggerLabelsForEditor(activeEditor); - if (!equals(this.debuggerLabels, debuggerLabels)) { - this.debuggerLabels = debuggerLabels; - const enabled = this.debuggerLabels.length > 0; - - this.debugButton.enabled = enabled; - const debugKeybinding = this.keybindingService.lookupKeybinding(StartAction.ID); - let debugLabel = this.debuggerLabels.length !== 1 ? localize('debug', "Run and Debug") : localize('debugWith', "Run and Debug {0}", this.debuggerLabels[0]); - if (debugKeybinding) { - debugLabel += ` (${debugKeybinding.getLabel()})`; - } - this.debugButton.label = debugLabel; - - const emptyWorkbench = this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY; - this.firstMessageContainer.innerHTML = ''; - this.secondMessageContainer.innerHTML = ''; - const secondMessageElement = $('span'); - this.secondMessageContainer.appendChild(secondMessageElement); - - const setSecondMessage = () => { - secondMessageElement.textContent = localize('specifyHowToRun', "To customize Run and Debug"); - this.clickElement = createClickElement(localize('configure', " create a launch.json file."), () => { - this.telemetryService.publicLog2('debugStart.configure', { debuggers: this.debuggerLabels }); - this.commandService.executeCommand(ConfigureAction.ID); - }); - this.secondMessageContainer.appendChild(this.clickElement); - }; - const setSecondMessageWithFolder = () => { - secondMessageElement.textContent = localize('noLaunchConfiguration', "To customize Run and Debug, "); - this.clickElement = createClickElement(localize('openFolder', " open a folder"), () => { - this.telemetryService.publicLog2('debugStart.openFolder', { debuggers: this.debuggerLabels }); - this.dialogService.pickFolderAndOpen({ forceNewWindow: false }); - }); - this.secondMessageContainer.appendChild(this.clickElement); - - const moreText = $('span.moreText'); - moreText.textContent = localize('andconfigure', " and create a launch.json file."); - this.secondMessageContainer.appendChild(moreText); - }; - - if (enabled && !emptyWorkbench) { - setSecondMessage(); - } - - if (enabled && emptyWorkbench) { - setSecondMessageWithFolder(); - } - - if (!enabled && !emptyWorkbench) { - const firstMessageElement = $('span'); - this.firstMessageContainer.appendChild(firstMessageElement); - firstMessageElement.textContent = localize('simplyDebugAndRun', "Open a file which can be debugged or run."); - - setSecondMessage(); - } - - if (!enabled && emptyWorkbench) { - this.clickElement = createClickElement(localize('openFile', "Open a file"), () => { - this.telemetryService.publicLog2('debugStart.openFile'); - this.dialogService.pickFileAndOpen({ forceNewWindow: false }); - }); - this.firstMessageContainer.appendChild(this.clickElement); - const firstMessageElement = $('span'); - this.firstMessageContainer.appendChild(firstMessageElement); - firstMessageElement.textContent = localize('canBeDebuggedOrRun', " which can be debugged or run."); - - setSecondMessageWithFolder(); - } - } - } - - protected renderBody(container: HTMLElement): void { - super.renderBody(container); - - this.firstMessageContainer = $('.top-section'); - container.appendChild(this.firstMessageContainer); - - this.debugButton = new Button(container); - this._register(this.debugButton.onDidClick(() => { - this.commandService.executeCommand(StartAction.ID); - this.telemetryService.publicLog2('debugStart.runAndDebug', { debuggers: this.debuggerLabels }); - })); - attachButtonStyler(this.debugButton, this.themeService); - - dom.addClass(this.element, 'debug-pane'); - dom.addClass(container, 'debug-start-view'); - - this.secondMessageContainer = $('.section'); - container.appendChild(this.secondMessageContainer); - - this.updateView(); - } - - protected layoutBody(_: number, __: number): void { - // no-op - } - - focus(): void { - if (this.debugButton.enabled) { - this.debugButton.focus(); - } else if (this.clickElement) { - this.clickElement.focus(); - } + shouldShowWelcome(): boolean { + return true; } } + +const viewsRegistry = Registry.as(Extensions.ViewsRegistry); +viewsRegistry.registerViewWelcomeContent(StartView.ID, { + content: localize('openAFileWhichCanBeDebugged', "[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID), + when: CONTEXT_DEBUGGER_INTERESTED.toNegated() +}); + +viewsRegistry.registerViewWelcomeContent(StartView.ID, { + content: localize('runAndDebugAction', "[Run and Debug](command:{0})", StartAction.ID) +}); + +viewsRegistry.registerViewWelcomeContent(StartView.ID, { + content: localize('customizeRunAndDebug', "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), + when: WorkbenchStateContext.notEqualsTo('empty') +}); + +viewsRegistry.registerViewWelcomeContent(StartView.ID, { + content: localize('customizeRunAndDebugOpenFolder', "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID), + when: WorkbenchStateContext.isEqualTo('empty') +}); diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 16d06f535a..62e54ca424 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -1773,6 +1773,16 @@ declare module DebugProtocol { If missing the value 0 is assumed which results in the completion text being inserted. */ length?: number; + /** Determines the start of the new selection after the text has been inserted (or replaced). + The start position must in the range 0 and length of the completion text. + If omitted the selection starts at the end of the completion text. + */ + selectionStart?: number; + /** Determines the length of the new selection after the text has been inserted (or replaced). + The selection can not extend beyond the bounds of the completion text. + If omitted the length is assumed to be 0. + */ + selectionLength?: number; } /** Some predefined types for the CompletionItem. Please note that not all clients have specific icons for all of them. */ diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index e0564ea843..03b2f34e17 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -14,7 +14,7 @@ import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } fro import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, @@ -48,6 +48,8 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { CONTEXT_SYNC_ENABLEMENT } from 'vs/platform/userDataSync/common/userDataSync'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -443,6 +445,33 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + + constructor() { + super({ + id: TOGGLE_IGNORE_EXTENSION_ACTION_ID, + title: { value: localize('workbench.extensions.action.toggleIgnoreExtension', "Don't Sync This Extension"), original: `Don't Sync This Extension` }, + menu: { + id: MenuId.ExtensionContext, + group: '2_configure', + when: CONTEXT_SYNC_ENABLEMENT + }, + }); + } + + async run(accessor: ServicesAccessor, id: string) { + const configurationService = accessor.get(IConfigurationService); + const ignoredExtensions = [...configurationService.getValue('sync.ignoredExtensions')]; + const index = ignoredExtensions.findIndex(ignoredExtension => areSameExtensions({ id: ignoredExtension }, { id })); + if (index !== -1) { + ignoredExtensions.splice(index, 1); + } else { + ignoredExtensions.push(id); + } + return configurationService.updateValue('sync.ignoredExtensions', ignoredExtensions.length ? ignoredExtensions : undefined, ConfigurationTarget.USER); + } +}); + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); class ExtensionsContributions implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index f1d60fbd02..050f0921c6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -13,7 +13,7 @@ import * as json from 'vs/base/common/json'; import { ActionViewItem, Separator, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { dispose, Disposable } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, EXTENSIONS_CONFIG } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, EXTENSIONS_CONFIG, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { ExtensionsLabel, IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionTipsService, IExtensionRecommendation, IExtensionsConfigContent, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -679,7 +679,7 @@ export class DropDownMenuActionViewItem extends ExtensionActionViewItem { } } -export function getContextMenuActions(menuService: IMenuService, contextKeyService: IContextKeyService, extension: IExtension | undefined | null): ExtensionAction[][] { +export function getContextMenuActions(menuService: IMenuService, contextKeyService: IContextKeyService, instantiationService: IInstantiationService, extension: IExtension | undefined | null): ExtensionAction[][] { const scopedContextKeyService = contextKeyService.createScoped(); if (extension) { scopedContextKeyService.createKey('isBuiltinExtension', extension.type === ExtensionType.System); @@ -691,7 +691,7 @@ export function getContextMenuActions(menuService: IMenuService, contextKeyServi const groups: ExtensionAction[][] = []; const menu = menuService.createMenu(MenuId.ExtensionContext, scopedContextKeyService); - menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => new MenuItemExtensionAction(action)))); + menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => instantiationService.createInstance(MenuItemExtensionAction, action)))); menu.dispose(); return groups; @@ -745,7 +745,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction { groups.push([this.instantiationService.createInstance(UninstallAction)]); groups.push([this.instantiationService.createInstance(InstallAnotherVersionAction)]); - getContextMenuActions(this.menuService, this.contextKeyService, this.extension).forEach(actions => groups.push(actions)); + getContextMenuActions(this.menuService, this.contextKeyService, this.instantiationService, this.extension).forEach(actions => groups.push(actions)); groups.forEach(group => group.forEach(extensionAction => extensionAction.extension = this.extension)); @@ -773,11 +773,21 @@ export class ManageExtensionAction extends ExtensionDropDownAction { export class MenuItemExtensionAction extends ExtensionAction { - constructor(private readonly action: IAction) { + constructor( + private readonly action: IAction, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { super(action.id, action.label); } - update() { } + update() { + if (!this.extension) { + return; + } + if (this.action.id === TOGGLE_IGNORE_EXTENSION_ACTION_ID) { + this.checked = this.configurationService.getValue('sync.ignoredExtensions').some(id => areSameExtensions({ id }, this.extension!.identifier)); + } + } async run(): Promise { if (this.extension) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 2ad5f0b677..ddce32c7ad 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -247,7 +247,7 @@ export class ExtensionsListView extends ViewPane { getActions: () => actions.slice(0, actions.length - 1) }); } else if (e.element) { - const groups = getContextMenuActions(this.menuService, this.contextKeyService.createScoped(), e.element); + const groups = getContextMenuActions(this.menuService, this.contextKeyService.createScoped(), this.instantiationService, e.element); groups.forEach(group => group.forEach(extensionAction => extensionAction.extension = e.element!)); let actions: IAction[] = []; for (const menuActions of groups) { diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 7e3a55c928..961fdd2eee 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -143,3 +143,5 @@ export class ExtensionContainers extends Disposable { } } } + +export const TOGGLE_IGNORE_EXTENSION_ACTION_ID = 'workbench.extensions.action.toggleIgnoreExtension'; diff --git a/src/vs/workbench/contrib/extensions/common/extensionsInput.ts b/src/vs/workbench/contrib/extensions/common/extensionsInput.ts index dbd1861fa1..42b50901aa 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionsInput.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionsInput.ts @@ -13,13 +13,15 @@ export class ExtensionsInput extends EditorInput { static readonly ID = 'workbench.extensions.input2'; get extension(): IExtension { return this._extension; } - readonly resource = URI.from({ - scheme: 'extension', - path: this.extension.identifier.id - }); + get resource() { + return URI.from({ + scheme: 'extension', + path: this.extension.identifier.id + }); + } constructor( - private _extension: IExtension, + private readonly _extension: IExtension ) { super(); } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts index 7c0a2f1649..aac8a7778f 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput.ts @@ -16,10 +16,6 @@ export class RuntimeExtensionsInput extends EditorInput { path: 'default' }); - constructor() { - super(); - } - getTypeId(): string { return RuntimeExtensionsInput.ID; } diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts index 542ed3e58c..bda93e0034 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts @@ -7,7 +7,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { URI } from 'vs/base/common/uri'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { toResource, SideBySideEditorInput, IWorkbenchEditorConfiguration, SideBySideEditor as SideBySideEditorChoice } from 'vs/workbench/common/editor'; -import { ITextFileService, ModelState } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; @@ -62,9 +62,9 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); // Ensure dirty text file and untitled models are always opened as editors - this._register(this.textFileService.files.onDidChangeDirty(m => this.ensureDirtyFilesAreOpenedWorker.work(m.resource))); - this._register(this.textFileService.files.onDidSaveError(m => this.ensureDirtyFilesAreOpenedWorker.work(m.resource))); - this._register(this.textFileService.untitled.onDidChangeDirty(r => this.ensureDirtyFilesAreOpenedWorker.work(r))); + this._register(this.textFileService.files.onDidChangeDirty(model => this.ensureDirtyFilesAreOpenedWorker.work(model.resource))); + this._register(this.textFileService.files.onDidSaveError(model => this.ensureDirtyFilesAreOpenedWorker.work(model.resource))); + this._register(this.textFileService.untitled.onDidChangeDirty(model => this.ensureDirtyFilesAreOpenedWorker.work(model.resource))); // Out of workspace file watchers this._register(this.editorService.onDidVisibleEditorsChange(() => this.onDidVisibleEditorsChange())); @@ -290,7 +290,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut } const model = this.textFileService.files.get(resource); - if (model?.hasState(ModelState.PENDING_SAVE)) { + if (model?.hasState(TextFileEditorModelState.PENDING_SAVE)) { return false; // resource must not be pending to save } @@ -369,18 +369,14 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut } const model = this.textFileService.files.get(resource); - if (!model) { - return undefined; - } - - if (model.isDirty()) { + if (!model || model.isDirty() || !model.isResolved()) { return undefined; } return model; })), model => model.resource.toString() - ).forEach(model => model.load()); + ).forEach(model => this.textFileService.files.resolve(model.resource, { reload: { async: true } })); } } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 00fe780618..efeea54114 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -10,7 +10,7 @@ import { isValidBasename } from 'vs/base/common/extpath'; import { basename } from 'vs/base/common/resources'; import { Action } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; -import { ITextFileEditorModel, ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; import { EditorOptions, TextEditorOptions, IEditorCloseEvent } from 'vs/workbench/common/editor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; @@ -135,7 +135,7 @@ export class TextFileEditor extends BaseTextEditor { return this.openAsBinary(input, options); } - const textFileModel = resolvedModel; + const textFileModel = resolvedModel; // Editor const textEditor = assertIsDefined(this.getControl()); diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 615ecdb9d3..c56a2627fb 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -221,13 +221,11 @@ class DoNotShowResolveConflictLearnMoreAction extends Action { super('workbench.files.action.resolveConflictLearnMoreDoNotShowAgain', nls.localize('dontShowAgain', "Don't Show Again")); } - run(notification: IDisposable): Promise { + async run(notification: IDisposable): Promise { this.storageService.store(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, true, StorageScope.GLOBAL); // Hide notification notification.dispose(); - - return Promise.resolve(); } } @@ -262,8 +260,6 @@ class ResolveSaveConflictAction extends Action { Event.once(handle.onDidClose)(() => dispose(actions.primary!)); pendingResolveSaveConflictMessages.push(handle); } - - return Promise.resolve(true); } } @@ -276,7 +272,7 @@ class SaveElevatedAction extends Action { super('workbench.files.action.saveElevated', triedToMakeWriteable ? isWindows ? nls.localize('overwriteElevated', "Overwrite as Admin...") : nls.localize('overwriteElevatedSudo', "Overwrite as Sudo...") : isWindows ? nls.localize('saveElevated', "Retry as Admin...") : nls.localize('saveElevatedSudo', "Retry as Sudo...")); } - run(): Promise { + async run(): Promise { if (!this.model.isDisposed()) { this.model.save({ writeElevated: true, @@ -284,8 +280,6 @@ class SaveElevatedAction extends Action { reason: SaveReason.EXPLICIT }); } - - return Promise.resolve(true); } } @@ -297,12 +291,10 @@ class OverwriteReadonlyAction extends Action { super('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite")); } - run(): Promise { + async run(): Promise { if (!this.model.isDisposed()) { this.model.save({ overwriteReadonly: true, reason: SaveReason.EXPLICIT }); } - - return Promise.resolve(true); } } @@ -314,12 +306,10 @@ class SaveIgnoreModifiedSinceAction extends Action { super('workbench.files.action.saveIgnoreModifiedSince', nls.localize('overwrite', "Overwrite")); } - run(): Promise { + async run(): Promise { if (!this.model.isDisposed()) { this.model.save({ ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); } - - return Promise.resolve(true); } } @@ -331,10 +321,8 @@ class ConfigureSaveConflictAction extends Action { super('workbench.files.action.configureSaveConflict', nls.localize('configure', "Configure")); } - run(): Promise { + async run(): Promise { this.preferencesService.openSettings(undefined, 'files.saveConflictResolution'); - - return Promise.resolve(true); } } diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 708a8cd21b..5832af7777 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -18,7 +18,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewContainersRegistry, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -34,6 +34,9 @@ import { KeyChord, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { WorkbenchStateContext, RemoteNameContext, IsWebContext } from 'vs/workbench/browser/contextkeys'; +import { AddRootFolderAction, OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; +import { isMacintosh } from 'vs/base/common/platform'; export class ExplorerViewletViewsContribution extends Disposable implements IWorkbenchContribution { @@ -61,6 +64,23 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor private registerViews(): void { const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + + viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + content: localize('noWorkspaceHelp', "You have not yet added a folder to the workspace.\n[Add Folder](command:{0})", AddRootFolderAction.ID), + when: WorkbenchStateContext.isEqualTo('workspace') + }); + + const commandId = isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID; + viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + content: localize('remoteNoFolderHelp', "Connected to remote.\n[Open Folder](command:{0})", commandId), + when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), RemoteNameContext.notEqualsTo(''), IsWebContext.toNegated()) + }); + + viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + content: localize('noFolderHelp', "You have not yet opened a folder.\n[Open Folder](command:{0})", commandId), + when: ContextKeyExpr.or(ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), RemoteNameContext.isEqualTo('')), ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), IsWebContext)) + }); + const viewDescriptors = viewsRegistry.getViews(VIEW_CONTAINER); let viewDescriptorsToRegister: IViewDescriptor[] = []; diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 07cf1ed4f0..8ed8ab1f39 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -166,7 +166,7 @@ export class GlobalNewUntitledFileAction extends Action { } } -async function deleteFiles(workingCopyService: IWorkingCopyService, workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { +async function deleteFiles(workingCopyFileService: IWorkingCopyFileService, dialogService: IDialogService, configurationService: IConfigurationService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { let primaryButton: string; if (useTrash) { primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash"); @@ -174,20 +174,24 @@ async function deleteFiles(workingCopyService: IWorkingCopyService, workingCopyF primaryButton = nls.localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete"); } - const distinctElements = resources.distinctParents(elements, e => e.resource); - // Handle dirty + const distinctElements = resources.distinctParents(elements, e => e.resource); + const dirtyWorkingCopies = new Set(); + for (const distinctElement of distinctElements) { + for (const dirtyWorkingCopy of workingCopyFileService.getDirty(distinctElement.resource)) { + dirtyWorkingCopies.add(dirtyWorkingCopy); + } + } let confirmed = true; - const dirtyWorkingCopies = workingCopyService.dirtyWorkingCopies.filter(workingCopy => distinctElements.some(e => resources.isEqualOrParent(workingCopy.resource, e.resource))); - if (dirtyWorkingCopies.length) { + if (dirtyWorkingCopies.size) { let message: string; if (distinctElements.length > 1) { message = nls.localize('dirtyMessageFilesDelete', "You are deleting files with unsaved changes. Do you want to continue?"); } else if (distinctElements[0].isDirectory) { - if (dirtyWorkingCopies.length === 1) { + if (dirtyWorkingCopies.size === 1) { message = nls.localize('dirtyMessageFolderOneDelete', "You are deleting a folder {0} with unsaved changes in 1 file. Do you want to continue?", distinctElements[0].name); } else { - message = nls.localize('dirtyMessageFolderDelete', "You are deleting a folder {0} with unsaved changes in {1} files. Do you want to continue?", distinctElements[0].name, dirtyWorkingCopies.length); + message = nls.localize('dirtyMessageFolderDelete', "You are deleting a folder {0} with unsaved changes in {1} files. Do you want to continue?", distinctElements[0].name, dirtyWorkingCopies.size); } } else { message = nls.localize('dirtyMessageFileDelete', "You are deleting {0} with unsaved changes. Do you want to continue?", distinctElements[0].name); @@ -204,7 +208,6 @@ async function deleteFiles(workingCopyService: IWorkingCopyService, workingCopyF confirmed = false; } else { skipConfirm = true; - await Promise.all(dirtyWorkingCopies.map(dirty => dirty.revert())); } } @@ -296,7 +299,7 @@ async function deleteFiles(workingCopyService: IWorkingCopyService, workingCopyF skipConfirm = true; - return deleteFiles(workingCopyService, workingCopyFileService, dialogService, configurationService, elements, useTrash, skipConfirm); + return deleteFiles(workingCopyFileService, dialogService, configurationService, elements, useTrash, skipConfirm); } } } @@ -989,7 +992,7 @@ export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(IWorkingCopyService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); + await deleteFiles(accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); } }; @@ -998,7 +1001,7 @@ export const deleteFileHandler = async (accessor: ServicesAccessor) => { const stats = explorerService.getContext(true).filter(s => !s.isRoot); if (stats.length) { - await deleteFiles(accessor.get(IWorkingCopyService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); + await deleteFiles(accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); } }; diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index b3f30602de..7f6736ae43 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -66,17 +66,6 @@ border-radius: 0; /* goes better when ellipsis shows up on narrow sidebar */ } -.explorer-viewlet .explorer-empty-view { - padding: 0 20px 0 20px; -} - -.explorer-viewlet .explorer-empty-view .monaco-button { - max-width: 260px; - margin-left: auto; - margin-right: auto; - display: block; -} - .explorer-viewlet .explorer-item.nonexistent-root { opacity: 0.5; } diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index d70a244e3a..fac178b5a7 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -4,13 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as errors from 'vs/base/common/errors'; -import * as DOM from 'vs/base/browser/dom'; -import { Button } from 'vs/base/browser/ui/button/button'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { OpenFolderAction, AddRootFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; -import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -20,10 +15,7 @@ import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/vie import { ResourcesDropHandler, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { listDropBackground } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { Schemas } from 'vs/base/common/network'; -import { isWeb } from 'vs/base/common/platform'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -33,9 +25,6 @@ export class EmptyView extends ViewPane { static readonly ID: string = 'workbench.explorer.emptyView'; static readonly NAME = nls.localize('noWorkspace', "No Folder Opened"); - private button!: Button; - private messageElement!: HTMLElement; - constructor( options: IViewletViewOptions, @IThemeService themeService: IThemeService, @@ -45,55 +34,31 @@ export class EmptyView extends ViewPane { @IContextMenuService contextMenuService: IContextMenuService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IConfigurationService configurationService: IConfigurationService, - @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService, @ILabelService private labelService: ILabelService, @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService ) { super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('explorerSection', "Explorer Section: No Folder Opened") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService); - this._register(this.contextService.onDidChangeWorkbenchState(() => this.setLabels())); - this._register(this.labelService.onDidChangeFormatters(() => this.setLabels())); + + this._register(this.contextService.onDidChangeWorkbenchState(() => this.refreshTitle())); + this._register(this.labelService.onDidChangeFormatters(() => this.refreshTitle())); + } + + shouldShowWelcome(): boolean { + return true; } protected renderBody(container: HTMLElement): void { super.renderBody(container); - DOM.addClass(container, 'explorer-empty-view'); - container.tabIndex = 0; - - const messageContainer = document.createElement('div'); - DOM.addClass(messageContainer, 'section'); - container.appendChild(messageContainer); - - this.messageElement = document.createElement('p'); - messageContainer.appendChild(this.messageElement); - - this.button = new Button(messageContainer); - attachButtonStyler(this.button, this.themeService); - - this._register(this.button.onDidClick(() => { - if (!this.actionRunner) { - return; - } - const action = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE - ? this.instantiationService.createInstance(AddRootFolderAction, AddRootFolderAction.ID, AddRootFolderAction.LABEL) - : this.instantiationService.createInstance(OpenFolderAction, OpenFolderAction.ID, OpenFolderAction.LABEL); - this.actionRunner.run(action).then(() => { - action.dispose(); - }, err => { - action.dispose(); - errors.onUnexpectedError(err); - }); - })); - this._register(new DragAndDropObserver(container, { onDrop: e => { const color = this.themeService.getTheme().getColor(SIDE_BAR_BACKGROUND); container.style.backgroundColor = color ? color.toString() : ''; const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true }); - dropHandler.handleDrop(e, () => undefined, targetGroup => undefined); + dropHandler.handleDrop(e, () => undefined, () => undefined); }, - onDragEnter: (e) => { + onDragEnter: () => { const color = this.themeService.getTheme().getColor(listDropBackground); container.style.backgroundColor = color ? color.toString() : ''; }, @@ -112,26 +77,13 @@ export class EmptyView extends ViewPane { } })); - this.setLabels(); + this.refreshTitle(); } - private setLabels(): void { + private refreshTitle(): void { if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - this.messageElement.textContent = nls.localize('noWorkspaceHelp', "You have not yet added a folder to the workspace."); - if (this.button) { - this.button.label = nls.localize('addFolder', "Add Folder"); - } this.updateTitle(EmptyView.NAME); } else { - if (this.environmentService.configuration.remoteAuthority && !isWeb) { - const hostLabel = this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.configuration.remoteAuthority); - this.messageElement.textContent = hostLabel ? nls.localize('remoteNoFolderHelp', "Connected to {0}", hostLabel) : nls.localize('connecting', "Connecting..."); - } else { - this.messageElement.textContent = nls.localize('noFolderHelp', "You have not yet opened a folder."); - } - if (this.button) { - this.button.label = nls.localize('openFolder', "Open Folder"); - } this.updateTitle(this.title); } } @@ -139,9 +91,4 @@ export class EmptyView extends ViewPane { layoutBody(_size: number): void { // no-op } - - focus(): void { - this.button.element.focus(); - } - } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 99a0fa1cdd..865255b170 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -55,7 +55,6 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { isNumber } from 'vs/base/common/types'; import { domEvent } from 'vs/base/browser/event'; import { IEditableData } from 'vs/workbench/common/views'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -643,8 +642,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { @IInstantiationService private instantiationService: IInstantiationService, @IWorkingCopyFileService private workingCopyFileService: IWorkingCopyFileService, @IHostService private hostService: IHostService, - @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, - @IWorkingCopyService private workingCopyService: IWorkingCopyService + @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService ) { this.toDispose = []; @@ -945,15 +943,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const sourceFile = resource; const targetFile = joinPath(target.resource, basename(sourceFile)); - // if the target exists and is dirty, make sure to revert it. otherwise the dirty contents - // of the target file would replace the contents of the added file. since we already - // confirmed the overwrite before, this is OK. - if (this.workingCopyService.isDirty(targetFile)) { - await Promise.all(this.workingCopyService.getWorkingCopies(targetFile).map(workingCopy => workingCopy.revert({ soft: true }))); - } - - const copyTarget = joinPath(target.resource, basename(sourceFile)); - const stat = await this.workingCopyFileService.copy(sourceFile, copyTarget, true); + const stat = await this.workingCopyFileService.copy(sourceFile, targetFile, true); // if we only add one file, just open it directly if (resources.length === 1 && !stat.isDirectory) { this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index e0bf4a2961..d59834aea7 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { EncodingMode, IFileEditorInput, Verbosity, TextResourceEditorInput } from 'vs/workbench/common/editor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; -import { ITextFileService, ModelState, LoadReason, TextFileOperationError, TextFileOperationResult, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, TextFileEditorModelState, TextFileLoadReason, TextFileOperationError, TextFileOperationResult, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IReference, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -56,6 +56,8 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi ) { super(resource, editorService, editorGroupService, textFileService, labelService, fileService, filesConfigurationService); + this.model = this.textFileService.files.get(resource); + if (preferredEncoding) { this.setPreferredEncoding(preferredEncoding); } @@ -63,6 +65,11 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi if (preferredMode) { this.setPreferredMode(preferredMode); } + + // If a file model already exists, make sure to wire it in + if (this.model) { + this.registerModelListeners(this.model); + } } protected registerListeners(): void { @@ -98,10 +105,10 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi this.modelListeners.add(model.onDidSaveError(() => this._onDidChangeDirty.fire())); // remove model association once it gets disposed - Event.once(model.onDispose)(() => { + this.modelListeners.add(Event.once(model.onDispose)(() => { this.modelListeners.clear(); this.model = undefined; - }); + })); } getEncoding(): string | undefined { @@ -167,7 +174,7 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi } private decorateLabel(label: string): string { - const orphaned = this.model?.hasState(ModelState.ORPHAN); + const orphaned = this.model?.hasState(TextFileEditorModelState.ORPHAN); const readonly = this.isReadonly(); if (orphaned && readonly) { @@ -198,7 +205,7 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi } isSaving(): boolean { - if (this.model?.hasState(ModelState.SAVED) || this.model?.hasState(ModelState.CONFLICT) || this.model?.hasState(ModelState.ERROR)) { + if (this.model?.hasState(TextFileEditorModelState.SAVED) || this.model?.hasState(TextFileEditorModelState.CONFLICT) || this.model?.hasState(TextFileEditorModelState.ERROR)) { return false; // require the model to be dirty and not in conflict or error state } @@ -234,7 +241,7 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi encoding: this.preferredEncoding, reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model allowBinary: this.forceOpenAs === ForceOpenAs.Text, - reason: LoadReason.EDITOR + reason: TextFileLoadReason.EDITOR }); // This is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary diff --git a/src/vs/workbench/contrib/output/browser/logViewer.ts b/src/vs/workbench/contrib/output/browser/logViewer.ts index 57d0794cbb..d62d2f3465 100644 --- a/src/vs/workbench/contrib/output/browser/logViewer.ts +++ b/src/vs/workbench/contrib/output/browser/logViewer.ts @@ -28,10 +28,8 @@ export class LogViewerInput extends ResourceEditorInput { static readonly ID = 'workbench.editorinputs.output'; - readonly resource = this.outputChannelDescriptor.file; - constructor( - private readonly outputChannelDescriptor: IFileOutputChannelDescriptor, + outputChannelDescriptor: IFileOutputChannelDescriptor, @ITextModelService textModelResolverService: ITextModelService, @ITextFileService textFileService: ITextFileService, @IEditorService editorService: IEditorService, diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css index 189307f96e..da23fbabe0 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css @@ -13,16 +13,16 @@ } /* Deal with overflow */ -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-widget .setting-list-value, -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-widget .setting-list-sibling { - white-space: nowrap; +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-value, +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-sibling { + white-space: pre; overflow: hidden; text-overflow: ellipsis; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-widget .setting-list-value { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-value { max-width: 90%; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-widget .setting-list-sibling { +.settings-editor > .settings-body > .settings-tree-container .setting-item.setting-item-list .setting-list-sibling { max-width: 10%; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index caa4c437a3..c613e2cd0c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -1640,12 +1640,13 @@ class StopSyncingSettingAction extends Action { } async run(): Promise { - const currentValue = this.configService.getValue('sync.ignoredSettings'); + let currentValue = [...this.configService.getValue('sync.ignoredSettings')]; if (this.checked) { - this.configService.updateValue('sync.ignoredSettings', currentValue.filter(v => v !== this.setting.key)); + currentValue = currentValue.filter(v => v !== this.setting.key); } else { - this.configService.updateValue('sync.ignoredSettings', [...currentValue, this.setting.key]); + currentValue.push(this.setting.key); } + this.configService.updateValue('sync.ignoredSettings', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER); return Promise.resolve(undefined); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 566133afd9..bd2425476c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -443,12 +443,12 @@ export class ListSettingWidget extends Disposable { const onSubmit = (edited: boolean) => { this.model.setEditKey('none'); - const value = valueInput.value.trim(); + const value = valueInput.value; if (edited && !isUndefinedOrNull(value)) { this._onDidChangeList.fire({ originalValue: item.value, value: value, - sibling: siblingInput && siblingInput.value.trim(), + sibling: siblingInput && siblingInput.value, targetIndex: idx }); } diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts index b9489a5bb4..2b0c2d1ba3 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts @@ -146,13 +146,16 @@ interface ResourceTemplate { disposables: IDisposable; } -class MultipleSelectionActionRunner extends ActionRunner { +class RepositoryPaneActionRunner extends ActionRunner { - constructor(private getSelectedResources: () => (ISCMResource | IResourceNode)[]) { + constructor( + private getSelectedResources: () => (ISCMResource | IResourceNode)[], + private focus: () => void + ) { super(); } - runAction(action: IAction, context: ISCMResource | IResourceNode): Promise { + async runAction(action: IAction, context: ISCMResource | IResourceNode): Promise { if (!(action instanceof MenuItemAction)) { return super.runAction(action, context); } @@ -161,7 +164,8 @@ class MultipleSelectionActionRunner extends ActionRunner { const contextIsSelected = selection.some(s => s === context); const actualContext = contextIsSelected ? selection : [context]; const args = flatten(actualContext.map(e => ResourceTree.isResourceNode(e) ? ResourceTree.collect(e) : [e])); - return action.run(...args); + await action.run(...args); + this.focus(); } } @@ -175,6 +179,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer (ISCMResource | IResourceNode)[], + private focus: () => void, private themeService: IThemeService, private menus: SCMMenus ) { } @@ -186,7 +191,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer this.viewModel, this.listLabels, actionViewItemProvider, () => this.getSelectedResources(), this.themeService, this.menus) + new ResourceRenderer(() => this.viewModel, this.listLabels, actionViewItemProvider, () => this.getSelectedResources(), () => this.tree.domFocus(), this.themeService, this.menus) ]; const filter = new SCMTreeFilter(); @@ -1024,7 +1030,7 @@ export class RepositoryPane extends ViewPane { getAnchor: () => e.anchor, getActions: () => actions, getActionsContext: () => element, - actionRunner: new MultipleSelectionActionRunner(() => this.getSelectedResources()) + actionRunner: new RepositoryPaneActionRunner(() => this.getSelectedResources(), () => this.tree.domFocus()) }); } diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index 039a851a36..9c8a786fe8 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -104,14 +104,7 @@ export class ReplaceService implements IReplaceService { const edits: WorkspaceTextEdit[] = this.createEdits(arg, resource); await this.bulkEditorService.apply({ edits }, { progress }); - return Promise.all(edits.map(e => { - const model = this.textFileService.files.get(e.resource); - if (model) { - return model.save(); - } - - return Promise.resolve(undefined); - })); + return Promise.all(edits.map(e => this.textFileService.files.get(e.resource)?.save())); } async openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 081a650a1d..2232c782d5 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -9,7 +9,7 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { ITreeContextMenuEvent, ITreeElement } from 'vs/base/browser/ui/tree/tree'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, ActionRunner } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; import * as errors from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; @@ -213,7 +213,7 @@ export class SearchView extends ViewPane { this.viewletState = this.memento.getMemento(StorageScope.WORKSPACE); this._register(this.fileService.onDidFilesChange(e => this.onFilesChanged(e))); - this._register(this.textFileService.untitled.onDidDisposeModel(e => this.onUntitledDidDispose(e))); + this._register(this.textFileService.untitled.onDidDispose(model => this.onUntitledDidDispose(model.resource))); this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState())); this._register(this.searchHistoryService.onDidClearHistory(() => this.clearHistory())); @@ -1584,6 +1584,7 @@ export class SearchView extends ViewPane { const openFolderLink = dom.append(textEl, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openFolder', "Open Folder"))); + const actionRunner = new ActionRunner(); this.messageDisposables.push(dom.addDisposableListener(openFolderLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); @@ -1591,7 +1592,7 @@ export class SearchView extends ViewPane { this.instantiationService.createInstance(OpenFileFolderAction, OpenFileFolderAction.ID, OpenFileFolderAction.LABEL) : this.instantiationService.createInstance(OpenFolderAction, OpenFolderAction.ID, OpenFolderAction.LABEL); - this.actionRunner!.run(action).then(() => { + actionRunner.run(action).then(() => { action.dispose(); }, err => { action.dispose(); diff --git a/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts b/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts index 41060b14a0..28f93385f7 100644 --- a/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/languageSurveys.contribution.ts @@ -53,8 +53,7 @@ class LanguageSurvey extends Disposable { // Process model-save event every 250ms to reduce load const onModelsSavedWorker = this._register(new RunOnceWorker(models => { models.forEach(m => { - const model = modelService.getModel(m.resource); - if (model && model.getModeId() === data.languageId && date !== storageService.get(EDITED_LANGUAGE_DATE_KEY, StorageScope.GLOBAL)) { + if (m.getMode() === data.languageId && date !== storageService.get(EDITED_LANGUAGE_DATE_KEY, StorageScope.GLOBAL)) { const editedCount = storageService.getNumber(EDITED_LANGUAGE_COUNT_KEY, StorageScope.GLOBAL, 0) + 1; storageService.store(EDITED_LANGUAGE_COUNT_KEY, editedCount, StorageScope.GLOBAL); storageService.store(EDITED_LANGUAGE_DATE_KEY, date, StorageScope.GLOBAL); diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index e5fe9ca9cc..52e5c46138 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -19,7 +19,7 @@ import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; import { configurationTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { ITextFileService, ITextFileModelSaveEvent, ITextFileModelLoadEvent } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, ITextFileSaveEvent, ITextFileLoadEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { extname, basename, isEqual, isEqualOrParent, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; @@ -58,7 +58,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IConfigurationService configurationService: IConfigurationService, @IViewletService viewletService: IViewletService, - @ITextFileService textFileService: ITextFileService, + @ITextFileService textFileService: ITextFileService ) { super(); @@ -131,7 +131,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr this._register(lifecycleService.onShutdown(() => this.dispose())); } - private onTextFileModelLoaded(e: ITextFileModelLoadEvent): void { + private onTextFileModelLoaded(e: ITextFileLoadEvent): void { const settingsType = this.getTypeIfSettings(e.model.resource); if (settingsType) { type SettingsReadClassification = { @@ -146,7 +146,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr } } - private onTextFileModelSaved(e: ITextFileModelSaveEvent): void { + private onTextFileModelSaved(e: ITextFileSaveEvent): void { const settingsType = this.getTypeIfSettings(e.model.resource); if (settingsType) { type SettingsWrittenClassification = { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index cc35698cb7..5c4e87ba51 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -690,6 +690,8 @@ export class RunActiveFileInTerminalAction extends Action { if (!instance) { return Promise.resolve(undefined); } + await instance.processReady; + const editor = this.codeEditorService.getActiveCodeEditor(); if (!editor || !editor.hasModel()) { return Promise.resolve(undefined); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 26c947f928..ad0388a18f 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -21,7 +21,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { localize } from 'vs/nls'; -import { IMenuItem, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey, ContextKeyRegexExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -31,7 +31,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { CONTEXT_SYNC_STATE, getSyncSourceFromRemoteContentResource, getUserDataSyncStore, ISyncConfiguration, IUserDataAuthTokenService, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncSource, SyncStatus, toRemoteContentResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, ResourceKey, getSyncSourceFromPreviewResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { CONTEXT_SYNC_STATE, getSyncSourceFromRemoteContentResource, getUserDataSyncStore, ISyncConfiguration, IUserDataAuthTokenService, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncSource, SyncStatus, toRemoteContentResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, ResourceKey, getSyncSourceFromPreviewResource, CONTEXT_SYNC_ENABLEMENT } from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -52,7 +52,6 @@ const enum AuthStatus { SignedOut = 'SignedOut', Unavailable = 'Unavailable' } -const CONTEXT_SYNC_ENABLEMENT = new RawContextKey('syncEnabled', false); const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey('authTokenStatus', AuthStatus.Initializing); const CONTEXT_CONFLICTS_SOURCES = new RawContextKey('conflictsSources', ''); @@ -547,7 +546,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } else { await this.userDataSyncService.resetLocal(); } - await this.signOut(); this.disableSync(); } } @@ -574,13 +572,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private async signOut(): Promise { - if (this.activeAccount) { - await this.authenticationService.logout(this.userDataSyncStore!.authenticationProviderId, this.activeAccount.id); - await this.setActiveAccount(undefined); - } - } - private getConflictsEditorInput(source: SyncSource): IEditorInput | undefined { const previewResource = source === SyncSource.Settings ? this.workbenchEnvironmentService.settingsSyncPreviewResource : source === SyncSource.Keybindings ? this.workbenchEnvironmentService.keybindingsSyncPreviewResource @@ -652,6 +643,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }, when: turnOnSyncWhenContext, }); + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '5_sync', + command: { + id: turnOnSyncCommandId, + title: localize('global activity turn on sync', "Turn on Sync...") + }, + when: turnOnSyncWhenContext, + }); const signInCommandId = 'workbench.userData.actions.signin'; const signInWhenContext = ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT, CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedOut)); @@ -697,6 +696,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }, when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT), }); + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '5_sync', + command: { + id: stopSyncCommandId, + title: localize('global activity stop sync', "Turn off Sync") + }, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT), + }); const resolveSettingsConflictsCommandId = 'workbench.userData.actions.resolveSettingsConflicts'; const resolveSettingsConflictsWhenContext = ContextKeyRegexExpr.create(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*settings.*/i); @@ -736,17 +743,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo when: resolveKeybindingsConflictsWhenContext, }); - const signOutMenuItem: IMenuItem = { - group: '5_sync', - command: { - id: 'workbench.userData.actions.signout', - title: localize('sign out', "Sync: Sign out") - }, - when: ContextKeyExpr.and(CONTEXT_AUTH_TOKEN_STATE.isEqualTo(AuthStatus.SignedIn)), - }; - CommandsRegistry.registerCommand(signOutMenuItem.command.id, () => this.signOut()); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, signOutMenuItem); - const configureSyncCommandId = 'workbench.userData.actions.configureSync'; CommandsRegistry.registerCommand(configureSyncCommandId, () => this.configureSyncOptions()); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { @@ -767,15 +763,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)), }); - const resetLocalCommandId = 'workbench.userData.actions.resetLocal'; - CommandsRegistry.registerCommand(resetLocalCommandId, () => this.userDataSyncService.resetLocal()); - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: resetLocalCommandId, - title: localize('reset local', "Developer: Reset Local (Sync)") - }, - when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)), - }); } } diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts index e043441bf5..0307159015 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts @@ -26,10 +26,12 @@ export class WebviewInput extends EditorInput { private readonly _onDisposeWebview = this._register(new Emitter()); readonly onDisposeWebview = this._onDisposeWebview.event; - readonly resource = URI.from({ - scheme: WebviewPanelResourceScheme, - path: `webview-panel/webview-${this.id}` - }); + get resource() { + return URI.from({ + scheme: WebviewPanelResourceScheme, + path: `webview-panel/webview-${this.id}` + }); + } constructor( public readonly id: string, diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts index f0be8dad96..0f1bc2b03b 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts @@ -53,10 +53,10 @@ export class WalkThroughInput extends EditorInput { private maxTopScroll = 0; private maxBottomScroll = 0; - readonly resource = this.options.resource; + get resource() { return this.options.resource; } constructor( - private options: WalkThroughInputOptions, + private readonly options: WalkThroughInputOptions, @ITextModelService private readonly textModelResolverService: ITextModelService ) { super(); diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index aa4e9cdc8b..e204abaac3 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -289,10 +289,9 @@ export class ElectronWindow extends Disposable { private updateDocumentEdited(isDirty = this.workingCopyService.hasDirty): void { if ((!this.isDocumentedEdited && isDirty) || (this.isDocumentedEdited && !isDirty)) { - const hasDirtyFiles = this.workingCopyService.hasDirty; - this.isDocumentedEdited = hasDirtyFiles; + this.isDocumentedEdited = isDirty; - this.electronService.setDocumentEdited(hasDirtyFiles); + this.electronService.setDocumentEdited(isDirty); } } diff --git a/src/vs/workbench/services/clipboard/browser/clipboardService.ts b/src/vs/workbench/services/clipboard/browser/clipboardService.ts index 9b2fd14606..9f3fb2e7ff 100644 --- a/src/vs/workbench/services/clipboard/browser/clipboardService.ts +++ b/src/vs/workbench/services/clipboard/browser/clipboardService.ts @@ -5,73 +5,6 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { URI } from 'vs/base/common/uri'; - -export class BrowserClipboardService implements IClipboardService { - - _serviceBrand: undefined; - - private _internalResourcesClipboard: URI[] | undefined; - - async writeText(text: string, type?: string): Promise { - if (type) { - return; // TODO@sbatten - } - - if (navigator.clipboard && navigator.clipboard.writeText) { - return navigator.clipboard.writeText(text); - } else { - const activeElement = document.activeElement; - const newTextarea = document.createElement('textarea'); - newTextarea.className = 'clipboard-copy'; - newTextarea.style.visibility = 'false'; - newTextarea.style.height = '1px'; - newTextarea.style.width = '1px'; - newTextarea.setAttribute('aria-hidden', 'true'); - newTextarea.style.position = 'absolute'; - newTextarea.style.top = '-1000'; - newTextarea.style.left = '-1000'; - document.body.appendChild(newTextarea); - newTextarea.value = text; - newTextarea.focus(); - newTextarea.select(); - document.execCommand('copy'); - activeElement.focus(); - document.body.removeChild(newTextarea); - } - return; - } - - async readText(type?: string): Promise { - if (type) { - return ''; // TODO@sbatten - } - - return navigator.clipboard.readText(); - } - - readTextSync(): string | undefined { - return undefined; - } - - readFindText(): string { - // @ts-ignore - return undefined; - } - - writeFindText(text: string): void { } - - writeResources(resources: URI[]): void { - this._internalResourcesClipboard = resources; - } - - readResources(): URI[] { - return this._internalResourcesClipboard || []; - } - - hasResources(): boolean { - return this._internalResourcesClipboard !== undefined && this._internalResourcesClipboard.length > 0; - } -} +import { BrowserClipboardService } from 'vs/platform/clipboard/browser/clipboardService'; registerSingleton(IClipboardService, BrowserClipboardService, true); diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 133ada177b..f7e7bc111e 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -191,22 +191,6 @@ export class ProgressService extends Disposable implements IProgressService { } }; - const createWindowProgress = () => { - this.withWindowProgress({ - location: ProgressLocation.Window, - title: options.title - }, progress => { - if (progressStateModel.step) { - progress.report(progressStateModel.step); - } - - const disposable = progressStateModel.onDidReport(step => progress.report(step)); - Event.once(progressStateModel.onDispose)(() => disposable.dispose()); - - return progressStateModel.promise; - }); - }; - const createNotification = (message: string, increment?: number): INotificationHandle => { const notificationDisposables = new DisposableStore(); @@ -254,18 +238,8 @@ export class ProgressService extends Disposable implements IProgressService { updateProgress(handle, increment); - Event.once(handle.onDidClose)(() => { - - // Switch to window based progress once the notification - // is being closed even though still running and not - // cancelled. - if (!progressStateModel.done) { - createWindowProgress(); - } - - // Clear disposables - notificationDisposables.dispose(); - }); + // Clear upon dispose + Event.once(handle.onDidClose)(() => notificationDisposables.dispose()); return handle; }; diff --git a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts index faa788de02..0cb36cb8f6 100644 --- a/src/vs/workbench/services/textfile/browser/browserTextFileService.ts +++ b/src/vs/workbench/services/textfile/browser/browserTextFileService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; -import { ITextFileService, IResourceEncodings, IResourceEncoding, ModelState } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, IResourceEncodings, IResourceEncoding, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; @@ -24,7 +24,7 @@ export class BrowserTextFileService extends AbstractTextFileService { } protected onBeforeShutdown(reason: ShutdownReason): boolean { - if (this.files.getAll().some(model => model.hasState(ModelState.PENDING_SAVE))) { + if (this.files.models.some(model => model.hasState(TextFileEditorModelState.PENDING_SAVE))) { console.warn('Unload prevented: pending file saves'); return true; // files are pending to be saved: veto diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 9f29e61cf2..8ed72fbf57 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -204,11 +204,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex else { const model = this.files.get(resource); if (model) { - - // Save with options - await model.save(options); - - return !model.isDirty() ? resource : undefined; + return await model.save(options) ? resource : undefined; } } @@ -382,9 +378,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } // save model - await targetModel.save(options); - - return true; + return await targetModel.save(options); } private async confirmOverwrite(resource: URI): Promise { diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index ff06385046..d1008e44b1 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; -import { ITextFileService, ModelState, ITextFileEditorModel, ITextFileStreamContent, ILoadOptions, IResolvedTextFileEditorModel, ITextFileSaveOptions, LoadReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, TextFileEditorModelState, ITextFileEditorModel, ITextFileStreamContent, ITextFileLoadOptions, IResolvedTextFileEditorModel, ITextFileSaveOptions, TextFileLoadReason } from 'vs/workbench/services/textfile/common/textfiles'; import { EncodingMode, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; @@ -43,7 +43,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent = this._onDidChangeContent.event; - private readonly _onDidLoad = this._register(new Emitter()); + private readonly _onDidLoad = this._register(new Emitter()); readonly onDidLoad = this._onDidLoad.event; private readonly _onDidChangeDirty = this._register(new Emitter()); @@ -248,7 +248,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#region Load - async load(options?: ILoadOptions): Promise { + async load(options?: ITextFileLoadOptions): Promise { this.logService.trace('[text file model] load() - enter', this.resource.toString()); // It is very important to not reload the model when the model is dirty. @@ -281,7 +281,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.loadFromFile(options); } - private async loadFromBackup(backup: IResolvedBackup, options?: ILoadOptions): Promise { + private async loadFromBackup(backup: IResolvedBackup, options?: ITextFileLoadOptions): Promise { // Load with backup this.loadFromContent({ @@ -303,7 +303,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this; } - private async loadFromFile(options?: ILoadOptions): Promise { + private async loadFromFile(options?: ITextFileLoadOptions): Promise { const forceReadFromDisk = options?.forceReadFromDisk; const allowBinary = this.isResolved() /* always allow if we resolved previously */ || options?.allowBinary; @@ -358,7 +358,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - private loadFromContent(content: ITextFileStreamContent, options?: ILoadOptions, fromBackup?: boolean): TextFileEditorModel { + private loadFromContent(content: ITextFileStreamContent, options?: ITextFileLoadOptions, fromBackup?: boolean): TextFileEditorModel { this.logService.trace('[text file model] load() - resolved content', this.resource.toString()); // Update our resolved disk stat model @@ -396,7 +396,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } // Emit as event - this._onDidLoad.fire(options?.reason ?? LoadReason.OTHER); + this._onDidLoad.fire(options?.reason ?? TextFileLoadReason.OTHER); return this; } @@ -544,7 +544,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } if ( - (this.hasState(ModelState.CONFLICT) || this.hasState(ModelState.ERROR)) && + (this.hasState(TextFileEditorModelState.CONFLICT) || this.hasState(TextFileEditorModelState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) ) { this.logService.trace('[text file model] save() - ignoring auto save request for model that is in conflict or error', this.resource.toString()); @@ -794,19 +794,19 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#endregion - hasState(state: ModelState): boolean { + hasState(state: TextFileEditorModelState): boolean { switch (state) { - case ModelState.CONFLICT: + case TextFileEditorModelState.CONFLICT: return this.inConflictMode; - case ModelState.DIRTY: + case TextFileEditorModelState.DIRTY: return this.dirty; - case ModelState.ERROR: + case TextFileEditorModelState.ERROR: return this.inErrorMode; - case ModelState.ORPHAN: + case TextFileEditorModelState.ORPHAN: return this.inOrphanMode; - case ModelState.PENDING_SAVE: + case TextFileEditorModelState.PENDING_SAVE: return this.saveSequentializer.hasPending(); - case ModelState.SAVED: + case TextFileEditorModelState.SAVED: return !this.dirty; } } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 4794fd7d9c..3abec099c3 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -9,7 +9,7 @@ import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ITextFileEditorModel, ITextFileEditorModelManager, IModelLoadOrCreateOptions, ITextFileModelLoadEvent, ITextFileModelSaveEvent, ITextFileSaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileEditorModel, ITextFileEditorModelManager, ITextFileEditorModelLoadOrCreateOptions, ITextFileLoadEvent, ITextFileSaveEvent, ITextFileSaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; @@ -29,27 +29,34 @@ import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; export class TextFileEditorModelManager extends Disposable implements ITextFileEditorModelManager { - private readonly _onDidCreate = this._register(new Emitter()); + private readonly _onDidCreate = this._register(new Emitter()); readonly onDidCreate = this._onDidCreate.event; - private readonly _onDidLoad = this._register(new Emitter()); + private readonly _onDidLoad = this._register(new Emitter()); readonly onDidLoad = this._onDidLoad.event; - private readonly _onDidChangeDirty = this._register(new Emitter()); + private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; - private readonly _onDidSaveError = this._register(new Emitter()); + private readonly _onDidSaveError = this._register(new Emitter()); readonly onDidSaveError = this._onDidSaveError.event; - private readonly _onDidSave = this._register(new Emitter()); + private readonly _onDidSave = this._register(new Emitter()); readonly onDidSave = this._onDidSave.event; - private readonly _onDidRevert = this._register(new Emitter()); + private readonly _onDidRevert = this._register(new Emitter()); readonly onDidRevert = this._onDidRevert.event; - private readonly _onDidChangeEncoding = this._register(new Emitter()); + private readonly _onDidChangeEncoding = this._register(new Emitter()); readonly onDidChangeEncoding = this._onDidChangeEncoding.event; + private readonly mapResourceToModel = new ResourceMap(); + private readonly mapResourceToModelListeners = new ResourceMap(); + private readonly mapResourceToDisposeListener = new ResourceMap(); + private readonly mapResourceToPendingModelLoaders = new ResourceMap>(); + + private readonly modelLoadQueue = this._register(new ResourceQueue()); + saveErrorHandler = (() => { const notificationService = this.notificationService; @@ -60,12 +67,9 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE }; })(); - private readonly mapResourceToModel = new ResourceMap(); - private readonly mapResourceToModelListeners = new ResourceMap(); - private readonly mapResourceToDisposeListener = new ResourceMap(); - private readonly mapResourceToPendingModelLoaders = new ResourceMap>(); - - private readonly modelLoadQueue = this._register(new ResourceQueue()); + get models(): TextFileEditorModel[] { + return this.mapResourceToModel.values(); + } constructor( @ILifecycleService private readonly lifecycleService: ILifecycleService, @@ -99,13 +103,15 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // // Note: we also consider the added event because it could be that a file was added // and updated right after. - distinct(coalesce([...e.getUpdated(), ...e.getAdded()] - .map(({ resource }) => this.get(resource))) - .filter(model => model && !model.isDirty()), model => model.resource.toString()) - .forEach(model => this.queueModelLoad(model)); + distinct( + coalesce( + [...e.getUpdated(), ...e.getAdded()].map(({ resource }) => this.get(resource)) + ).filter(model => model && model.isResolved() && !model.isDirty()), + model => model.resource.toString() + ).forEach(model => this.queueModelLoad(model)); } - private queueModelLoad(model: ITextFileEditorModel): void { + private queueModelLoad(model: TextFileEditorModel): void { // Load model to update (use a queue to prevent accumulation of loads // when the load actually takes long. At most we only want the queue @@ -131,9 +137,9 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE if (source && (e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE)) { // find all models that related to either source or target (can be many if resource is a folder) - const sourceModels: ITextFileEditorModel[] = []; - const targetModels: ITextFileEditorModel[] = []; - for (const model of this.getAll()) { + const sourceModels: TextFileEditorModel[] = []; + const targetModels: TextFileEditorModel[] = []; + for (const model of this.models) { const resource = model.resource; if (isEqualOrParent(resource, e.target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)) { @@ -232,11 +238,11 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } - get(resource: URI): ITextFileEditorModel | undefined { + get(resource: URI): TextFileEditorModel | undefined { return this.mapResourceToModel.get(resource); } - async resolve(resource: URI, options?: IModelLoadOrCreateOptions): Promise { + async resolve(resource: URI, options?: ITextFileEditorModelLoadOrCreateOptions): Promise { // Return early if model is currently being loaded const pendingLoad = this.mapResourceToPendingModelLoaders.get(resource); @@ -244,7 +250,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE return pendingLoad; } - let modelPromise: Promise; + let modelPromise: Promise; let model = this.get(resource); let didCreateModel = false; @@ -275,20 +281,23 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE modelPromise = model.load(options); // Install model listeners - const listeners = new DisposableStore(); - listeners.add(model.onDidLoad(reason => this._onDidLoad.fire({ model: newModel, reason }))); - listeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(newModel))); - listeners.add(model.onDidSaveError(() => this._onDidSaveError.fire(newModel))); - listeners.add(model.onDidSave(reason => this._onDidSave.fire({ model: newModel, reason }))); - listeners.add(model.onDidRevert(() => this._onDidRevert.fire(newModel))); - listeners.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(newModel))); + const modelListeners = new DisposableStore(); + modelListeners.add(model.onDidLoad(reason => this._onDidLoad.fire({ model: newModel, reason }))); + modelListeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(newModel))); + modelListeners.add(model.onDidSaveError(() => this._onDidSaveError.fire(newModel))); + modelListeners.add(model.onDidSave(reason => this._onDidSave.fire({ model: newModel, reason }))); + modelListeners.add(model.onDidRevert(() => this._onDidRevert.fire(newModel))); + modelListeners.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(newModel))); - this.mapResourceToModelListeners.set(resource, listeners); + this.mapResourceToModelListeners.set(resource, modelListeners); } // Store pending loads to avoid race conditions this.mapResourceToPendingModelLoaders.set(resource, modelPromise); + // Make known to manager (if not already known) + this.add(resource, model); + // Signal as event if we created the model if (didCreateModel) { this._onDidCreate.fire(model); @@ -297,9 +306,6 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE try { const resolvedModel = await modelPromise; - // Make known to manager (if not already known) - this.add(resource, resolvedModel); - // Remove from pending loads this.mapResourceToPendingModelLoaders.delete(resource); @@ -308,8 +314,9 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE resolvedModel.setMode(options.mode); } - // Model can be dirty if a backup was restored, so we make sure to have this event delivered - if (resolvedModel.isDirty()) { + // Model can be dirty if a backup was restored, so we make sure to + // have this event delivered if we created the model here + if (didCreateModel && resolvedModel.isDirty()) { this._onDidChangeDirty.fire(resolvedModel); } @@ -328,18 +335,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } - getAll(filter?: (model: ITextFileEditorModel) => boolean): ITextFileEditorModel[] { - const res: ITextFileEditorModel[] = []; - this.mapResourceToModel.forEach(model => { - if (!filter || filter(model)) { - res.push(model); - } - }); - - return res; - } - - add(resource: URI, model: ITextFileEditorModel): void { + add(resource: URI, model: TextFileEditorModel): void { const knownModel = this.mapResourceToModel.get(resource); if (knownModel === model) { return; // already cached diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 8f0b069bfa..017f3d47ed 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -161,13 +161,18 @@ export const enum TextFileOperationResult { } export class TextFileOperationError extends FileOperationError { - constructor(message: string, public textFileOperationResult: TextFileOperationResult, public options?: IReadTextFileOptions & IWriteTextFileOptions) { - super(message, FileOperationResult.FILE_OTHER_ERROR); - } static isTextFileOperationError(obj: unknown): obj is TextFileOperationError { return obj instanceof Error && !isUndefinedOrNull((obj as TextFileOperationError).textFileOperationResult); } + + constructor( + message: string, + public textFileOperationResult: TextFileOperationResult, + public options?: IReadTextFileOptions & IWriteTextFileOptions + ) { + super(message, FileOperationResult.FILE_OTHER_ERROR); + } } export interface IResourceEncodings { @@ -193,7 +198,7 @@ export interface ISaveErrorHandler { /** * States the text file editor model can be in. */ -export const enum ModelState { +export const enum TextFileEditorModelState { /** * A model is saved. @@ -228,17 +233,7 @@ export const enum ModelState { ERROR } -export interface ITextFileOperationResult { - results: IResult[]; -} - -export interface IResult { - source: URI; - target?: URI; - error?: boolean; -} - -export const enum LoadReason { +export const enum TextFileLoadReason { EDITOR = 1, REFERENCE = 2, OTHER = 3 @@ -268,12 +263,12 @@ export interface ITextFileStreamContent extends IBaseTextFileContent { value: ITextBufferFactory; } -export interface IModelLoadOrCreateOptions { +export interface ITextFileEditorModelLoadOrCreateOptions { /** * Context why the model is being loaded or created. */ - reason?: LoadReason; + reason?: TextFileLoadReason; /** * The language mode to use for the model text content. @@ -303,14 +298,14 @@ export interface IModelLoadOrCreateOptions { allowBinary?: boolean; } -export interface ITextFileModelSaveEvent { +export interface ITextFileSaveEvent { model: ITextFileEditorModel; reason: SaveReason; } -export interface ITextFileModelLoadEvent { +export interface ITextFileLoadEvent { model: ITextFileEditorModel; - reason: LoadReason; + reason: TextFileLoadReason; } export interface ITextFileSaveParticipant { @@ -330,35 +325,69 @@ export interface ITextFileSaveParticipant { export interface ITextFileEditorModelManager { readonly onDidCreate: Event; - readonly onDidLoad: Event; + readonly onDidLoad: Event; readonly onDidChangeDirty: Event; - readonly onDidSaveError: Event; - readonly onDidSave: Event; - readonly onDidRevert: Event; readonly onDidChangeEncoding: Event; + readonly onDidSaveError: Event; + readonly onDidSave: Event; + readonly onDidRevert: Event; + + readonly models: ITextFileEditorModel[]; saveErrorHandler: ISaveErrorHandler; + /** + * Returns the text file editor model for the provided resource + * or undefined if none. + */ get(resource: URI): ITextFileEditorModel | undefined; - getAll(): ITextFileEditorModel[]; - resolve(resource: URI, options?: IModelLoadOrCreateOptions): Promise; + /** + * Allows to load a text file model from disk. + */ + resolve(resource: URI, options?: ITextFileEditorModelLoadOrCreateOptions): Promise; + /** + * Adds a participant for saving text file models. + */ addSaveParticipant(participant: ITextFileSaveParticipant): IDisposable; + runSaveParticipants(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise disposeModel(model: ITextFileEditorModel): void; } export interface ITextFileSaveOptions extends ISaveOptions { + + /** + * Makes the file writable if it is readonly. + */ overwriteReadonly?: boolean; + + /** + * Overwrite the encoding of the file on disk as configured. + */ overwriteEncoding?: boolean; + + /** + * Save the file with elevated privileges. + * + * Note: This may not be supported in all environments. + */ writeElevated?: boolean; + + /** + * Allows to write to a file even if it has been modified on disk. + */ ignoreModifiedSince?: boolean; + + /** + * If set, will bubble up the error to the caller instead of handling it. + */ ignoreErrorHandler?: boolean; } -export interface ILoadOptions { +export interface ITextFileLoadOptions { /** * Go to disk bypassing any cache of the model if any. @@ -373,39 +402,29 @@ export interface ILoadOptions { /** * Context why the model is being loaded. */ - reason?: LoadReason; + reason?: TextFileLoadReason; } export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport, IModeSupport, IWorkingCopy { readonly onDidChangeContent: Event; - readonly onDidLoad: Event; readonly onDidSaveError: Event; - readonly onDidSave: Event; - readonly onDidRevert: Event; - readonly onDidChangeEncoding: Event; readonly onDidChangeOrphaned: Event; - hasState(state: ModelState): boolean; + hasState(state: TextFileEditorModelState): boolean; updatePreferredEncoding(encoding: string | undefined): void; - updateTextEditorModel(newValue?: ITextBufferFactory, preferredMode?: string): void; - save(options?: ITextFileSaveOptions): Promise; - - load(options?: ILoadOptions): Promise; - revert(options?: IRevertOptions): Promise; - isDirty(): boolean; // {{SQL CARBON EDIT}} strict-null-check + load(options?: ITextFileLoadOptions): Promise; - setDirty(dirty: boolean): void; + isDirty(): boolean; // {{SQL CARBON EDIT}} strict-null-check getMode(): string | undefined; isResolved(): this is IResolvedTextFileEditorModel; - isDisposed(): boolean; } diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 95764b835f..5e572096dc 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EncodingMode } from 'vs/workbench/common/editor'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { ITextFileService, ModelState, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, TextFileEditorModelState, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; import { createFileInput, TestFileService, TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -100,7 +100,7 @@ suite('Files - TextFileEditorModel', () => { model.textEditorModel!.setValue('bar'); assert.ok(getLastModifiedTime(model) <= Date.now()); - assert.ok(model.hasState(ModelState.DIRTY)); + assert.ok(model.hasState(TextFileEditorModelState.DIRTY)); assert.equal(accessor.workingCopyService.dirtyCount, 1); assert.equal(accessor.workingCopyService.isDirty(model.resource), true); @@ -116,11 +116,11 @@ suite('Files - TextFileEditorModel', () => { }); const pendingSave = model.save(); - assert.ok(model.hasState(ModelState.PENDING_SAVE)); + assert.ok(model.hasState(TextFileEditorModelState.PENDING_SAVE)); await pendingSave; - assert.ok(model.hasState(ModelState.SAVED)); + assert.ok(model.hasState(TextFileEditorModelState.SAVED)); assert.ok(!model.isDirty()); assert.ok(savedEvent); assert.ok(workingCopyEvent); @@ -169,11 +169,11 @@ suite('Files - TextFileEditorModel', () => { accessor.fileService.writeShouldThrowError = new Error('failed to write'); try { const pendingSave = model.save(); - assert.ok(model.hasState(ModelState.PENDING_SAVE)); + assert.ok(model.hasState(TextFileEditorModelState.PENDING_SAVE)); await pendingSave; - assert.ok(model.hasState(ModelState.ERROR)); + assert.ok(model.hasState(TextFileEditorModelState.ERROR)); assert.ok(model.isDirty()); assert.ok(saveErrorEvent); @@ -199,11 +199,11 @@ suite('Files - TextFileEditorModel', () => { accessor.fileService.writeShouldThrowError = new FileOperationError('save conflict', FileOperationResult.FILE_MODIFIED_SINCE); try { const pendingSave = model.save(); - assert.ok(model.hasState(ModelState.PENDING_SAVE)); + assert.ok(model.hasState(TextFileEditorModelState.PENDING_SAVE)); await pendingSave; - assert.ok(model.hasState(ModelState.CONFLICT)); + assert.ok(model.hasState(TextFileEditorModelState.CONFLICT)); assert.ok(model.isDirty()); assert.ok(saveErrorEvent); @@ -273,7 +273,7 @@ suite('Files - TextFileEditorModel', () => { test('Load does not trigger save', async function () { const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8', undefined); - assert.ok(model.hasState(ModelState.SAVED)); + assert.ok(model.hasState(TextFileEditorModelState.SAVED)); model.onDidSave(e => assert.fail()); model.onDidChangeDirty(e => assert.fail()); @@ -290,7 +290,7 @@ suite('Files - TextFileEditorModel', () => { await model.load(); model.textEditorModel!.setValue('foo'); assert.ok(model.isDirty()); - assert.ok(model.hasState(ModelState.DIRTY)); + assert.ok(model.hasState(TextFileEditorModelState.DIRTY)); await model.load(); assert.ok(model.isDirty()); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts index d426d9af5d..3f5eaa3d0f 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModelManager.test.ts @@ -51,7 +51,7 @@ suite('Files - TextFileEditorModelManager', () => { assert.ok(!manager.get(fileUpper)); - let results = manager.getAll(); + let results = manager.models; assert.strictEqual(3, results.length); let result = manager.get(URI.file('/yes')); @@ -68,19 +68,19 @@ suite('Files - TextFileEditorModelManager', () => { manager.remove(URI.file('')); - results = manager.getAll(); + results = manager.models; assert.strictEqual(3, results.length); manager.remove(URI.file('/some/other.html')); - results = manager.getAll(); + results = manager.models; assert.strictEqual(2, results.length); manager.remove(fileUpper); - results = manager.getAll(); + results = manager.models; assert.strictEqual(2, results.length); manager.clear(); - results = manager.getAll(); + results = manager.models; assert.strictEqual(0, results.length); model1.dispose(); @@ -98,7 +98,10 @@ suite('Files - TextFileEditorModelManager', () => { events.push(model); }); - const model = await manager.resolve(resource, { encoding }); + const modelPromise = manager.resolve(resource, { encoding }); + assert.ok(manager.get(resource)); // model known even before resolved() + + const model = await modelPromise; assert.ok(model); assert.equal(model.getEncoding(), encoding); assert.equal(manager.get(resource), model); diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index 94ccbee962..f354730cfb 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -10,7 +10,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { IDisposable, toDisposable, IReference, ReferenceCollection, ImmortalReference } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; -import { ITextFileService, LoadReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, TextFileLoadReason } from 'vs/workbench/services/textfile/common/textfiles'; import * as network from 'vs/base/common/network'; import { ITextModelService, ITextModelContentProvider, ITextEditorModel, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; @@ -38,7 +38,7 @@ class ResourceModelCollection extends ReferenceCollection; + readonly onDidChangeDirty: Event; /** * Events for when untitled text editor encodings change. */ - readonly onDidChangeEncoding: Event; + readonly onDidChangeEncoding: Event; /** * Events for when untitled text editor labels change. */ - readonly onDidChangeLabel: Event; + readonly onDidChangeLabel: Event; /** * Events for when untitled text editors are disposed. */ - readonly onDidDisposeModel: Event; + readonly onDidDispose: Event; /** * Creates a new untitled editor model with the provided options. If the `untitledResource` @@ -117,16 +117,16 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe _serviceBrand: undefined; - private readonly _onDidChangeDirty = this._register(new Emitter()); + private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; - private readonly _onDidChangeEncoding = this._register(new Emitter()); + private readonly _onDidChangeEncoding = this._register(new Emitter()); readonly onDidChangeEncoding = this._onDidChangeEncoding.event; - private readonly _onDidDisposeModel = this._register(new Emitter()); - readonly onDidDisposeModel = this._onDidDisposeModel.event; + private readonly _onDidDispose = this._register(new Emitter()); + readonly onDidDispose = this._onDidDispose.event; - private readonly _onDidChangeLabel = this._register(new Emitter()); + private readonly _onDidChangeLabel = this._register(new Emitter()); readonly onDidChangeLabel = this._onDidChangeLabel.event; private readonly mapResourceToModel = new ResourceMap(); @@ -220,10 +220,10 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe private registerModel(model: UntitledTextEditorModel): void { const modelDisposables = new DisposableStore(); - modelDisposables.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(model.resource))); - modelDisposables.add(model.onDidChangeName(() => this._onDidChangeLabel.fire(model.resource))); - modelDisposables.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(model.resource))); - modelDisposables.add(model.onDispose(() => this._onDidDisposeModel.fire(model.resource))); + modelDisposables.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire(model))); + modelDisposables.add(model.onDidChangeName(() => this._onDidChangeLabel.fire(model))); + modelDisposables.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(model))); + modelDisposables.add(model.onDispose(() => this._onDidDispose.fire(model))); // Remove from cache on dispose Event.once(model.onDispose)(() => { diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index 4ce72561cd..d9997f668c 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -108,10 +108,10 @@ suite('Untitled text editors', () => { function awaitDidChangeDirty(service: IUntitledTextEditorService): Promise { return new Promise(c => { - const listener = service.onDidChangeDirty(async resource => { + const listener = service.onDidChangeDirty(async model => { listener.dispose(); - c(resource); + c(model.resource); }); }); } @@ -328,9 +328,9 @@ suite('Untitled text editors', () => { let counter = 0; - service.onDidChangeEncoding(r => { + service.onDidChangeEncoding(model => { counter++; - assert.equal(r.toString(), input.resource.toString()); + assert.equal(model.resource.toString(), input.resource.toString()); }); // encoding @@ -347,9 +347,9 @@ suite('Untitled text editors', () => { let counter = 0; - service.onDidChangeLabel(r => { + service.onDidChangeLabel(model => { counter++; - assert.equal(r.toString(), input.resource.toString()); + assert.equal(model.resource.toString(), input.resource.toString()); }); // label @@ -366,9 +366,9 @@ suite('Untitled text editors', () => { let counter = 0; - service.onDidDisposeModel(r => { + service.onDidDispose(model => { counter++; - assert.equal(r.toString(), input.resource.toString()); + assert.equal(model.resource.toString(), input.resource.toString()); }); const model = await input.resolve(); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts index 181293caca..de2b10eaaf 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts @@ -118,6 +118,18 @@ export interface IWorkingCopyFileService { delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise; //#endregion + + + //#region Path related + + /** + * Will return all working copies that are dirty matching the provided resource. + * If the resource is a folder and the scheme supports file operations, a working + * copy that is dirty and is a child of that folder will also be returned. + */ + getDirty(resource: URI): IWorkingCopy[]; + + //#endregion } export class WorkingCopyFileService extends Disposable implements IWorkingCopyFileService { @@ -167,7 +179,7 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi // handle dirty working copies depending on the operation: // - move: revert both source and target (if any) // - copy: revert target (if any) - const dirtyWorkingCopies = (move ? [...this.getDirtyWorkingCopies(source), ...this.getDirtyWorkingCopies(target)] : this.getDirtyWorkingCopies(target)); + const dirtyWorkingCopies = (move ? [...this.getDirty(source), ...this.getDirty(target)] : this.getDirty(target)); await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true }))); // now we can rename the source to target via file operation @@ -202,7 +214,7 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi // Check for any existing dirty working copies for the resource // and do a soft revert before deleting to be able to close // any opened editor with these working copies - const dirtyWorkingCopies = this.getDirtyWorkingCopies(resource); + const dirtyWorkingCopies = this.getDirty(resource); await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true }))); // Now actually delete from disk @@ -220,7 +232,10 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None); } - private getDirtyWorkingCopies(resource: URI): IWorkingCopy[] { + + //#region Path related + + getDirty(resource: URI): IWorkingCopy[] { return this.workingCopyService.dirtyWorkingCopies.filter(dirty => { if (this.fileService.canHandleResource(resource)) { // only check for parents if the resource can be handled @@ -232,6 +247,8 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi return isEqual(dirty.resource, resource); }); } + + //#endregion } registerSingleton(IWorkingCopyFileService, WorkingCopyFileService, true); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts index 384f751f03..e071f70681 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts @@ -165,4 +165,35 @@ suite('WorkingCopyFileService', () => { listener1.dispose(); listener2.dispose(); } + + test('getDirty', async function () { + const model1 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-1.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + const model2 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-2.txt'), 'utf8', undefined); + (accessor.textFileService.files).add(model.resource, model); + + let dirty = accessor.workingCopyFileService.getDirty(model1.resource); + assert.equal(dirty.length, 0); + + await model1.load(); + model1.textEditorModel!.setValue('foo'); + + dirty = accessor.workingCopyFileService.getDirty(model1.resource); + assert.equal(dirty.length, 1); + assert.equal(dirty[0], model1); + + dirty = accessor.workingCopyFileService.getDirty(toResource.call(this, '/path')); + assert.equal(dirty.length, 1); + assert.equal(dirty[0], model1); + + await model2.load(); + model2.textEditorModel!.setValue('bar'); + + dirty = accessor.workingCopyFileService.getDirty(toResource.call(this, '/path')); + assert.equal(dirty.length, 2); + + model1.dispose(); + model2.dispose(); + }); });