diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index be451a201a..d08cfcc3cf 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -341,7 +341,8 @@ export class AzureActiveDirectoryService { const query = parseQuery(uri); const code = query.code; - if (query.state !== state) { + // Workaround double encoding issues of state in web + if (query.state !== state && decodeURIComponent(query.state) !== state) { throw new Error('State does not match.'); } diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 4a18aa9cca..3a81ce296f 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -360,7 +360,6 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi // Match quickOpen outline styles - ignore for disabled options if (this.styles.listFocusOutline) { content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { outline: 1.6px dotted ${this.styles.listFocusOutline} !important; outline-offset: -1.6px !important; }`); - } if (this.styles.listHoverOutline) { diff --git a/src/vs/base/parts/quickinput/browser/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css index f1f697abce..2971e01c42 100644 --- a/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -55,6 +55,12 @@ margin-bottom: -2px; } +.quick-input-widget.quick-navigate-mode .quick-input-header { + /* reduce margins and paddings in quick navigate mode */ + padding: 0; + margin-bottom: 0; +} + .quick-input-and-message { display: flex; flex-direction: column; @@ -126,6 +132,10 @@ margin-top: 6px; } +.quick-input-widget.quick-navigate-mode .quick-input-list { + margin-top: 0; /* reduce margins in quick navigate mode */ +} + .quick-input-list .monaco-list { overflow: hidden; max-height: calc(20 * 22px); diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 9353c8bfa9..ce47bd546e 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/quickInput'; -import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent, NO_KEY_MODS } from 'vs/base/parts/quickinput/common/quickInput'; import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { QuickInputList } from './quickInputList'; @@ -125,6 +125,7 @@ type Visibilities = { list?: boolean; ok?: boolean; customButton?: boolean; + progressBar?: boolean; }; class QuickInput extends Disposable implements IQuickInput { @@ -406,8 +407,16 @@ class QuickPick extends QuickInput implements IQuickPi private _customButton = false; private _customButtonLabel: string | undefined; private _customButtonHover: string | undefined; + private _quickNavigate: IQuickNavigateConfiguration | undefined; - quickNavigate: IQuickNavigateConfiguration | undefined; + get quickNavigate() { + return this._quickNavigate; + } + + set quickNavigate(quickNavigate: IQuickNavigateConfiguration | undefined) { + this._quickNavigate = quickNavigate; + this.update(); + } get value() { return this._value; @@ -451,6 +460,10 @@ class QuickPick extends QuickInput implements IQuickPi set items(items: Array) { this._items = items; this.itemsUpdated = true; + if (this._items.length === 0) { + // quick-navigate requires at least 1 item + this._quickNavigate = undefined; + } this.update(); } @@ -540,6 +553,13 @@ class QuickPick extends QuickInput implements IQuickPi } get keyMods() { + if (this._quickNavigate) { + // Disable keyMods when quick navigate is enabled + // because in this model the interaction is purely + // keyboard driven and Ctrl/Alt are typically + // pressed and hold during this interaction. + return NO_KEY_MODS; + } return this.ui.keyMods; } @@ -622,8 +642,10 @@ class QuickPick extends QuickInput implements IQuickPi return; } this._value = value; - this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); - this.trySelectFirst(); + const didFilter = this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); + if (didFilter) { + this.trySelectFirst(); + } this.onDidChangeValueEmitter.fire(value); })); this.visibleDisposables.add(this.ui.inputBox.onMouseDown(event => { @@ -796,8 +818,12 @@ class QuickPick extends QuickInput implements IQuickPi if (!this.visible) { return; } + dom.toggleClass(this.ui.container, 'quick-navigate-mode', !!this._quickNavigate); const ok = this.ok === 'default' ? this.canSelectMany : this.ok; - this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: true, visibleCount: true, count: true, ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : { title: !!this.title || !!this.step, description: !!this.description, inputBox: true, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok }); + const visibilities: Visibilities = this.canSelectMany ? + { title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: !this._quickNavigate, progressBar: !this._quickNavigate, visibleCount: true, count: true, ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : + { title: !!this.title || !!this.step, description: !!this.description, inputBox: !this._quickNavigate, progressBar: !this._quickNavigate, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok }; + this.ui.setVisibilities(visibilities); super.update(); if (this.ui.inputBox.value !== this.value) { this.ui.inputBox.value = this.value; @@ -818,12 +844,18 @@ class QuickPick extends QuickInput implements IQuickPi this.ui.list.sortByLabel = this.sortByLabel; if (this.itemsUpdated) { this.itemsUpdated = false; + const previousItemCount = this.ui.list.getElementsCount(); this.ui.list.setElements(this.items); this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); this.ui.count.setCount(this.ui.list.getCheckedCount()); this.trySelectFirst(); + if (this._quickNavigate && previousItemCount === 0 && this.items.length > 1) { + // quick navigate: automatically focus the second entry + // so that upon release the item is picked directly + this.ui.list.focus('Next'); + } } if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) { if (this.canSelectMany) { @@ -862,6 +894,11 @@ class QuickPick extends QuickInput implements IQuickPi this.ui.customButton.label = this.customLabel || ''; this.ui.customButton.element.title = this.customHover || ''; this.ui.setComboboxAccessibility(true); + if (!visibilities.inputBox) { + // we need to move focus into the tree to detect keybindings + // properly when the input box is not visible (quick nav) + this.ui.list.domFocus(); + } } } @@ -1440,6 +1477,7 @@ export class QuickInputController extends Disposable { ui.okContainer.style.display = visibilities.ok ? '' : 'none'; ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none'; ui.message.style.display = visibilities.message ? '' : 'none'; + ui.progressBar.getContainer().style.display = visibilities.progressBar ? '' : 'none'; ui.list.display(!!visibilities.list); ui.container.classList[visibilities.checkAll ? 'add' : 'remove']('show-checkboxes'); this.updateLayout(); // TODO diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index 47e730d804..fbb48eb4e5 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -416,6 +416,10 @@ export class QuickInputList { this._onChangedVisibleCount.fire(this.elements.length); } + getElementsCount(): number { + return this.inputElements.length; + } + getFocusedElements() { return this.list.getFocusedElements() .map(e => e.item); @@ -498,10 +502,10 @@ export class QuickInputList { this.list.layout(); } - filter(query: string) { + filter(query: string): boolean { if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) { this.list.layout(); - return; + return false; } query = query.trim(); @@ -559,6 +563,8 @@ export class QuickInputList { this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); this._onChangedVisibleCount.fire(shownElements.length); + + return true; } toggleCheckbox() { diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index aa2465e576..e37f20edda 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -49,6 +49,8 @@ export interface IKeyMods { readonly alt: boolean; } +export const NO_KEY_MODS: IKeyMods = { ctrlCmd: false, alt: false }; + export interface IQuickNavigateConfiguration { keybindings: ResolvedKeybinding[]; } diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index c1d283761a..814479ff15 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -747,13 +747,13 @@ export class DiffReview extends Disposable { let ariaLabel: string = ''; switch (type) { case DiffEntryType.Equal: - ariaLabel = nls.localize('equalLine', "original {0}, modified {1}: {2}", originalLine, modifiedLine, lineContent); + ariaLabel = nls.localize('equalLine', "{0} original line {1} modified line {2}", lineContent, originalLine, modifiedLine); break; case DiffEntryType.Insert: - ariaLabel = nls.localize('insertLine', "+ modified {0}: {1}", modifiedLine, lineContent); + ariaLabel = nls.localize('insertLine', "+ {0} modified line {1}", lineContent, modifiedLine); break; case DiffEntryType.Delete: - ariaLabel = nls.localize('deleteLine', "- original {0}: {1}", originalLine, lineContent); + ariaLabel = nls.localize('deleteLine', "- {0} original line {1}", lineContent, originalLine); break; } row.setAttribute('aria-label', ariaLabel); @@ -869,9 +869,14 @@ class DiffReviewPrev extends EditorAction { function findFocusedDiffEditor(accessor: ServicesAccessor): DiffEditorWidget | null { const codeEditorService = accessor.get(ICodeEditorService); const diffEditors = codeEditorService.listDiffEditors(); + const activeCodeEditor = codeEditorService.getActiveCodeEditor(); + if (!activeCodeEditor) { + return null; + } + for (let i = 0, len = diffEditors.length; i < len; i++) { const diffEditor = diffEditors[i]; - if (diffEditor.hasWidgetFocus()) { + if (diffEditor.getModifiedEditor().getId() === activeCodeEditor.getId() || diffEditor.getOriginalEditor().getId() === activeCodeEditor.getId()) { return diffEditor; } } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index deebd6a8d5..753c0de52a 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2841,6 +2841,14 @@ export interface ISuggestOptions { * Show typeParameter-suggestions. */ showTypeParameters?: boolean; + /** + * Show issue-suggestions. + */ + showIssues?: boolean; + /** + * Show user-suggestions. + */ + showUsers?: boolean; /** * Show snippet-suggestions. */ @@ -2895,6 +2903,8 @@ class EditorSuggest extends BaseEditorOption { - if (lastKnownEditorViewState) { - editor.restoreViewState(lastKnownEditorViewState); - } - }); + const codeEditor = getCodeEditor(editor); + if (codeEditor) { + + // Remember view state and update it when the cursor position + // changes even later because it could be that the user has + // configured quick open to remain open when focus is lost and + // we always want to restore the current location. + let lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); + disposables.add(codeEditor.onDidChangeCursorPosition(() => { + lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); + })); + + once(token.onCancellationRequested)(() => { + if (lastKnownEditorViewState) { + editor.restoreViewState(lastKnownEditorViewState); + } + }); + } // Clean up decorations on dispose disposables.add(toDisposable(() => this.clearDecorations(editor))); @@ -98,9 +110,9 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu */ protected abstract provideWithoutTextEditor(picker: IQuickPick, token: CancellationToken): IDisposable; - protected gotoLocation(editor: IEditor, range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean): void { - editor.setSelection(range); - editor.revealRangeInCenter(range, ScrollType.Smooth); + protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean }): void { + editor.setSelection(options.range); + editor.revealRangeInCenter(options.range, ScrollType.Smooth); editor.focus(); } diff --git a/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts index c60bbfa792..a38e5f0a52 100644 --- a/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts @@ -38,7 +38,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor return; } - this.gotoLocation(editor, this.toRange(item.lineNumber, item.column), picker.keyMods); + this.gotoLocation(editor, { range: this.toRange(item.lineNumber, item.column), keyMods: picker.keyMods }); picker.hide(); } diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index ac83aff021..8008fe6ca2 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -95,7 +95,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit disposables.add(picker.onDidAccept(() => { const [item] = picker.selectedItems; if (item && item.range) { - this.gotoLocation(editor, item.range.selection, picker.keyMods); + this.gotoLocation(editor, { range: item.range.selection, keyMods: picker.keyMods }); picker.hide(); } @@ -104,7 +104,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit // Goto symbol side by side if enabled disposables.add(picker.onDidTriggerItemButton(({ item }) => { if (item && item.range) { - this.gotoLocation(editor, item.range.selection, picker.keyMods, true); + this.gotoLocation(editor, { range: item.range.selection, keyMods: picker.keyMods, forceSideBySide: true }); picker.hide(); } diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index eec49f41d5..c4dee163ba 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -195,7 +195,6 @@ class ItemRenderer implements IListRenderer= 0) { diff --git a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts index 24f8273210..65616da45a 100644 --- a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts @@ -360,10 +360,8 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { const lineHeight = this.editor.getOption(EditorOption.lineHeight); // adjust heightInLines to viewport - const maxHeightInLines = (this.editor.getLayoutInfo().height / lineHeight) * 0.8; - if (heightInLines >= maxHeightInLines) { - heightInLines = maxHeightInLines; - } + const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8); + heightInLines = Math.min(heightInLines, maxHeightInLines); let arrowHeight = 0; let frameThickness = 0; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 71f295b3dc..bbc656d78d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3771,6 +3771,14 @@ declare namespace monaco.editor { * Show typeParameter-suggestions. */ showTypeParameters?: boolean; + /** + * Show issue-suggestions. + */ + showIssues?: boolean; + /** + * Show user-suggestions. + */ + showUsers?: boolean; /** * Show snippet-suggestions. */ @@ -5393,7 +5401,9 @@ declare namespace monaco.languages { Customcolor = 22, Folder = 23, TypeParameter = 24, - Snippet = 25 + User = 25, + Issue = 26, + Snippet = 27 } export interface CompletionItemLabel { diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 0bd2644919..c0fe2f7082 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -114,6 +114,7 @@ export class MenuId { static readonly CommentThreadActions = new MenuId('CommentThreadActions'); static readonly CommentTitle = new MenuId('CommentTitle'); static readonly CommentActions = new MenuId('CommentActions'); + static readonly NotebookCellTitle = new MenuId('NotebookCellTitle'); static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditContext = new MenuId('BulkEditContext'); static readonly ObjectExplorerItemContext = new MenuId('ObjectExplorerItemContext'); // {{SQL CARBON EDIT}} diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index ab6cb003ae..a434b4e76b 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -132,6 +132,7 @@ export interface IEnvironmentService extends IUserHomeProvider { keybindingsResource: URI; keyboardLayoutResource: URI; argvResource: URI; + snippetsHome: URI; // sync resources userDataSyncLogResource: URI; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index e80bdba251..1226d440ae 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -142,6 +142,9 @@ export class EnvironmentService implements IEnvironmentService { return URI.file(path.join(this.userHome, product.dataFolderName, 'argv.json')); } + @memoize + get snippetsHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'snippets'); } + @memoize get isExtensionDevelopment(): boolean { return !!this._args.extensionDevelopmentPath; } diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index 9960ee0fad..53e56503f4 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -97,7 +97,14 @@ export abstract class PickerQuickAccessProvider | Promise> | FastAndSlowPicksType; + protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array | Promise> | FastAndSlowPicksType | null; } diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index 2b2fa72df6..d5b966139a 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -5,7 +5,7 @@ import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor, IQuickAccessOptions } from 'vs/platform/quickinput/common/quickAccess'; import { Registry } from 'vs/platform/registry/common/platform'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -25,7 +25,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon super(); } - show(value = ''): void { + show(value = '', options?: IQuickAccessOptions): void { const disposables = new DisposableStore(); // Hide any previous picker if any @@ -39,7 +39,8 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon const picker = disposables.add(this.quickInputService.createQuickPick()); picker.placeholder = descriptor?.placeholder; picker.value = value; - picker.valueSelection = [value.length, value.length]; + picker.quickNavigate = options?.quickNavigateConfiguration; + picker.valueSelection = options?.inputSelection ? [options.inputSelection.start, options.inputSelection.end] : [value.length, value.length]; picker.contextKey = descriptor?.contextKey; picker.filterValue = (value: string) => value.substring(descriptor ? descriptor.prefix.length : 0); diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index db1fac175f..b0e03da2a6 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -3,19 +3,32 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPick, IQuickPickItem, IQuickNavigateConfiguration } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Registry } from 'vs/platform/registry/common/platform'; import { first, coalesce } from 'vs/base/common/arrays'; import { startsWith } from 'vs/base/common/strings'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +export interface IQuickAccessOptions { + + /** + * Allows to control the part of text in the input field that should be selected. + */ + inputSelection?: { start: number; end: number; }; + + /** + * Allows to enable quick navigate support in quick input. + */ + quickNavigateConfiguration?: IQuickNavigateConfiguration; +} + export interface IQuickAccessController { /** * Open the quick access picker with the optional value prefilled. */ - show(value?: string): void; + show(value?: string, options?: IQuickAccessOptions): void; } export interface IQuickAccessProvider { diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 3121844870..8687c03b48 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { IFileService, IFileContent, FileChangesEvent, FileSystemProviderError, FileSystemProviderErrorCode, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; +import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict } from 'vs/platform/userDataSync/common/userDataSync'; @@ -110,6 +110,9 @@ export abstract class AbstractSynchroniser extends Disposable { async sync(ref?: string): Promise { if (!this.isEnabled()) { + if (this.status !== SyncStatus.Idle) { + await this.stop(); + } this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`); return; } @@ -264,6 +267,7 @@ export abstract class AbstractSynchroniser extends Disposable { protected abstract readonly version: number; protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; + abstract stop(): Promise; } export interface IFileSyncPreviewResult { @@ -299,7 +303,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { async stop(): Promise { this.cancel(); - this.logService.trace(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`); + this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`); try { await this.fileService.del(this.localPreviewResource); } catch (e) { /* ignore */ } @@ -339,7 +343,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false }); } } catch (e) { - if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) || + if ((e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) || (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) { throw new UserDataSyncError(e.message, UserDataSyncErrorCode.LocalPreconditionFailed); } else { diff --git a/src/vs/platform/userDataSync/common/snippetsMerge.ts b/src/vs/platform/userDataSync/common/snippetsMerge.ts new file mode 100644 index 0000000000..0d955316ae --- /dev/null +++ b/src/vs/platform/userDataSync/common/snippetsMerge.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { values } from 'vs/base/common/map'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { deepClone } from 'vs/base/common/objects'; + +export interface IMergeResult { + added: IStringDictionary; + updated: IStringDictionary; + removed: string[]; + conflicts: string[]; + remote: IStringDictionary | null; +} + +export function merge(local: IStringDictionary, remote: IStringDictionary | null, base: IStringDictionary | null, resolvedConflicts: IStringDictionary = {}): IMergeResult { + const added: IStringDictionary = {}; + const updated: IStringDictionary = {}; + const removed: Set = new Set(); + + if (!remote) { + return { + added, + removed: values(removed), + updated, + conflicts: [], + remote: local + }; + } + + const localToRemote = compare(local, remote); + if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { + // No changes found between local and remote. + return { + added, + removed: values(removed), + updated, + conflicts: [], + remote: null + }; + } + + const baseToLocal = compare(base, local); + const baseToRemote = compare(base, remote); + const remoteContent: IStringDictionary = deepClone(remote); + const conflicts: Set = new Set(); + const handledConflicts: Set = new Set(); + const handleConflict = (key: string): void => { + if (handledConflicts.has(key)) { + return; + } + handledConflicts.add(key); + const conflictContent = resolvedConflicts[key]; + + // add to conflicts + if (conflictContent === undefined) { + conflicts.add(key); + } + + // remove the snippet + else if (conflictContent === null) { + delete remote[key]; + if (local[key]) { + removed.add(key); + } + } + + // add/update the snippet + else { + if (local[key]) { + if (local[key] !== conflictContent) { + updated[key] = conflictContent; + } + } else { + added[key] = conflictContent; + } + remoteContent[key] = conflictContent; + } + }; + + // Removed snippets in Local + for (const key of values(baseToLocal.removed)) { + // Conflict - Got updated in remote. + if (baseToRemote.updated.has(key)) { + // Add to local + added[key] = remote[key]; + } + // Remove it in remote + else { + delete remoteContent[key]; + } + } + + // Removed snippets in Remote + for (const key of values(baseToRemote.removed)) { + if (handledConflicts.has(key)) { + continue; + } + // Conflict - Got updated in local + if (baseToLocal.updated.has(key)) { + handleConflict(key); + } + // Also remove in Local + else { + removed.add(key); + } + } + + // Updated snippets in Local + for (const key of values(baseToLocal.updated)) { + if (handledConflicts.has(key)) { + continue; + } + // Got updated in remote + if (baseToRemote.updated.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + handleConflict(key); + } + } else { + remoteContent[key] = local[key]; + } + } + + // Updated snippets in Remote + for (const key of values(baseToRemote.updated)) { + if (handledConflicts.has(key)) { + continue; + } + // Got updated in local + if (baseToLocal.updated.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + handleConflict(key); + } + } else if (local[key] !== undefined) { + updated[key] = remote[key]; + } + } + + // Added snippets in Local + for (const key of values(baseToLocal.added)) { + if (handledConflicts.has(key)) { + continue; + } + // Got added in remote + if (baseToRemote.added.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + handleConflict(key); + } + } else { + remoteContent[key] = local[key]; + } + } + + // Added snippets in remote + for (const key of values(baseToRemote.added)) { + if (handledConflicts.has(key)) { + continue; + } + // Got added in local + if (baseToLocal.added.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + handleConflict(key); + } + } else { + added[key] = remote[key]; + } + } + + return { added, removed: values(removed), updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent }; +} + +function compare(from: IStringDictionary | null, to: IStringDictionary | null): { added: Set, removed: Set, updated: Set } { + const fromKeys = from ? Object.keys(from) : []; + const toKeys = to ? Object.keys(to) : []; + const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const updated: Set = new Set(); + + for (const key of fromKeys) { + if (removed.has(key)) { + continue; + } + const fromSnippet = from![key]!; + const toSnippet = to![key]!; + if (fromSnippet !== toSnippet) { + updated.add(key); + } + } + + return { added, removed, updated }; +} + +function areSame(a: IStringDictionary, b: IStringDictionary): boolean { + const { added, removed, updated } = compare(a, b); + return added.size === 0 && removed.size === 0 && updated.size === 0; +} diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts new file mode 100644 index 0000000000..65fc303f2b --- /dev/null +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -0,0 +1,403 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { URI } from 'vs/base/common/uri'; +import { joinPath, extname, relativePath, isEqualOrParent, isEqual, basename } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +interface ISyncPreviewResult { + readonly local: IStringDictionary; + readonly remoteUserData: IRemoteUserData; + readonly lastSyncUserData: IRemoteUserData | null; + readonly added: IStringDictionary; + readonly updated: IStringDictionary; + readonly removed: string[]; + readonly conflicts: Conflict[]; + readonly resolvedConflicts: IStringDictionary; + readonly remote: IStringDictionary | null; +} + +export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { + + protected readonly version: number = 1; + private readonly snippetsFolder: URI; + private readonly snippetsPreviewFolder: URI; + private syncPreviewResultPromise: CancelablePromise | null = null; + + constructor( + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService fileService: IFileService, + @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + @IConfigurationService configurationService: IConfigurationService, + @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + super(SyncResource.Snippets, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + this.snippetsFolder = environmentService.snippetsHome; + this.snippetsPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME); + this._register(this.fileService.watch(environmentService.userRoamingDataHome)); + this._register(this.fileService.watch(this.snippetsFolder)); + this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); + } + + private onFileChanges(e: FileChangesEvent): void { + if (!e.changes.some(change => isEqualOrParent(change.resource, this.snippetsFolder))) { + return; + } + if (!this.isEnabled()) { + return; + } + // Sync again if local file has changed and current status is in conflicts + if (this.status === SyncStatus.HasConflicts) { + this.syncPreviewResultPromise!.then(result => { + this.cancel(); + this.doSync(result.remoteUserData, result.lastSyncUserData).then(status => this.setStatus(status)); + }); + } + // Otherwise fire change event + else { + this._onDidChangeLocal.fire(); + } + } + + async pull(): Promise { + if (!this.isEnabled()) { + this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling snippets as it is disabled.`); + return; + } + + this.stop(); + + try { + this.logService.info(`${this.syncResourceLogLabel}: Started pulling snippets...`); + this.setStatus(SyncStatus.Syncing); + + const lastSyncUserData = await this.getLastSyncUserData(); + const remoteUserData = await this.getRemoteUserData(lastSyncUserData); + + if (remoteUserData.syncData !== null) { + const local = await this.getSnippetsFileContents(); + const localSnippets = this.toSnippetsContents(local); + const remoteSnippets = this.parseSnippets(remoteUserData.syncData); + const { added, updated, remote, removed } = merge(localSnippets, remoteSnippets, localSnippets); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {} + })); + await this.apply(); + } + + // No remote exists to pull + else { + this.logService.info(`${this.syncResourceLogLabel}: Remote snippets does not exist.`); + } + + this.logService.info(`${this.syncResourceLogLabel}: Finished pulling snippets.`); + } finally { + this.setStatus(SyncStatus.Idle); + } + } + + async push(): Promise { + if (!this.isEnabled()) { + this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing snippets as it is disabled.`); + return; + } + + this.stop(); + + try { + this.logService.info(`${this.syncResourceLogLabel}: Started pushing snippets...`); + this.setStatus(SyncStatus.Syncing); + + const local = await this.getSnippetsFileContents(); + const localSnippets = this.toSnippetsContents(local); + const { added, removed, updated, remote } = merge(localSnippets, null, null); + const lastSyncUserData = await this.getLastSyncUserData(); + const remoteUserData = await this.getRemoteUserData(lastSyncUserData); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {} + })); + + await this.apply(true); + + this.logService.info(`${this.syncResourceLogLabel}: Finished pushing snippets.`); + } finally { + this.setStatus(SyncStatus.Idle); + } + + } + + async stop(): Promise { + await this.clearConflicts(); + this.cancel(); + this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.syncResourceLogLabel}.`); + this.setStatus(SyncStatus.Idle); + } + + async getConflictContent(conflictResource: URI): Promise { + if (isEqualOrParent(conflictResource.with({ scheme: this.syncFolder.scheme }), this.snippetsPreviewFolder) && this.syncPreviewResultPromise) { + const result = await this.syncPreviewResultPromise; + const key = relativePath(this.snippetsPreviewFolder, conflictResource.with({ scheme: this.snippetsPreviewFolder.scheme }))!; + if (conflictResource.scheme === this.snippetsPreviewFolder.scheme) { + return result.local[key] ? result.local[key].value.toString() : null; + } else if (result.remoteUserData && result.remoteUserData.syncData) { + const snippets = this.parseSnippets(result.remoteUserData.syncData); + return snippets[key] || null; + } + } + return null; + } + + async getRemoteContent(ref?: string, fragment?: string): Promise { + const content = await super.getRemoteContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + async getLocalBackupContent(ref?: string, fragment?: string): Promise { + let content = await super.getLocalBackupContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + private getFragment(content: string, fragment: string): string | null { + const syncData = this.parseSyncData(content); + return syncData ? this.getFragmentFromSyncData(syncData, fragment) : null; + } + + private getFragmentFromSyncData(syncData: ISyncData, fragment: string): string | null { + switch (fragment) { + case 'snippets': + return syncData.content; + default: + const remoteSnippets = this.parseSnippets(syncData); + return remoteSnippets[fragment] || null; + } + } + + async acceptConflict(conflictResource: URI, content: string): Promise { + const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0]; + if (this.status === SyncStatus.HasConflicts && conflict) { + const key = relativePath(this.snippetsPreviewFolder, conflict.local)!; + let previewResult = await this.syncPreviewResultPromise!; + this.cancel(); + previewResult.resolvedConflicts[key] = content || null; + this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, previewResult.resolvedConflicts, token)); + previewResult = await this.syncPreviewResultPromise; + this.setConflicts(previewResult.conflicts); + if (!this.conflicts.length) { + await this.apply(); + this.setStatus(SyncStatus.Idle); + } + } + } + + async hasLocalData(): Promise { + try { + const localSnippets = await this.getSnippetsFileContents(); + if (Object.keys(localSnippets).length) { + return true; + } + } catch (error) { + /* ignore error */ + } + return false; + } + + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + try { + const previewResult = await this.getPreview(remoteUserData, lastSyncUserData); + this.setConflicts(previewResult.conflicts); + if (this.conflicts.length) { + return SyncStatus.HasConflicts; + } + await this.apply(); + return SyncStatus.Idle; + } catch (e) { + this.syncPreviewResultPromise = null; + if (e instanceof UserDataSyncError) { + switch (e.code) { + case UserDataSyncErrorCode.LocalPreconditionFailed: + // Rejected as there is a new local version. Syncing again. + this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize snippets as there is a new local version available. Synchronizing again...`); + return this.performSync(remoteUserData, lastSyncUserData); + } + } + throw e; + } + } + + private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + if (!this.syncPreviewResultPromise) { + this.syncPreviewResultPromise = createCancelablePromise(token => this.getSnippetsFileContents() + .then(local => this.generatePreview(local, remoteUserData, lastSyncUserData, {}, token))); + } + return this.syncPreviewResultPromise; + } + + protected cancel(): void { + if (this.syncPreviewResultPromise) { + this.syncPreviewResultPromise.cancel(); + this.syncPreviewResultPromise = null; + } + } + + private async clearConflicts(): Promise { + if (this.conflicts.length) { + await Promise.all(this.conflicts.map(({ local }) => this.fileService.del(local))); + this.setConflicts([]); + } + } + + private async generatePreview(local: IStringDictionary, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary, token: CancellationToken): Promise { + const localSnippets = this.toSnippetsContents(local); + const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; + const lastSyncSnippets: IStringDictionary | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null; + + if (remoteSnippets) { + this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`); + } else { + this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`); + } + + const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts); + + const conflicts: Conflict[] = []; + for (const key of mergeResult.conflicts) { + const localPreview = joinPath(this.snippetsPreviewFolder, key); + conflicts.push({ local: localPreview, remote: localPreview.with({ scheme: USER_DATA_SYNC_SCHEME }) }); + const content = local[key]; + if (!token.isCancellationRequested) { + await this.fileService.writeFile(localPreview, content ? content.value : VSBuffer.fromString('')); + } + } + + for (const conflict of this.conflicts) { + // clear obsolete conflicts + if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) { + try { + await this.fileService.del(conflict.local); + } catch (error) { + // Ignore & log + this.logService.error(error); + } + } + } + + return { remoteUserData, local, lastSyncUserData, added: mergeResult.added, removed: mergeResult.removed, updated: mergeResult.updated, conflicts, remote: mergeResult.remote, resolvedConflicts }; + } + + private async apply(forcePush?: boolean): Promise { + if (!this.syncPreviewResultPromise) { + return; + } + + let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData } = await this.syncPreviewResultPromise; + + const hasChanges = Object.keys(added).length || removed.length || Object.keys(updated).length || remote; + + if (!hasChanges) { + this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`); + } + + if (Object.keys(added).length || removed.length || Object.keys(updated).length) { + // back up all snippets + await this.backupLocal(JSON.stringify(this.toSnippetsContents(local))); + await this.updateLocalSnippets(added, removed, updated, local); + } + + if (remote) { + // update remote + this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`); + const content = JSON.stringify(remote); + remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`); + } + + if (lastSyncUserData?.ref !== remoteUserData.ref) { + // update last sync + this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized snippets...`); + await this.updateLastSyncUserData(remoteUserData); + this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`); + } + + this.syncPreviewResultPromise = null; + } + + private async updateLocalSnippets(added: IStringDictionary, removed: string[], updated: IStringDictionary, local: IStringDictionary): Promise { + for (const key of removed) { + const resource = joinPath(this.snippetsFolder, key); + this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource)); + await this.fileService.del(resource); + this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource)); + } + + for (const key of Object.keys(added)) { + const resource = joinPath(this.snippetsFolder, key); + this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource)); + await this.fileService.createFile(resource, VSBuffer.fromString(added[key]), { overwrite: false }); + this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource)); + } + + for (const key of Object.keys(updated)) { + const resource = joinPath(this.snippetsFolder, key); + this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource)); + await this.fileService.writeFile(resource, VSBuffer.fromString(updated[key]), local[key]); + this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource)); + } + } + + private parseSnippets(syncData: ISyncData): IStringDictionary { + return JSON.parse(syncData.content); + } + + private toSnippetsContents(snippetsFileContents: IStringDictionary): IStringDictionary { + const snippets: IStringDictionary = {}; + for (const key of Object.keys(snippetsFileContents)) { + snippets[key] = snippetsFileContents[key].value.toString(); + } + return snippets; + } + + private async getSnippetsFileContents(): Promise> { + const snippets: IStringDictionary = {}; + let stat: IFileStat; + try { + stat = await this.fileService.resolve(this.snippetsFolder); + } catch (e) { + // No snippets + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return snippets; + } else { + throw e; + } + } + for (const entry of stat.children || []) { + const resource = entry.resource; + if (extname(resource) === '.json') { + const key = relativePath(this.snippetsFolder, resource)!; + const content = await this.fileService.readFile(resource); + snippets[key] = content; + } + } + return snippets; + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 62bcefacca..60c25cbed0 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -138,10 +138,11 @@ export function getUserDataSyncStore(productService: IProductService, configurat export const enum SyncResource { Settings = 'settings', Keybindings = 'keybindings', + Snippets = 'snippets', Extensions = 'extensions', GlobalState = 'globalState' } -export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Extensions, SyncResource.GlobalState]; +export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState]; export interface IUserDataManifest { latest?: Record @@ -373,10 +374,3 @@ export function getSyncResourceFromLocalPreview(localPreview: URI, environmentSe localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme }); return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0]; } -export function getSyncResourceFromRemotePreview(remotePreview: URI, environmentService: IEnvironmentService): SyncResource | undefined { - if (remotePreview.scheme !== USER_DATA_SYNC_SCHEME) { - return undefined; - } - remotePreview = remotePreview.with({ scheme: environmentService.userDataSyncHome.scheme }); - return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(remotePreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0]; -} diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 376b4ef2ad..57a9858e90 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -18,6 +18,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { URI } from 'vs/base/common/uri'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { isEqual } from 'vs/base/common/resources'; +import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync'; type SyncErrorClassification = { source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -55,6 +56,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private readonly settingsSynchroniser: SettingsSynchroniser; private readonly keybindingsSynchroniser: KeybindingsSynchroniser; + private readonly snippetsSynchroniser: SnippetsSynchroniser; private readonly extensionsSynchroniser: ExtensionsSynchroniser; private readonly globalStateSynchroniser: GlobalStateSynchroniser; @@ -68,9 +70,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ super(); this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser)); this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); + this.snippetsSynchroniser = this._register(this.instantiationService.createInstance(SnippetsSynchroniser)); this.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser)); this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser)); - this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser]; + this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser]; this.updateStatus(); if (this.userDataSyncStoreService.userDataSyncStore) { diff --git a/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts new file mode 100644 index 0000000000..96b205de75 --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts @@ -0,0 +1,436 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; + +const tsSnippet1 = `{ + + // Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the + // same ids are connected. + "Print to console": { + // Example: + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console", + } + +}`; + +const tsSnippet2 = `{ + + // Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the + // same ids are connected. + "Print to console": { + // Example: + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console always", + } + +}`; + +const htmlSnippet1 = `{ +/* + // Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +*/ +"Div": { + "prefix": "div", + "body": [ + "
", + "", + "
" + ], + "description": "New div" + } +}`; + +const htmlSnippet2 = `{ +/* + // Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +*/ +"Div": { + "prefix": "div", + "body": [ + "
", + "", + "
" + ], + "description": "New div changed" + } +}`; + +const cSnippet = `{ + // Place your snippets for c here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position.Placeholders with the + // same ids are connected. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +}`; + +suite('SnippetsMerge', () => { + + test('merge when local and remote are same with one snippet', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when local and remote are same with multiple entries', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when local and remote are same with multiple entries in different order', async () => { + const local = { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when local and remote are same with different base content', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const base = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2 }; + + const actual = merge(local, remote, base); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when a new entry is added to remote', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when multiple new entries are added to remote', async () => { + const local = {}; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, remote); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when new entry is added to remote from base and local has not changed', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, local); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when an entry is removed from remote from base and local has not changed', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const remote = { 'html.json': htmlSnippet1 }; + + const actual = merge(local, remote, local); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, ['typescript.json']); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when all entries are removed from base and local has not changed', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const remote = {}; + + const actual = merge(local, remote, local); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, ['html.json', 'typescript.json']); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when an entry is updated in remote from base and local has not changed', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet2 }; + + const actual = merge(local, remote, local); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const remote = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; + + const actual = merge(local, remote, local); + + assert.deepEqual(actual.added, { 'c.json': cSnippet }); + assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.removed, ['typescript.json']); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when a new entries are added to local', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1, 'c.json': cSnippet }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when multiple new entries are added to local from base and remote is not changed', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1, 'c.json': cSnippet }; + const remote = { 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, remote); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1, 'c.json': cSnippet }); + }); + + test('merge when an entry is removed from local from base and remote has not changed', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, remote); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when an entry is updated in local from base and remote has not changed', async () => { + const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, remote); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => { + const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, remote); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when local and remote with one entry but different value', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet2 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, ['html.json']); + assert.deepEqual(actual.remote, null); + }); + + test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => { + const base = { 'html.json': htmlSnippet1 }; + const local = { 'html.json': htmlSnippet2 }; + const remote = { 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, base); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, ['html.json']); + assert.deepEqual(actual.remote, null); + }); + + test('merge with single entry and local is empty', async () => { + const base = { 'html.json': htmlSnippet1 }; + const local = {}; + const remote = { 'html.json': htmlSnippet2 }; + + const actual = merge(local, remote, base); + + assert.deepEqual(actual.added, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when local and remote has moved forwareded with conflicts', async () => { + const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; + const remote = { 'typescript.json': tsSnippet2 }; + + const actual = merge(local, remote, base); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, ['html.json']); + assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet }); + }); + + test('merge when local and remote has moved forwareded with resolved conflicts - update', async () => { + const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; + const remote = { 'typescript.json': tsSnippet2 }; + const resolvedConflicts = { 'html.json': htmlSnippet2 }; + + const actual = merge(local, remote, base, resolvedConflicts); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'html.json': htmlSnippet2, 'c.json': cSnippet }); + }); + + test('merge when local and remote has moved forwareded with resolved conflicts - remove', async () => { + const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; + const remote = { 'typescript.json': tsSnippet2 }; + const resolvedConflicts = { 'html.json': null }; + + const actual = merge(local, remote, base, resolvedConflicts); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, ['html.json']); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet }); + }); + + test('merge when local and remote has moved forwareded with multiple conflicts', async () => { + const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet }; + const remote = { 'c.json': cSnippet }; + + const actual = merge(local, remote, base); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, ['html.json', 'typescript.json']); + assert.deepEqual(actual.remote, null); + }); + + test('merge when local and remote has moved forwareded with multiple conflicts and resolving one conflict', async () => { + const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet }; + const remote = { 'c.json': cSnippet }; + const resolvedConflicts = { 'html.json': htmlSnippet1 }; + + const actual = merge(local, remote, base, resolvedConflicts); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, { 'html.json': htmlSnippet1 }); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, ['typescript.json']); + assert.deepEqual(actual.remote, { 'c.json': cSnippet, 'html.json': htmlSnippet1 }); + }); + +}); diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts new file mode 100644 index 0000000000..6343a1b96b --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -0,0 +1,614 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync'; +import { joinPath } from 'vs/base/common/resources'; +import { IStringDictionary } from 'vs/base/common/collections'; + +const tsSnippet1 = `{ + + // Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the + // same ids are connected. + "Print to console": { + // Example: + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console", + } + +}`; + +const tsSnippet2 = `{ + + // Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the + // same ids are connected. + "Print to console": { + // Example: + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console always", + } + +}`; + +const htmlSnippet1 = `{ +/* + // Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +*/ +"Div": { + "prefix": "div", + "body": [ + "
", + "", + "
" + ], + "description": "New div" + } +}`; + +const htmlSnippet2 = `{ +/* + // Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +*/ +"Div": { + "prefix": "div", + "body": [ + "
", + "", + "
" + ], + "description": "New div changed" + } +}`; + +const htmlSnippet3 = `{ +/* + // Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +*/ +"Div": { + "prefix": "div", + "body": [ + "
", + "", + "
" + ], + "description": "New div changed again" + } +}`; + +suite('SnippetsSync', () => { + + const disposableStore = new DisposableStore(); + const server = new UserDataSyncTestServer(); + let testClient: UserDataSyncClient; + let client2: UserDataSyncClient; + + let testObject: SnippetsSynchroniser; + + setup(async () => { + testClient = disposableStore.add(new UserDataSyncClient(server)); + await testClient.setUp(true); + testObject = (testClient.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Snippets) as SnippetsSynchroniser; + disposableStore.add(toDisposable(() => testClient.instantiationService.get(IUserDataSyncStoreService).clear())); + + client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + }); + + teardown(() => disposableStore.clear()); + + test('first time sync - outgoing to server (no snippets)', async () => { + await updateSnippet('html.json', htmlSnippet1, testClient); + await updateSnippet('typescript.json', tsSnippet1, testClient); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }); + }); + + test('first time sync - incoming from server (no snippets)', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + }); + + test('first time sync when snippets exists', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + + await updateSnippet('typescript.json', tsSnippet1, testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }); + }); + + test('first time sync when snippets exists - has conflicts', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + + assert.equal(testObject.status, SyncStatus.HasConflicts); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); + assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]); + }); + + test('first time sync when snippets exists - has conflicts and accept conflicts', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + const conflicts = testObject.conflicts; + await testObject.acceptConflict(conflicts[0].local, htmlSnippet1); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + const fileService = testClient.instantiationService.get(IFileService); + assert.ok(!await fileService.exists(conflicts[0].local)); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet1 }); + }); + + test('first time sync when snippets exists - has multiple conflicts', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await updateSnippet('typescript.json', tsSnippet2, testClient); + await testObject.sync(); + + assert.equal(testObject.status, SyncStatus.HasConflicts); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + const local1 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); + const local2 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'); + assertConflicts(testObject.conflicts, [ + { local: local1, remote: local1.with({ scheme: USER_DATA_SYNC_SCHEME }) }, + { local: local2, remote: local2.with({ scheme: USER_DATA_SYNC_SCHEME }) } + ]); + }); + + test('first time sync when snippets exists - has multiple conflicts and accept one conflict', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await updateSnippet('typescript.json', tsSnippet2, testClient); + await testObject.sync(); + + let conflicts = testObject.conflicts; + await testObject.acceptConflict(conflicts[0].local, htmlSnippet2); + const fileService = testClient.instantiationService.get(IFileService); + assert.ok(!await fileService.exists(conflicts[0].local)); + + conflicts = testObject.conflicts; + assert.equal(testObject.status, SyncStatus.HasConflicts); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'); + assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]); + }); + + test('first time sync when snippets exists - has multiple conflicts and accept all conflicts', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await updateSnippet('typescript.json', tsSnippet2, testClient); + await testObject.sync(); + + const conflicts = testObject.conflicts; + await testObject.acceptConflict(conflicts[0].local, htmlSnippet2); + await testObject.acceptConflict(conflicts[1].local, tsSnippet1); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + const fileService = testClient.instantiationService.get(IFileService); + assert.ok(!await fileService.exists(conflicts[0].local)); + assert.ok(!await fileService.exists(conflicts[1].local)); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet2); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet1 }); + }); + + test('sync adding a snippet', async () => { + await updateSnippet('html.json', htmlSnippet1, testClient); + await testObject.sync(); + + await updateSnippet('typescript.json', tsSnippet1, testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }); + }); + + test('sync adding a snippet - accept', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + }); + + test('sync updating a snippet', async () => { + await updateSnippet('html.json', htmlSnippet1, testClient); + await testObject.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet2); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet2 }); + }); + + test('sync updating a snippet - accept', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await updateSnippet('html.json', htmlSnippet2, client2); + await client2.sync(); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet2); + }); + + test('sync updating a snippet - conflict', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await updateSnippet('html.json', htmlSnippet2, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet3, testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.HasConflicts); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); + assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]); + }); + + test('sync updating a snippet - resolve conflict', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await updateSnippet('html.json', htmlSnippet2, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet3, testClient); + await testObject.sync(); + await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet2); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet2); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet2 }); + }); + + test('sync removing a snippet', async () => { + await updateSnippet('html.json', htmlSnippet1, testClient); + await updateSnippet('typescript.json', tsSnippet1, testClient); + await testObject.sync(); + + await removeSnippet('html.json', testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('typescript.json', testClient); + assert.equal(actual1, tsSnippet1); + const actual2 = await readSnippet('html.json', testClient); + assert.equal(actual2, null); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'typescript.json': tsSnippet1 }); + }); + + test('sync removing a snippet - accept', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await removeSnippet('html.json', client2); + await client2.sync(); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('typescript.json', testClient); + assert.equal(actual1, tsSnippet1); + const actual2 = await readSnippet('html.json', testClient); + assert.equal(actual2, null); + }); + + test('sync removing a snippet locally and updating it remotely', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await updateSnippet('html.json', htmlSnippet2, client2); + await client2.sync(); + + await removeSnippet('html.json', testClient); + await testObject.sync(); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('typescript.json', testClient); + assert.equal(actual1, tsSnippet1); + const actual2 = await readSnippet('html.json', testClient); + assert.equal(actual2, htmlSnippet2); + }); + + test('sync removing a snippet - conflict', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await removeSnippet('html.json', client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + + assert.equal(testObject.status, SyncStatus.HasConflicts); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); + assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]); + }); + + test('sync removing a snippet - resolve conflict', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await removeSnippet('html.json', client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet3); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('typescript.json', testClient); + assert.equal(actual1, tsSnippet1); + const actual2 = await readSnippet('html.json', testClient); + assert.equal(actual2, htmlSnippet3); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet3 }); + }); + + test('sync removing a snippet - resolve conflict by removing', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await removeSnippet('html.json', client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + await testObject.acceptConflict(testObject.conflicts[0].local, ''); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('typescript.json', testClient); + assert.equal(actual1, tsSnippet1); + const actual2 = await readSnippet('html.json', testClient); + assert.equal(actual2, null); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'typescript.json': tsSnippet1 }); + }); + + test('first time sync - push', async () => { + await updateSnippet('html.json', htmlSnippet1, testClient); + await updateSnippet('typescript.json', tsSnippet1, testClient); + + await testObject.push(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }); + }); + + test('first time sync - pull', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await testObject.pull(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + }); + + function parseSnippets(content: string): IStringDictionary { + const syncData: ISyncData = JSON.parse(content); + return JSON.parse(syncData.content); + } + + async function updateSnippet(name: string, content: string, client: UserDataSyncClient): Promise { + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + const snippetsResource = joinPath(environmentService.snippetsHome, name); + await fileService.writeFile(snippetsResource, VSBuffer.fromString(content)); + } + + async function removeSnippet(name: string, client: UserDataSyncClient): Promise { + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + const snippetsResource = joinPath(environmentService.snippetsHome, name); + await fileService.del(snippetsResource); + } + + async function readSnippet(name: string, client: UserDataSyncClient): Promise { + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + const snippetsResource = joinPath(environmentService.snippetsHome, name); + if (await fileService.exists(snippetsResource)) { + const content = await fileService.readFile(snippetsResource); + return content.value.toString(); + } + return null; + } + + function assertConflicts(actual: Conflict[], expected: Conflict[]) { + assert.deepEqual(actual.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() })), expected.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() }))); + } + +}); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index e5f4c60be7..e62499294d 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -44,7 +44,7 @@ class TestSynchroniser extends AbstractSynchroniser { await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } }); } - stop(): void { + async stop(): Promise { this.cancelled = true; this.syncBarrier.open(); } diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index cb445f0837..a4d310364a 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -53,6 +53,7 @@ export class UserDataSyncClient extends Disposable { userDataSyncHome, settingsResource: joinPath(userDataDirectory, 'settings.json'), keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'), + snippetsHome: joinPath(userDataDirectory, 'snippets'), argvResource: joinPath(userDataDirectory, 'argv.json'), args: {} }); @@ -108,6 +109,7 @@ export class UserDataSyncClient extends Disposable { if (!empty) { await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({}))); await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([]))); + await fileService.writeFile(joinPath(environmentService.snippetsHome, 'c.json'), VSBuffer.fromString(`{}`)); await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' }))); } await configurationService.reloadConfiguration(); @@ -201,16 +203,13 @@ export class UserDataSyncTestServer implements IRequestService { } private async writeData(resource: string, content: string = '', headers: IHeaders = {}): Promise { - if (!headers['If-Match']) { - return this.toResponse(428); - } if (!this.session) { this.session = generateUuid(); } const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource); if (resourceKey) { const data = this.data.get(resourceKey); - if (headers['If-Match'] !== (data ? data.ref : '0')) { + if (headers['If-Match'] !== undefined && headers['If-Match'] !== (data ? data.ref : '0')) { return this.toResponse(412); } const ref = `${parseInt(data?.ref || '0') + 1}`; diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index 14db085753..792028dfed 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -10,6 +10,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IFileService } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { VSBuffer } from 'vs/base/common/buffer'; +import { joinPath } from 'vs/base/common/resources'; suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing tests @@ -36,6 +37,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, @@ -65,6 +69,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, @@ -102,6 +109,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, // Extensions @@ -140,6 +149,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, // Extensions @@ -174,6 +185,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, // Extensions @@ -198,6 +211,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`)); const testObject = testClient.instantiationService.get(IUserDataSyncService); // Sync (merge) from the test client @@ -215,6 +229,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, @@ -258,6 +275,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te const environmentService = client.instantiationService.get(IEnvironmentService); await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`)); await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); // Sync from the client @@ -270,6 +288,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } }, // Keybindings { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, + // Snippets + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } }, // Global state { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, ]); @@ -294,6 +314,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te const environmentService = client.instantiationService.get(IEnvironmentService); await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{ "a": "changed" }`)); await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); await client.instantiationService.get(IUserDataSyncService).sync(); @@ -308,6 +329,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: { 'If-None-Match': '1' } }, // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: { 'If-None-Match': '1' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: { 'If-None-Match': '1' } }, ]); @@ -359,6 +382,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, @@ -454,7 +480,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te await testObject.sync(); disposable.dispose(); - assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]); + assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]); }); test('test sync conflicts status', async () => { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 29ce7aed92..2f0e0155a8 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -6020,6 +6020,14 @@ declare module 'vscode' { * @param messageOrUri Message or uri. */ constructor(messageOrUri?: string | Uri); + + /** + * A code that identifies this error. + * + * Possible values are names of errors, like [`FileNotFound`](#FileSystemError.FileNotFound), + * or `Unknown` for unspecified errors. + */ + readonly code: string; } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 392694ea72..50ee5ad39c 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1635,6 +1635,10 @@ declare module 'vscode' { export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; + export interface NotebookCellMetadata { + editable: boolean; + } + export interface NotebookCell { readonly uri: Uri; handle: number; @@ -1642,6 +1646,11 @@ declare module 'vscode' { cellKind: CellKind; outputs: CellOutput[]; getContent(): string; + metadata?: NotebookCellMetadata; + } + + export interface NotebookDocumentMetadata { + editable: boolean; } export interface NotebookDocument { @@ -1651,15 +1660,29 @@ declare module 'vscode' { languages: string[]; cells: NotebookCell[]; displayOrder?: GlobPattern[]; + metadata?: NotebookDocumentMetadata; } export interface NotebookEditor { readonly document: NotebookDocument; viewColumn?: ViewColumn; + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable; + /** * Create a notebook cell. The cell is not inserted into current document when created. Extensions should insert the cell into the document by [TextDocument.cells](#TextDocument.cells) */ - createCell(content: string, language: string, type: CellKind, outputs: CellOutput[]): NotebookCell; + createCell(content: string, language: string, type: CellKind, outputs: CellOutput[], metadata: NotebookCellMetadata): NotebookCell; } export interface NotebookProvider { @@ -2023,21 +2046,6 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/90517 - - export interface FileSystemError { - /** - * A code that identifies this error. - * - * Possible values are names of errors, like [`FileNotFound`](#FileSystemError.FileNotFound), - * or `Unknown` for an unspecified error. - */ - readonly code: string; - } - - //#endregion - - //#region https://github.com/microsoft/vscode/issues/90208 export namespace Uri { @@ -2053,4 +2061,13 @@ declare module 'vscode' { //#endregion + //#region https://github.com/microsoft/vscode/issues/91541 + + export enum CompletionItemKind { + User = 25, + Issue = 26, + } + + //#endregion + } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index e022a0503a..4814526ddb 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -8,10 +8,12 @@ import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IEx import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; -import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; export class MainThreadNotebookDocument extends Disposable { private _textModel: NotebookTextModel; @@ -54,7 +56,9 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo constructor( extHostContext: IExtHostContext, @INotebookService private _notebookService: INotebookService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEditorService private readonly editorService: IEditorService, + ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); @@ -123,6 +127,14 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } } + async $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata | undefined): Promise { + let controller = this._notebookProviders.get(viewType); + + if (controller) { + controller.updateNotebookMetadata(resource, metadata); + } + } + async resolveNotebook(viewType: string, uri: URI): Promise { let handle = await this._proxy.$resolveNotebook(viewType, uri); return handle; @@ -141,6 +153,21 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo async executeNotebook(viewType: string, uri: URI): Promise { return this._proxy.$executeNotebook(viewType, uri, undefined); } + + async $postMessage(handle: number, value: any): Promise { + + const activeEditorPane = this.editorService.activeEditorPane as any | undefined; + if (activeEditorPane?.isNotebookEditor) { + const notebookEditor = (activeEditorPane as INotebookEditor); + + if (notebookEditor.viewModel?.handle === handle) { + notebookEditor.postMessage(value); + return true; + } + } + + return false; + } } export class MainThreadNotebookController implements IMainNotebookController { @@ -186,6 +213,10 @@ export class MainThreadNotebookController implements IMainNotebookController { this._mainThreadNotebook.executeNotebook(viewType, uri); } + onDidReceiveMessage(uri: UriComponents, message: any): void { + this._proxy.$onDidReceiveMessage(uri, message); + } + // Methods for ExtHost async createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource)); @@ -197,6 +228,11 @@ export class MainThreadNotebookController implements IMainNotebookController { document?.textModel.updateLanguages(languages); } + updateNotebookMetadata(resource: UriComponents, metadata: NotebookDocumentMetadata | undefined) { + let document = this._mapping.get(URI.from(resource).toString()); + document?.textModel.updateNotebookMetadata(metadata); + } + updateNotebookRenderers(resource: UriComponents, renderers: number[]): void { let document = this._mapping.get(URI.from(resource).toString()); document?.textModel.updateRenderers(renderers); @@ -227,11 +263,11 @@ export class MainThreadNotebookController implements IMainNotebookController { return false; } - executeNotebookActiveCell(uri: URI): void { + async executeNotebookActiveCell(uri: URI): Promise { let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); if (mainthreadNotebook && mainthreadNotebook.textModel.activeCell) { - this._proxy.$executeNotebook(this._viewType, uri, mainthreadNotebook.textModel.activeCell.handle); + return this._proxy.$executeNotebook(this._viewType, uri, mainthreadNotebook.textModel.activeCell.handle); } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 52b32ca05e..0169c60731 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -132,7 +132,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol)); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostDocumentsAndEditors)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c0dc4c525f..70559ac721 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; @@ -669,6 +669,7 @@ export interface ICellDto { language: string; cellKind: CellKind; outputs: IOutput[]; + metadata?: NotebookCellMetadata; } export type NotebookCellsSplice = [ @@ -690,8 +691,10 @@ export interface MainThreadNotebookShape extends IDisposable { $unregisterNotebookRenderer(handle: number): Promise; $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; + $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata | undefined): Promise; $spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise; $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise; + $postMessage(handle: number, value: any): Promise; } export interface MainThreadUrlsShape extends IDisposable { @@ -1531,6 +1534,7 @@ export interface ExtHostNotebookShape { $updateActiveEditor(viewType: string, uri: UriComponents): Promise; $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; + $onDidReceiveMessage(uri: UriComponents, message: any): void; } export interface ExtHostStorageShape { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 5d632229ff..bf4d9982f2 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -14,6 +14,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { INotebookDisplayOrder, ITransformedDisplayOutputDto, IOrderedMimeType, IStreamOutput, IErrorOutput, mimeTypeSupportedByCore, IOutput, sortMimeTypes, diff, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ISplice } from 'vs/base/common/sequence'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; export class ExtHostCell implements vscode.NotebookCell { @@ -31,7 +32,8 @@ export class ExtHostCell implements vscode.NotebookCell { private _content: string, public cellKind: CellKind, public language: string, - outputs: any[] + outputs: any[], + public metadata: vscode.NotebookCellMetadata | undefined, ) { this.source = this._content.split(/\r|\n|\r\n/g); this._outputs = outputs; @@ -129,6 +131,17 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); } + private _metadata: vscode.NotebookDocumentMetadata | undefined = undefined; + + get metadata() { + return this._metadata; + } + + set metadata(newMetadata: vscode.NotebookDocumentMetadata | undefined) { + this._metadata = newMetadata; + this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata); + } + private _displayOrder: string[] = []; get displayOrder() { @@ -330,11 +343,14 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor { private _viewColumn: vscode.ViewColumn | undefined; private static _cellhandlePool: number = 0; + onDidReceiveMessage: vscode.Event = this._onDidReceiveMessage.event; constructor( viewType: string, readonly id: string, public uri: URI, + private _proxy: MainThreadNotebookShape, + private _onDidReceiveMessage: Emitter, public document: ExtHostNotebookDocument, private _documentsAndEditors: ExtHostDocumentsAndEditors ) { @@ -362,10 +378,10 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook })); } - createCell(content: string, language: string, type: CellKind, outputs: vscode.CellOutput[]): vscode.NotebookCell { + createCell(content: string, language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): vscode.NotebookCell { const handle = ExtHostNotebookEditor._cellhandlePool++; const uri = CellUri.generate(this.document.uri, handle); - const cell = new ExtHostCell(handle, uri, content, type, language, outputs); + const cell = new ExtHostCell(handle, uri, content, type, language, outputs, metadata); return cell; } @@ -376,6 +392,11 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook set viewColumn(value) { throw readonly('viewColumn'); } + + async postMessage(message: any): Promise { + return this._proxy.$postMessage(this.document.handle, message); + } + } export class ExtHostNotebookOutputRenderer { @@ -415,9 +436,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private static _handlePool: number = 0; private readonly _proxy: MainThreadNotebookShape; - private readonly _notebookProviders = new Map(); + private readonly _notebookProviders = new Map(); private readonly _documents = new Map(); - private readonly _editors = new Map(); + private readonly _editors = new Map }>(); private readonly _notebookOutputRenderers = new Map(); private _outputDisplayOrder: INotebookDisplayOrder | undefined; @@ -431,8 +452,28 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return this._activeNotebookDocument; } - constructor(mainContext: IMainContext, private _documentsAndEditors: ExtHostDocumentsAndEditors) { + constructor(mainContext: IMainContext, commands: ExtHostCommands, private _documentsAndEditors: ExtHostDocumentsAndEditors) { this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); + + commands.registerArgumentProcessor({ + processArgument: arg => { + if (arg && arg.$mid === 12) { + const documentHandle = arg.notebookEditor?.notebookHandle; + const cellHandle = arg.cell.handle; + + for (let value of this._editors) { + if (value[1].editor.document.handle === documentHandle) { + const cell = value[1].editor.document.getCell(cellHandle); + if (cell) { + return cell; + } + } + } + + return arg; + } + } + }); } registerNotebookOutputRenderer( @@ -494,15 +535,19 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._documents.set(URI.revive(uri).toString(), document); } + const onDidReceiveMessage = new Emitter(); + let editor = new ExtHostNotebookEditor( viewType, `${ExtHostNotebookController._handlePool++}`, URI.revive(uri), + this._proxy, + onDidReceiveMessage, this._documents.get(URI.revive(uri).toString())!, this._documentsAndEditors ); - this._editors.set(URI.revive(uri).toString(), editor); + this._editors.set(URI.revive(uri).toString(), { editor, onDidReceiveMessage }); await provider.provider.resolveNotebook(editor); // await editor.document.$updateCells(); return editor.document.handle; @@ -535,7 +580,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN let editor = this._editors.get(URI.revive(uri).toString()); let document = this._documents.get(URI.revive(uri).toString()); - let rawCell = editor?.createCell('', language, type, []) as ExtHostCell; + let rawCell = editor?.editor.createCell('', language, type, [], undefined) as ExtHostCell; document?.insertCell(index, rawCell!); let allDocuments = this._documentsAndEditors.allDocuments(); @@ -553,6 +598,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN source: rawCell.source, language: rawCell.language, cellKind: rawCell.cellKind, + metadata: rawCell.metadata, outputs: [] }; } @@ -608,7 +654,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN let editor = this._editors.get(URI.revive(uri).toString()); if (editor) { - editor.dispose(); + editor.editor.dispose(); + editor.onDidReceiveMessage.dispose(); this._editors.delete(URI.revive(uri).toString()); } @@ -618,4 +665,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void { this._outputDisplayOrder = displayOrder; } + + $onDidReceiveMessage(uri: UriComponents, message: any): void { + let editor = this._editors.get(URI.revive(uri).toString()); + + if (editor) { + editor.onDidReceiveMessage.fire(message); + } + } } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 1fcc46f4d9..9344049bea 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -835,6 +835,8 @@ export namespace CompletionItemKind { case types.CompletionItemKind.Event: return modes.CompletionItemKind.Event; case types.CompletionItemKind.Operator: return modes.CompletionItemKind.Operator; case types.CompletionItemKind.TypeParameter: return modes.CompletionItemKind.TypeParameter; + case types.CompletionItemKind.Issue: return modes.CompletionItemKind.Issue; + case types.CompletionItemKind.User: return modes.CompletionItemKind.User; } return modes.CompletionItemKind.Property; } @@ -866,6 +868,8 @@ export namespace CompletionItemKind { case modes.CompletionItemKind.Event: return types.CompletionItemKind.Event; case modes.CompletionItemKind.Operator: return types.CompletionItemKind.Operator; case modes.CompletionItemKind.TypeParameter: return types.CompletionItemKind.TypeParameter; + case modes.CompletionItemKind.User: return types.CompletionItemKind.User; + case modes.CompletionItemKind.Issue: return types.CompletionItemKind.Issue; } return types.CompletionItemKind.Property; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 0bae7255a1..2669cec22f 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1348,7 +1348,9 @@ export enum CompletionItemKind { Struct = 21, Event = 22, Operator = 23, - TypeParameter = 24 + TypeParameter = 24, + User = 25, + Issue = 26 } export enum CompletionItemTag { diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index a66729d3fc..bd660bd099 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -56,6 +56,7 @@ namespace schema { case 'comments/commentThread/context': return MenuId.CommentThreadActions; case 'comments/comment/title': return MenuId.CommentTitle; case 'comments/comment/context': return MenuId.CommentActions; + case 'notebook/cell/title': return MenuId.NotebookCellTitle; case 'extension/context': return MenuId.ExtensionContext; case 'timeline/title': return MenuId.TimelineTitle; case 'timeline/item/context': return MenuId.TimelineItemContext; @@ -217,6 +218,11 @@ namespace schema { type: 'array', items: menuItem }, + 'notebook/cell/title': { + description: localize('notebook.cell.title', "The contributed notebook cell title menu"), + type: 'array', + items: menuItem + }, 'extension/context': { description: localize('menus.extensionContext', "The extension context menu"), type: 'array', diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 407381d7eb..88ed68e246 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -16,7 +16,7 @@ import { IsFullscreenContext } from 'vs/workbench/browser/contextkeys'; import { IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IQuickInputButton, IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -27,7 +27,6 @@ import { URI } from 'vs/base/common/uri'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { FileKind } from 'vs/platform/files/common/files'; import { splitName } from 'vs/base/common/labels'; -import { IKeyMods } from 'vs/base/parts/quickopen/common/quickOpen'; import { isMacintosh } from 'vs/base/common/platform'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { inQuickOpenContext, getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index ad3f7bfa3d..7828280cfd 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -8,16 +8,20 @@ overflow: hidden; } + +.monaco-workbench .part > .drop-block-overlay.visible { + display: block; + backdrop-filter: brightness(97%) blur(2px); + opacity: 1; + z-index: 10; +} + .monaco-workbench .part > .drop-block-overlay { - visibility: hidden; /* use visibility to ensure transitions */ - transition-property: opacity; - transition-timing-function: linear; - transition-duration: 250ms; + display: none; width: 100%; height: 100%; position: absolute; top: 0; - opacity: 0; pointer-events: none; } diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index fb26f74550..e02a939722 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -7,12 +7,6 @@ width: 48px; } -.monaco-workbench .part > .drop-block-overlay.visible { - visibility: visible; - backdrop-filter: brightness(97%) blur(2px); - opacity: 1; -} - .monaco-workbench .activitybar > .content { height: 100%; display: flex; diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index 29955909f9..a27214e80c 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -26,7 +26,7 @@ import { EditorInput, IWorkbenchEditorConfiguration, IEditorInput } from 'vs/wor import { Component } from 'vs/workbench/common/component'; import { Event, Emitter } from 'vs/base/common/event'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { QuickOpenHandler, QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions, EditorQuickOpenEntry, CLOSE_ON_FOCUS_LOST_CONFIG, SEARCH_EDITOR_HISTORY, PRESERVE_INPUT_CONFIG } from 'vs/workbench/browser/quickopen'; +import { QuickOpenHandler, QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions, EditorQuickOpenEntry, CLOSE_ON_FOCUS_LOST_CONFIG, SEARCH_EDITOR_HISTORY, PRESERVE_INPUT_CONFIG, ENABLE_EXPERIMENTAL_VERSION_CONFIG } from 'vs/workbench/browser/quickopen'; import * as errors from 'vs/base/common/errors'; import { IQuickOpenService, IShowOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -86,6 +86,10 @@ export class QuickOpenController extends Component implements IQuickOpenService private editorHistoryHandler: EditorHistoryHandler; private pendingGetResultsInvocation: CancellationTokenSource | null = null; + private get useNewExperimentalVersion() { + return this.configurationService.getValue(ENABLE_EXPERIMENTAL_VERSION_CONFIG) === true; + } + constructor( @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @INotificationService private readonly notificationService: INotificationService, @@ -95,7 +99,8 @@ export class QuickOpenController extends Component implements IQuickOpenService @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(QuickOpenController.ID, themeService, storageService); @@ -125,26 +130,42 @@ export class QuickOpenController extends Component implements IQuickOpenService } navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void { - if (this.quickOpenWidget) { - this.quickOpenWidget.navigate(next, quickNavigate); + if (this.useNewExperimentalVersion) { + // already handled + } else { + if (this.quickOpenWidget) { + this.quickOpenWidget.navigate(next, quickNavigate); + } } } accept(): void { - if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { - this.quickOpenWidget.accept(); + if (this.useNewExperimentalVersion) { + // already handled + } else { + if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { + this.quickOpenWidget.accept(); + } } } focus(): void { - if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { - this.quickOpenWidget.focus(); + if (this.useNewExperimentalVersion) { + // already handled + } else { + if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { + this.quickOpenWidget.focus(); + } } } close(): void { - if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { - this.quickOpenWidget.hide(HideReason.CANCELED); + if (this.useNewExperimentalVersion) { + // already handled + } else { + if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { + this.quickOpenWidget.hide(HideReason.CANCELED); + } } } @@ -157,6 +178,12 @@ export class QuickOpenController extends Component implements IQuickOpenService } show(prefix?: string, options?: IShowOptions): Promise { + if (this.useNewExperimentalVersion) { + this.quickInputService.quickAccess.show(prefix, options); + + return Promise.resolve(); + } + let quickNavigateConfiguration = options ? options.quickNavigateConfiguration : undefined; let inputSelection = options ? options.inputSelection : undefined; let autoFocus = options ? options.autoFocus : undefined; diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index ec56a89b4c..162f94bc58 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -336,7 +336,7 @@ export abstract class ViewPane extends Pane implements IView { } if (this.progressIndicator === undefined) { - this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isVisible()); + this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isBodyVisible()); } return this.progressIndicator; } diff --git a/src/vs/workbench/browser/quickopen.ts b/src/vs/workbench/browser/quickopen.ts index 8008b630cf..0ffb626688 100644 --- a/src/vs/workbench/browser/quickopen.ts +++ b/src/vs/workbench/browser/quickopen.ts @@ -21,6 +21,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; export const CLOSE_ON_FOCUS_LOST_CONFIG = 'workbench.quickOpen.closeOnFocusLost'; export const PRESERVE_INPUT_CONFIG = 'workbench.quickOpen.preserveInput'; +export const ENABLE_EXPERIMENTAL_VERSION_CONFIG = 'workbench.quickOpen.enableExperimentalNewVersion'; export const SEARCH_EDITOR_HISTORY = 'search.quickOpen.includeHistory'; export interface IWorkbenchQuickOpenConfiguration { @@ -28,6 +29,9 @@ export interface IWorkbenchQuickOpenConfiguration { commandPalette: { history: number; preserveInput: boolean; + }, + quickOpen: { + enableExperimentalNewVersion: boolean; } }; } diff --git a/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index f01c05da08..a998c4340f 100644 --- a/src/vs/workbench/browser/style.ts +++ b/src/vs/workbench/browser/style.ts @@ -14,18 +14,22 @@ import { isSafari, isStandalone } from 'vs/base/browser/browser'; registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - // Icon defaults - const iconForegroundColor = theme.getColor(iconForeground); - if (iconForegroundColor) { - collector.addRule(`.monaco-workbench .codicon { color: ${iconForegroundColor}; }`); - } - // Foreground const windowForeground = theme.getColor(foreground); if (windowForeground) { collector.addRule(`.monaco-workbench { color: ${windowForeground}; }`); } + // Background (We need to set the workbench background color so that on Windows we get subpixel-antialiasing) + const workbenchBackground = WORKBENCH_BACKGROUND(theme); + collector.addRule(`.monaco-workbench { background-color: ${workbenchBackground}; }`); + + // Icon defaults + const iconForegroundColor = theme.getColor(iconForeground); + if (iconForegroundColor) { + collector.addRule(`.monaco-workbench .codicon { color: ${iconForegroundColor}; }`); + } + // Selection const windowSelectionBackground = theme.getColor(selectionBackground); if (windowSelectionBackground) { @@ -58,10 +62,6 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = `); } - // We need to set the workbench background color so that on Windows we get subpixel-antialiasing. - const workbenchBackground = WORKBENCH_BACKGROUND(theme); - collector.addRule(`.monaco-workbench { background-color: ${workbenchBackground}; }`); - // Scrollbars const scrollbarShadowColor = theme.getColor(scrollbarShadow); if (scrollbarShadowColor) { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 2380b35282..224bfe30e3 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -179,6 +179,11 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'description': nls.localize('workbench.quickOpen.preserveInput', "Controls whether the last typed input to Quick Open should be restored when opening it the next time."), 'default': false }, + 'workbench.quickOpen.enableExperimentalNewVersion': { + 'type': 'boolean', + 'description': nls.localize('workbench.quickOpen.enableExperimentalNewVersion', "Will use the new quick open implementation for testing purposes."), + 'default': false + }, 'workbench.settings.openDefaultSettings': { 'type': 'boolean', 'description': nls.localize('openDefaultSettings', "Controls whether opening settings also opens an editor showing all default settings."), diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 71c0d62c4b..bd095780ea 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -323,6 +323,9 @@ export class Workbench extends Layout { private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void { + // ARIA + this.container.setAttribute('role', 'application'); + // State specific classes const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac'; const workbenchClasses = coalesce([ @@ -335,7 +338,6 @@ export class Workbench extends Layout { addClasses(this.container, ...workbenchClasses); addClass(document.body, platformClass); // used by our fonts - this.container.setAttribute('role', 'application'); if (isWeb) { addClass(document.body, 'web'); diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index c27d2fc6cc..802c00988c 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -37,19 +37,19 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv return this.editorService.activeTextEditorControl; } - protected gotoLocation(editor: IEditor, range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean): void { + protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean }): void { // Check for sideBySide use - if ((keyMods.ctrlCmd || forceSideBySide) && this.editorService.activeEditor) { + if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { this.editorService.openEditor(this.editorService.activeEditor, { - selection: range, - pinned: keyMods.alt || this.configuration.openEditorPinned + selection: options.range, + pinned: options.keyMods.alt || this.configuration.openEditorPinned }, SIDE_GROUP); } // Otherwise let parent handle it else { - super.gotoLocation(editor, range, keyMods); + super.gotoLocation(editor, options); } } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 8eed2bd300..f1912bb2d4 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -40,19 +40,19 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return this.editorService.activeTextEditorControl; } - protected gotoLocation(editor: IEditor, range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean): void { + protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean }): void { // Check for sideBySide use - if ((keyMods.ctrlCmd || forceSideBySide) && this.editorService.activeEditor) { + if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { this.editorService.openEditor(this.editorService.activeEditor, { - selection: range, - pinned: keyMods.alt || this.configuration.openEditorPinned + selection: options.range, + pinned: options.keyMods.alt || this.configuration.openEditorPinned }, SIDE_GROUP); } // Otherwise let parent handle it else { - super.gotoLocation(editor, range, keyMods); + super.gotoLocation(editor, options); } } } diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index f8e28c2e73..4a6559d3df 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -162,6 +162,15 @@ export class ConfigurationManager implements IConfigurationManager { return Promise.resolve(undefined); } + getDebuggerLabel(session: IDebugSession): string | undefined { + const dbgr = this.getDebugger(session.configuration.type); + if (dbgr) { + return dbgr.label; + } + + return undefined; + } + get onDidRegisterDebugger(): Event { return this._onDidRegisterDebugger.event; } diff --git a/src/vs/workbench/contrib/debug/browser/debugProgress.ts b/src/vs/workbench/contrib/debug/browser/debugProgress.ts index 6e398c1b8a..626676f460 100644 --- a/src/vs/workbench/contrib/debug/browser/debugProgress.ts +++ b/src/vs/workbench/contrib/debug/browser/debugProgress.ts @@ -34,12 +34,31 @@ export class DebugProgressContribution implements IWorkbenchContribution { }); this.progressService.withProgress({ location: VIEWLET_ID }, () => promise); + const source = this.debugService.getConfigurationManager().getDebuggerLabel(session); this.progressService.withProgress({ location: ProgressLocation.Notification, title: progressStartEvent.body.title, cancellable: progressStartEvent.body.cancellable, - silent: true - }, () => promise, () => session.cancel(progressStartEvent.body.progressId)); + silent: true, + source, + delay: 500 + }, progressStep => { + let increment = 0; + const progressUpdateListener = session.onDidProgressUpdate(e => { + if (e.body.progressId === progressStartEvent.body.progressId) { + if (typeof e.body.percentage === 'number') { + increment = e.body.percentage - increment; + } + progressStep.report({ + message: e.body.message, + increment: typeof e.body.percentage === 'number' ? increment : undefined, + total: typeof e.body.percentage === 'number' ? 100 : undefined, + }); + } + }); + + return promise.then(() => progressUpdateListener.dispose()); + }, () => session.cancel(progressStartEvent.body.progressId)); }); } }; diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 2a36496dee..b883a6e8f8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -55,6 +55,7 @@ export class DebugSession implements IDebugSession { private readonly _onDidLoadedSource = new Emitter(); private readonly _onDidCustomEvent = new Emitter(); private readonly _onDidProgressStart = new Emitter(); + private readonly _onDidProgressUpdate = new Emitter(); private readonly _onDidProgressEnd = new Emitter(); private readonly _onDidChangeREPLElements = new Emitter(); @@ -190,6 +191,10 @@ export class DebugSession implements IDebugSession { return this._onDidProgressStart.event; } + get onDidProgressUpdate(): Event { + return this._onDidProgressUpdate.event; + } + get onDidProgressEnd(): Event { return this._onDidProgressEnd.event; } @@ -935,6 +940,9 @@ export class DebugSession implements IDebugSession { this.rawListeners.push(this.raw.onDidProgressStart(event => { this._onDidProgressStart.fire(event); })); + this.rawListeners.push(this.raw.onDidProgressUpdate(event => { + this._onDidProgressUpdate.fire(event); + })); this.rawListeners.push(this.raw.onDidProgressEnd(event => { this._onDidProgressEnd.fire(event); })); diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 267755665f..41044f466b 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -66,6 +66,7 @@ export class RawDebugSession implements IDisposable { private readonly _onDidBreakpoint = new Emitter(); private readonly _onDidLoadedSource = new Emitter(); private readonly _onDidProgressStart = new Emitter(); + private readonly _onDidProgressUpdate = new Emitter(); private readonly _onDidProgressEnd = new Emitter(); private readonly _onDidCustomEvent = new Emitter(); private readonly _onDidEvent = new Emitter(); @@ -142,6 +143,9 @@ export class RawDebugSession implements IDisposable { case 'progressStart': this._onDidProgressStart.fire(event as DebugProtocol.ProgressStartEvent); break; + case 'progressUpdate': + this._onDidProgressUpdate.fire(event as DebugProtocol.ProgressUpdateEvent); + break; case 'progressEnd': this._onDidProgressEnd.fire(event as DebugProtocol.ProgressEndEvent); break; @@ -217,6 +221,10 @@ export class RawDebugSession implements IDisposable { return this._onDidProgressStart.event; } + get onDidProgressUpdate(): Event { + return this._onDidProgressUpdate.event; + } + get onDidProgressEnd(): Event { return this._onDidProgressEnd.event; } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 011b2f683f..fadaacd006 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -200,6 +200,7 @@ export interface IDebugSession extends ITreeElement { readonly onDidLoadedSource: Event; readonly onDidCustomEvent: Event; readonly onDidProgressStart: Event; + readonly onDidProgressUpdate: Event; readonly onDidProgressEnd: Event; // DAP request @@ -662,6 +663,7 @@ export interface IConfigurationManager { resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: any, token: CancellationToken): Promise; getDebugAdapterDescriptor(session: IDebugSession): Promise; + getDebuggerLabel(session: IDebugSession): string | undefined; registerDebugAdapterFactory(debugTypes: string[], debugAdapterFactory: IDebugAdapterFactory): IDisposable; createDebugAdapter(session: IDebugSession): IDebugAdapter | undefined; diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index e01ea9ae01..cb119a6755 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - + /** Declaration module describing the VS Code debug protocol. Auto-generated from json schema. Do not edit manually. */ @@ -72,12 +72,15 @@ declare module DebugProtocol { /** Cancel request; value of command field is 'cancel'. The 'cancel' request is used by the frontend in two situations: - to indicate that it is no longer interested in the result produced by a specific request issued earlier - - to cancel a progress sequence. + - to cancel a progress sequence. Clients should only call this request if the capability 'supportsCancelRequest' is true. This request has a hint characteristic: a debug adapter can only be expected to make a 'best effort' in honouring this request but there are no guarantees. The 'cancel' request may return an error if it could not cancel an operation but a frontend should refrain from presenting this error to end users. A frontend client should only call this request if the capability 'supportsCancelRequest' is true. - The request that got canceled still needs to send a response back. This can either be a normal result ('success' attribute true) or an error response ('success' attribute false and the 'message' set to 'cancelled'). Returning partial results from a cancelled request is possible but please note that a frontend client has no generic way for detecting that a response is partial or not. - The progress that got cancelled still needs to send a 'progressEnd' event back. A client should not assume that progress just got cancelled after sending the 'cancel' request. + The request that got canceled still needs to send a response back. This can either be a normal result ('success' attribute true) + or an error response ('success' attribute false and the 'message' set to 'cancelled'). + Returning partial results from a cancelled request is possible but please note that a frontend client has no generic way for detecting that a response is partial or not. + The progress that got cancelled still needs to send a 'progressEnd' event back. + A client should not assume that progress just got cancelled after sending the 'cancel' request. */ export interface CancelRequest extends Request { // command: 'cancel'; @@ -86,9 +89,13 @@ declare module DebugProtocol { /** Arguments for 'cancel' request. */ export interface CancelArguments { - /** The ID (attribute 'seq') of the request to cancel. If missing no request is cancelled. Both a 'requestId' and a 'progressId' can be specified in one request. */ + /** The ID (attribute 'seq') of the request to cancel. If missing no request is cancelled. + Both a 'requestId' and a 'progressId' can be specified in one request. + */ requestId?: number; - /** The ID (attribute 'progressId') of the progress to cancel. If missing no progress is cancelled. Both a 'requestId' and a 'progressId' can be specified in one request. */ + /** The ID (attribute 'progressId') of the progress to cancel. If missing no progress is cancelled. + Both a 'requestId' and a 'progressId' can be specified in one request. + */ progressId?: string; } @@ -309,11 +316,14 @@ declare module DebugProtocol { The event signals that a long running operation is about to start and provides additional information for the client to set up a corresponding progress and cancellation UI. The client is free to delay the showing of the UI in order to reduce flicker. + This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ export interface ProgressStartEvent extends Event { // event: 'progressStart'; body: { - /** An ID that must be used in subsequent 'progressUpdate' and 'progressEnd' events to make them refer to the same progress reporting. IDs must be unique within a debug session. */ + /** An ID that must be used in subsequent 'progressUpdate' and 'progressEnd' events to make them refer to the same progress reporting. + IDs must be unique within a debug session. + */ progressId: string; /** Mandatory (short) title of the progress reporting. Shown in the UI to describe the long running operation. */ title: string; @@ -337,6 +347,7 @@ declare module DebugProtocol { /** Event message for 'progressUpdate' event type. The event signals that the progress reporting needs to updated with a new message and/or percentage. The client does not have to update the UI immediately, but the clients needs to keep track of the message and/or percentage values. + This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ export interface ProgressUpdateEvent extends Event { // event: 'progressUpdate'; @@ -352,6 +363,7 @@ declare module DebugProtocol { /** Event message for 'progressEnd' event type. The event signals the end of the progress reporting with an optional final message. + This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ export interface ProgressEndEvent extends Event { // event: 'progressEnd'; @@ -364,7 +376,9 @@ declare module DebugProtocol { } /** RunInTerminal request; value of command field is 'runInTerminal'. - This request is sent from the debug adapter to the client to run a command in a terminal. This is typically used to launch the debuggee in a terminal provided by the client. + This optional request is sent from the debug adapter to the client to run a command in a terminal. + This is typically used to launch the debuggee in a terminal provided by the client. + This request should only be called if the client has passed the value true for the 'supportsRunInTerminalRequest' capability of the 'initialize' request. */ export interface RunInTerminalRequest extends Request { // command: 'runInTerminal'; @@ -396,8 +410,10 @@ declare module DebugProtocol { } /** Initialize request; value of command field is 'initialize'. - The 'initialize' request is sent as the first request from the client to the debug adapter in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. - Until the debug adapter has responded to with an 'initialize' response, the client must not send any additional requests or events to the debug adapter. In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an 'initialize' response. + The 'initialize' request is sent as the first request from the client to the debug adapter + in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. + Until the debug adapter has responded to with an 'initialize' response, the client must not send any additional requests or events to the debug adapter. + In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an 'initialize' response. The 'initialize' request may only be sent once. */ export interface InitializeRequest extends Request { @@ -442,7 +458,9 @@ declare module DebugProtocol { } /** ConfigurationDone request; value of command field is 'configurationDone'. - The client of the debug protocol must send this request at the end of the sequence of configuration requests (which was started by the 'initialized' event). + This optional request indicates that the client has finished initialization of the debug adapter. + So it is the last request in the sequence of configuration requests (which was started by the 'initialized' event). + Clients should only call this request if the capability 'supportsConfigurationDoneRequest' is true. */ export interface ConfigurationDoneRequest extends Request { // command: 'configurationDone'; @@ -458,7 +476,8 @@ declare module DebugProtocol { } /** Launch request; value of command field is 'launch'. - The launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if 'noDebug' is true). Since launching is debugger/runtime specific, the arguments for this request are not part of this specification. + This launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if 'noDebug' is true). + Since launching is debugger/runtime specific, the arguments for this request are not part of this specification. */ export interface LaunchRequest extends Request { // command: 'launch'; @@ -481,7 +500,8 @@ declare module DebugProtocol { } /** Attach request; value of command field is 'attach'. - The attach request is sent from the client to the debug adapter to attach to a debuggee that is already running. Since attaching is debugger/runtime specific, the arguments for this request are not part of this specification. + The attach request is sent from the client to the debug adapter to attach to a debuggee that is already running. + Since attaching is debugger/runtime specific, the arguments for this request are not part of this specification. */ export interface AttachRequest extends Request { // command: 'attach'; @@ -502,10 +522,8 @@ declare module DebugProtocol { } /** Restart request; value of command field is 'restart'. - Restarts a debug session. If the capability 'supportsRestartRequest' is missing or has the value false, - the client will implement 'restart' by terminating the debug adapter first and then launching it anew. - A debug adapter can override this default behaviour by implementing a restart request - and setting the capability 'supportsRestartRequest' to true. + Restarts a debug session. Clients should only call this request if the capability 'supportsRestartRequest' is true. + If the capability is missing or has the value false, a typical client will emulate 'restart' by terminating the debug adapter first and then launching it anew. */ export interface RestartRequest extends Request { // command: 'restart'; @@ -521,7 +539,11 @@ declare module DebugProtocol { } /** Disconnect request; value of command field is 'disconnect'. - The 'disconnect' request is sent from the client to the debug adapter in order to stop debugging. It asks the debug adapter to disconnect from the debuggee and to terminate the debug adapter. If the debuggee has been started with the 'launch' request, the 'disconnect' request terminates the debuggee. If the 'attach' request was used to connect to the debuggee, 'disconnect' does not terminate the debuggee. This behavior can be controlled with the 'terminateDebuggee' argument (if supported by the debug adapter). + The 'disconnect' request is sent from the client to the debug adapter in order to stop debugging. + It asks the debug adapter to disconnect from the debuggee and to terminate the debug adapter. + If the debuggee has been started with the 'launch' request, the 'disconnect' request terminates the debuggee. + If the 'attach' request was used to connect to the debuggee, 'disconnect' does not terminate the debuggee. + This behavior can be controlled with the 'terminateDebuggee' argument (if supported by the debug adapter). */ export interface DisconnectRequest extends Request { // command: 'disconnect'; @@ -534,7 +556,7 @@ declare module DebugProtocol { restart?: boolean; /** Indicates whether the debuggee should be terminated when the debugger is disconnected. If unspecified, the debug adapter is free to do whatever it thinks is best. - A client can only rely on this attribute being properly honored if a debug adapter returns true for the 'supportTerminateDebuggee' capability. + The attribute is only honored by a debug adapter if the capability 'supportTerminateDebuggee' is true. */ terminateDebuggee?: boolean; } @@ -545,6 +567,7 @@ declare module DebugProtocol { /** Terminate request; value of command field is 'terminate'. The 'terminate' request is sent from the client to the debug adapter in order to give the debuggee a chance for terminating itself. + Clients should only call this request if the capability 'supportsTerminateRequest' is true. */ export interface TerminateRequest extends Request { // command: 'terminate'; @@ -563,6 +586,7 @@ declare module DebugProtocol { /** BreakpointLocations request; value of command field is 'breakpointLocations'. The 'breakpointLocations' request returns all possible locations for source breakpoints in a given range. + Clients should only call this request if the capability 'supportsBreakpointLocationsRequest' is true. */ export interface BreakpointLocationsRequest extends Request { // command: 'breakpointLocations'; @@ -623,7 +647,9 @@ declare module DebugProtocol { */ export interface SetBreakpointsResponse extends Response { body: { - /** Information about the breakpoints. The array elements are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') array in the arguments. */ + /** Information about the breakpoints. + The array elements are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') array in the arguments. + */ breakpoints: Breakpoint[]; }; } @@ -632,6 +658,7 @@ declare module DebugProtocol { Replaces all existing function breakpoints with new function breakpoints. To clear all function breakpoints, specify an empty array. When a function breakpoint is hit, a 'stopped' event (with reason 'function breakpoint') is generated. + Clients should only call this request if the capability 'supportsFunctionBreakpoints' is true. */ export interface SetFunctionBreakpointsRequest extends Request { // command: 'setFunctionBreakpoints'; @@ -655,7 +682,9 @@ declare module DebugProtocol { } /** SetExceptionBreakpoints request; value of command field is 'setExceptionBreakpoints'. - The request configures the debuggers response to thrown exceptions. If an exception is configured to break, a 'stopped' event is fired (with reason 'exception'). + The request configures the debuggers response to thrown exceptions. + If an exception is configured to break, a 'stopped' event is fired (with reason 'exception'). + Clients should only call this request if the capability 'exceptionBreakpointFilters' returns one or more filters. */ export interface SetExceptionBreakpointsRequest extends Request { // command: 'setExceptionBreakpoints'; @@ -666,7 +695,9 @@ declare module DebugProtocol { export interface SetExceptionBreakpointsArguments { /** IDs of checked exception options. The set of IDs is returned via the 'exceptionBreakpointFilters' capability. */ filters: string[]; - /** Configuration options for selected exceptions. */ + /** Configuration options for selected exceptions. + The attribute is only honored by a debug adapter if the capability 'supportsExceptionOptions' is true. + */ exceptionOptions?: ExceptionOptions[]; } @@ -676,6 +707,7 @@ declare module DebugProtocol { /** DataBreakpointInfo request; value of command field is 'dataBreakpointInfo'. Obtains information on a possible data breakpoint that could be set on an expression or variable. + Clients should only call this request if the capability 'supportsDataBreakpoints' is true. */ export interface DataBreakpointInfoRequest extends Request { // command: 'dataBreakpointInfo'; @@ -686,7 +718,9 @@ declare module DebugProtocol { export interface DataBreakpointInfoArguments { /** Reference to the Variable container if the data breakpoint is requested for a child of the container. */ variablesReference?: number; - /** The name of the Variable's child to obtain data breakpoint information for. If variableReference isn’t provided, this can be an expression. */ + /** The name of the Variable's child to obtain data breakpoint information for. + If variableReference isn’t provided, this can be an expression. + */ name: string; } @@ -708,6 +742,7 @@ declare module DebugProtocol { Replaces all existing data breakpoints with new data breakpoints. To clear all data breakpoints, specify an empty array. When a data breakpoint is hit, a 'stopped' event (with reason 'data breakpoint') is generated. + Clients should only call this request if the capability 'supportsDataBreakpoints' is true. */ export interface SetDataBreakpointsRequest extends Request { // command: 'setDataBreakpoints'; @@ -740,14 +775,18 @@ declare module DebugProtocol { /** Arguments for 'continue' request. */ export interface ContinueArguments { - /** Continue execution for the specified thread (if possible). If the backend cannot continue on a single thread but will continue on all threads, it should set the 'allThreadsContinued' attribute in the response to true. */ + /** Continue execution for the specified thread (if possible). + If the backend cannot continue on a single thread but will continue on all threads, it should set the 'allThreadsContinued' attribute in the response to true. + */ threadId: number; } /** Response to 'continue' request. */ export interface ContinueResponse extends Response { body: { - /** If true, the 'continue' request has ignored the specified thread and continued all threads instead. If this attribute is missing a value of 'true' is assumed for backward compatibility. */ + /** If true, the 'continue' request has ignored the specified thread and continued all threads instead. + If this attribute is missing a value of 'true' is assumed for backward compatibility. + */ allThreadsContinued?: boolean; }; } @@ -817,7 +856,8 @@ declare module DebugProtocol { /** StepBack request; value of command field is 'stepBack'. The request starts the debuggee to run one step backwards. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. Clients should only call this request if the capability 'supportsStepBack' is true. + The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. + Clients should only call this request if the capability 'supportsStepBack' is true. */ export interface StepBackRequest extends Request { // command: 'stepBack'; @@ -835,7 +875,8 @@ declare module DebugProtocol { } /** ReverseContinue request; value of command field is 'reverseContinue'. - The request starts the debuggee to run backward. Clients should only call this request if the capability 'supportsStepBack' is true. + The request starts the debuggee to run backward. + Clients should only call this request if the capability 'supportsStepBack' is true. */ export interface ReverseContinueRequest extends Request { // command: 'reverseContinue'; @@ -855,6 +896,7 @@ declare module DebugProtocol { /** RestartFrame request; value of command field is 'restartFrame'. The request restarts execution of the specified stackframe. The debug adapter first sends the response and then a 'stopped' event (with reason 'restart') after the restart has completed. + Clients should only call this request if the capability 'supportsRestartFrame' is true. */ export interface RestartFrameRequest extends Request { // command: 'restartFrame'; @@ -876,6 +918,7 @@ declare module DebugProtocol { This makes it possible to skip the execution of code or to executed code again. The code between the current location and the goto target is not executed but skipped. The debug adapter first sends the response and then a 'stopped' event with reason 'goto'. + Clients should only call this request if the capability 'supportsGotoTargetsRequest' is true (because only then goto targets exist that can be passed as arguments). */ export interface GotoRequest extends Request { // command: 'goto'; @@ -929,7 +972,9 @@ declare module DebugProtocol { startFrame?: number; /** The maximum number of frames to return. If levels is not specified or 0, all frames are returned. */ levels?: number; - /** Specifies details on how to format the stack frames. */ + /** Specifies details on how to format the stack frames. + The attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true. + */ format?: StackFrameFormat; } @@ -986,7 +1031,9 @@ declare module DebugProtocol { start?: number; /** The number of variables to return. If count is missing or 0, all variables are returned. */ count?: number; - /** Specifies details on how to format the Variable values. */ + /** Specifies details on how to format the Variable values. + The attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true. + */ format?: ValueFormat; } @@ -999,7 +1046,7 @@ declare module DebugProtocol { } /** SetVariable request; value of command field is 'setVariable'. - Set the variable with the given name in the variable container to a new value. + Set the variable with the given name in the variable container to a new value. Clients should only call this request if the capability 'supportsSetVariable' is true. */ export interface SetVariableRequest extends Request { // command: 'setVariable'; @@ -1025,14 +1072,18 @@ declare module DebugProtocol { value: string; /** The type of the new value. Typically shown in the UI when hovering over the value. */ type?: string; - /** If variablesReference is > 0, the new value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ + /** If variablesReference is > 0, the new value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. + The value should be less than or equal to 2147483647 (2^31 - 1). + */ variablesReference?: number; /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ namedVariables?: number; /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ indexedVariables?: number; }; @@ -1050,7 +1101,9 @@ declare module DebugProtocol { export interface SourceArguments { /** Specifies the source content to load. Either source.path or source.sourceReference must be specified. */ source?: Source; - /** The reference to the source. This is the same as source.sourceReference. This is provided for backward compatibility since old backends do not understand the 'source' attribute. */ + /** The reference to the source. This is the same as source.sourceReference. + This is provided for backward compatibility since old backends do not understand the 'source' attribute. + */ sourceReference: number; } @@ -1081,6 +1134,7 @@ declare module DebugProtocol { /** TerminateThreads request; value of command field is 'terminateThreads'. The request terminates the threads with the given ids. + Clients should only call this request if the capability 'supportsTerminateThreadsRequest' is true. */ export interface TerminateThreadsRequest extends Request { // command: 'terminateThreads'; @@ -1098,7 +1152,8 @@ declare module DebugProtocol { } /** Modules request; value of command field is 'modules'. - Modules can be retrieved from the debug adapter with the ModulesRequest which can either return all modules or a range of modules to support paging. + Modules can be retrieved from the debug adapter with this request which can either return all modules or a range of modules to support paging. + Clients should only call this request if the capability 'supportsModulesRequest' is true. */ export interface ModulesRequest extends Request { // command: 'modules'; @@ -1125,6 +1180,7 @@ declare module DebugProtocol { /** LoadedSources request; value of command field is 'loadedSources'. Retrieves the set of all sources currently loaded by the debugged process. + Clients should only call this request if the capability 'supportsLoadedSourcesRequest' is true. */ export interface LoadedSourcesRequest extends Request { // command: 'loadedSources'; @@ -1164,10 +1220,13 @@ declare module DebugProtocol { 'repl': evaluate is run from REPL console. 'hover': evaluate is run from a data hover. 'clipboard': evaluate is run to generate the value that will be stored in the clipboard. + The attribute is only honored by a debug adapter if the capability 'supportsClipboardContext' is true. etc. */ context?: string; - /** Specifies details on how to format the Evaluate result. */ + /** Specifies details on how to format the Evaluate result. + The attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true. + */ format?: ValueFormat; } @@ -1176,21 +1235,30 @@ declare module DebugProtocol { body: { /** The result of the evaluate request. */ result: string; - /** The optional type of the evaluate result. */ + /** The optional type of the evaluate result. + This attribute should only be returned by a debug adapter if the client has passed the value true for the 'supportsVariableType' capability of the 'initialize' request. + */ type?: string; /** Properties of a evaluate result that can be used to determine how to render the result in the UI. */ presentationHint?: VariablePresentationHint; - /** If variablesReference is > 0, the evaluate result is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ + /** If variablesReference is > 0, the evaluate result is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. + The value should be less than or equal to 2147483647 (2^31 - 1). + */ variablesReference: number; /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ namedVariables?: number; /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ indexedVariables?: number; - /** Memory reference to a location appropriate for this result. For pointer type eval results, this is generally a reference to the memory address contained in the pointer. */ + /** Optional memory reference to a location appropriate for this result. + For pointer type eval results, this is generally a reference to the memory address contained in the pointer. + This attribute should be returned by a debug adapter if the client has passed the value true for the 'supportsMemoryReferences' capability of the 'initialize' request. + */ memoryReference?: string; }; } @@ -1198,6 +1266,7 @@ declare module DebugProtocol { /** SetExpression request; value of command field is 'setExpression'. Evaluates the given 'value' expression and assigns it to the 'expression' which must be a modifiable l-value. The expressions have access to any variables and arguments that are in scope of the specified frame. + Clients should only call this request if the capability 'supportsSetExpression' is true. */ export interface SetExpressionRequest extends Request { // command: 'setExpression'; @@ -1221,18 +1290,24 @@ declare module DebugProtocol { body: { /** The new value of the expression. */ value: string; - /** The optional type of the value. */ + /** The optional type of the value. + This attribute should only be returned by a debug adapter if the client has passed the value true for the 'supportsVariableType' capability of the 'initialize' request. + */ type?: string; /** Properties of a value that can be used to determine how to render the result in the UI. */ presentationHint?: VariablePresentationHint; - /** If variablesReference is > 0, the value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ + /** If variablesReference is > 0, the value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. + The value should be less than or equal to 2147483647 (2^31 - 1). + */ variablesReference?: number; /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ namedVariables?: number; /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ indexedVariables?: number; }; @@ -1242,6 +1317,7 @@ declare module DebugProtocol { This request retrieves the possible stepIn targets for the specified stack frame. These targets can be used in the 'stepIn' request. The StepInTargets may only be called if the 'supportsStepInTargetsRequest' capability exists and is true. + Clients should only call this request if the capability 'supportsStepInTargetsRequest' is true. */ export interface StepInTargetsRequest extends Request { // command: 'stepInTargets'; @@ -1265,7 +1341,7 @@ declare module DebugProtocol { /** GotoTargets request; value of command field is 'gotoTargets'. This request retrieves the possible goto targets for the specified source location. These targets can be used in the 'goto' request. - The GotoTargets request may only be called if the 'supportsGotoTargetsRequest' capability exists and is true. + Clients should only call this request if the capability 'supportsGotoTargetsRequest' is true. */ export interface GotoTargetsRequest extends Request { // command: 'gotoTargets'; @@ -1292,7 +1368,7 @@ declare module DebugProtocol { /** Completions request; value of command field is 'completions'. Returns a list of possible completions for a given caret position and text. - The CompletionsRequest may only be called if the 'supportsCompletionsRequest' capability exists and is true. + Clients should only call this request if the capability 'supportsCompletionsRequest' is true. */ export interface CompletionsRequest extends Request { // command: 'completions'; @@ -1321,6 +1397,7 @@ declare module DebugProtocol { /** ExceptionInfo request; value of command field is 'exceptionInfo'. Retrieves the details of the exception that caused this event to be raised. + Clients should only call this request if the capability 'supportsExceptionInfoRequest' is true. */ export interface ExceptionInfoRequest extends Request { // command: 'exceptionInfo'; @@ -1349,6 +1426,7 @@ declare module DebugProtocol { /** ReadMemory request; value of command field is 'readMemory'. Reads bytes from memory at the provided location. + Clients should only call this request if the capability 'supportsReadMemoryRequest' is true. */ export interface ReadMemoryRequest extends Request { // command: 'readMemory'; @@ -1368,9 +1446,13 @@ declare module DebugProtocol { /** Response to 'readMemory' request. */ export interface ReadMemoryResponse extends Response { body?: { - /** The address of the first byte of data returned. Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. */ + /** The address of the first byte of data returned. + Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. + */ address: string; - /** The number of unreadable bytes encountered after the last successfully read byte. This can be used to determine the number of bytes that must be skipped before a subsequent 'readMemory' request will succeed. */ + /** The number of unreadable bytes encountered after the last successfully read byte. + This can be used to determine the number of bytes that must be skipped before a subsequent 'readMemory' request will succeed. + */ unreadableBytes?: number; /** The bytes read from memory, encoded using base64. */ data?: string; @@ -1379,6 +1461,7 @@ declare module DebugProtocol { /** Disassemble request; value of command field is 'disassemble'. Disassembles code stored at the provided location. + Clients should only call this request if the capability 'supportsDisassembleRequest' is true. */ export interface DisassembleRequest extends Request { // command: 'disassemble'; @@ -1393,7 +1476,9 @@ declare module DebugProtocol { offset?: number; /** Optional offset (in instructions) to be applied after the byte offset (if any) before disassembling. Can be negative. */ instructionOffset?: number; - /** Number of instructions to disassemble starting at the specified location and offset. An adapter must return exactly this number of instructions - any unavailable instructions should be replaced with an implementation-defined 'invalid instruction' value. */ + /** Number of instructions to disassemble starting at the specified location and offset. + An adapter must return exactly this number of instructions - any unavailable instructions should be replaced with an implementation-defined 'invalid instruction' value. + */ instructionCount: number; /** If true, the adapter should attempt to resolve memory addresses and other values to symbolic names. */ resolveSymbols?: boolean; @@ -1543,7 +1628,8 @@ declare module DebugProtocol { addressRange?: string; } - /** A ColumnDescriptor specifies what module attribute to show in a column of the ModulesView, how to format it, and what the column's label should be. + /** A ColumnDescriptor specifies what module attribute to show in a column of the ModulesView, how to format it, + and what the column's label should be. It is only used if the underlying UI actually supports this level of customization. */ export interface ColumnDescriptor { @@ -1574,21 +1660,34 @@ declare module DebugProtocol { name: string; } - /** A Source is a descriptor for source code. It is returned from the debug adapter as part of a StackFrame and it is used by clients when specifying breakpoints. */ + /** A Source is a descriptor for source code. + It is returned from the debug adapter as part of a StackFrame and it is used by clients when specifying breakpoints. + */ export interface Source { - /** The short name of the source. Every source returned from the debug adapter has a name. When sending a source to the debug adapter this name is optional. */ + /** The short name of the source. Every source returned from the debug adapter has a name. + When sending a source to the debug adapter this name is optional. + */ name?: string; - /** The path of the source to be shown in the UI. It is only used to locate and load the content of the source if no sourceReference is specified (or its value is 0). */ + /** The path of the source to be shown in the UI. + It is only used to locate and load the content of the source if no sourceReference is specified (or its value is 0). + */ path?: string; - /** If sourceReference > 0 the contents of the source must be retrieved through the SourceRequest (even if a path is specified). A sourceReference is only valid for a session, so it must not be used to persist a source. The value should be less than or equal to 2147483647 (2^31 - 1). */ + /** If sourceReference > 0 the contents of the source must be retrieved through the SourceRequest (even if a path is specified). + A sourceReference is only valid for a session, so it must not be used to persist a source. + The value should be less than or equal to 2147483647 (2^31 - 1). + */ sourceReference?: number; - /** An optional hint for how to present the source in the UI. A value of 'deemphasize' can be used to indicate that the source is not available or that it is skipped on stepping. */ + /** An optional hint for how to present the source in the UI. + A value of 'deemphasize' can be used to indicate that the source is not available or that it is skipped on stepping. + */ presentationHint?: 'normal' | 'emphasize' | 'deemphasize'; /** The (optional) origin of this source: possible values 'internal module', 'inlined content from source map', etc. */ origin?: string; /** An optional list of sources that are related to this source. These may be the source that generated this source. */ sources?: Source[]; - /** Optional data that a debug adapter might want to loop through the client. The client should leave the data intact and persist it across sessions. The client should not interpret the data. */ + /** Optional data that a debug adapter might want to loop through the client. + The client should leave the data intact and persist it across sessions. The client should not interpret the data. + */ adapterData?: any; /** The checksums associated with this file. */ checksums?: Checksum[]; @@ -1596,7 +1695,9 @@ declare module DebugProtocol { /** A Stackframe contains the source location. */ export interface StackFrame { - /** An identifier for the stack frame. It must be unique across all threads. This id can be used to retrieve the scopes of the frame with the 'scopesRequest' or to restart the execution of a stackframe. */ + /** An identifier for the stack frame. It must be unique across all threads. + This id can be used to retrieve the scopes of the frame with the 'scopesRequest' or to restart the execution of a stackframe. + */ id: number; /** The name of the stack frame, typically a method name. */ name: string; @@ -1614,7 +1715,9 @@ declare module DebugProtocol { instructionPointerReference?: string; /** The module associated with this frame, if any. */ moduleId?: number | string; - /** An optional hint for how to present this frame in the UI. A value of 'label' can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of 'subtle' can be used to change the appearance of a frame in a 'subtle' way. */ + /** An optional hint for how to present this frame in the UI. + A value of 'label' can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of 'subtle' can be used to change the appearance of a frame in a 'subtle' way. + */ presentationHint?: 'normal' | 'label' | 'subtle'; } @@ -1666,7 +1769,9 @@ declare module DebugProtocol { name: string; /** The variable's value. This can be a multi-line text, e.g. for a function the body of a function. */ value: string; - /** The type of the variable's value. Typically shown in the UI when hovering over the value. */ + /** The type of the variable's value. Typically shown in the UI when hovering over the value. + This attribute should only be returned by a debug adapter if the client has passed the value true for the 'supportsVariableType' capability of the 'initialize' request. + */ type?: string; /** Properties of a variable that can be used to determine how to render the variable in the UI. */ presentationHint?: VariablePresentationHint; @@ -1682,7 +1787,9 @@ declare module DebugProtocol { The client can use this optional information to present the children in a paged UI and fetch them in chunks. */ indexedVariables?: number; - /** Optional memory reference for the variable if the variable represents executable code, such as a function pointer. */ + /** Optional memory reference for the variable if the variable represents executable code, such as a function pointer. + This attribute is only required if the client has passed the value true for the 'supportsMemoryReferences' capability of the 'initialize' request. + */ memoryReference?: string; } @@ -1699,7 +1806,8 @@ declare module DebugProtocol { 'innerClass': Indicates that the object is an inner class. 'interface': Indicates that the object is an interface. 'mostDerivedClass': Indicates that the object is the most derived class. - 'virtual': Indicates that the object is virtual, that means it is a synthetic object introduced by the adapter for rendering purposes, e.g. an index range for large arrays. + 'virtual': Indicates that the object is virtual, that means it is a synthetic object introducedby the + adapter for rendering purposes, e.g. an index range for large arrays. 'dataBreakpoint': Indicates that a data breakpoint is registered for the object. etc. */ @@ -1740,11 +1848,19 @@ declare module DebugProtocol { line: number; /** An optional source column of the breakpoint. */ column?: number; - /** An optional expression for conditional breakpoints. */ + /** An optional expression for conditional breakpoints. + It is only honored by a debug adapter if the capability 'supportsConditionalBreakpoints' is true. + */ condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ + /** An optional expression that controls how many hits of the breakpoint are ignored. + The backend is expected to interpret the expression as needed. + The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. + */ hitCondition?: string; - /** If this attribute exists and is non-empty, the backend must not 'break' (stop) but log the message instead. Expressions within {} are interpolated. */ + /** If this attribute exists and is non-empty, the backend must not 'break' (stop) + but log the message instead. Expressions within {} are interpolated. + The attribute is only honored by a debug adapter if the capability 'supportsLogPoints' is true. + */ logMessage?: string; } @@ -1752,9 +1868,14 @@ declare module DebugProtocol { export interface FunctionBreakpoint { /** The name of the function. */ name: string; - /** An optional expression for conditional breakpoints. */ + /** An optional expression for conditional breakpoints. + It is only honored by a debug adapter if the capability 'supportsConditionalBreakpoints' is true. + */ condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ + /** An optional expression that controls how many hits of the breakpoint are ignored. + The backend is expected to interpret the expression as needed. + The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. + */ hitCondition?: string; } @@ -1769,7 +1890,9 @@ declare module DebugProtocol { accessType?: DataBreakpointAccessType; /** An optional expression for conditional breakpoints. */ condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ + /** An optional expression that controls how many hits of the breakpoint are ignored. + The backend is expected to interpret the expression as needed. + */ hitCondition?: string; } @@ -1779,7 +1902,9 @@ declare module DebugProtocol { id?: number; /** If true breakpoint could be set (but not necessarily at the desired location). */ verified: boolean; - /** An optional message about the state of the breakpoint. This is shown to the user and can be used to explain why a breakpoint could not be verified. */ + /** An optional message about the state of the breakpoint. + This is shown to the user and can be used to explain why a breakpoint could not be verified. + */ message?: string; /** The source where the breakpoint is located. */ source?: Source; @@ -1789,7 +1914,9 @@ declare module DebugProtocol { column?: number; /** An optional end line of the actual range covered by the breakpoint. */ endLine?: number; - /** An optional end column of the actual range covered by the breakpoint. If no end line is given, then the end column is assumed to be in the start line. */ + /** An optional end column of the actual range covered by the breakpoint. + If no end line is given, then the end column is assumed to be in the start line. + */ endColumn?: number; } @@ -1891,7 +2018,9 @@ declare module DebugProtocol { /** An ExceptionOptions assigns configuration options to a set of exceptions. */ export interface ExceptionOptions { - /** A path that selects a single or multiple exceptions in a tree. If 'path' is missing, the whole tree is selected. By convention the first segment of the path is a category that is used to group exceptions in the UI. */ + /** A path that selects a single or multiple exceptions in a tree. If 'path' is missing, the whole tree is selected. + By convention the first segment of the path is a category that is used to group exceptions in the UI. + */ path?: ExceptionPathSegment[]; /** Condition when a thrown exception should result in a break. */ breakMode: ExceptionBreakMode; @@ -1905,7 +2034,10 @@ declare module DebugProtocol { */ export type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled'; - /** An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions. If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing or it matches anything except the names provided if 'negate' is true. */ + /** An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions. + If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing or + it matches anything except the names provided if 'negate' is true. + */ export interface ExceptionPathSegment { /** If false or missing this segment matches the names provided, otherwise it matches anything except the names provided. */ negate?: boolean; @@ -1939,7 +2071,10 @@ declare module DebugProtocol { instruction: string; /** Name of the symbol that corresponds with the location of this instruction, if any. */ symbol?: string; - /** Source location that corresponds to this instruction, if any. Should always be set (if available) on the first instruction returned, but can be omitted afterwards if this instruction maps to the same source file as the previous instruction. */ + /** Source location that corresponds to this instruction, if any. + Should always be set (if available) on the first instruction returned, + but can be omitted afterwards if this instruction maps to the same source file as the previous instruction. + */ location?: Source; /** The line within the source location that corresponds to this instruction, if any. */ line?: number; diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 57fb9caa11..83ed7cd766 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -230,6 +230,10 @@ export class MockSession implements IDebugSession { throw new Error('not implemented'); } + get onDidProgressUpdate(): Event { + throw new Error('not implemented'); + } + get onDidProgressEnd(): Event { throw new Error('not implemented'); } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index 022760f889..4abdb19dbf 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -28,6 +28,7 @@ height: calc(100% - 41px); } +.extensions-viewlet > .extensions .extension-view-header .count-badge-wrapper, .extensions-viewlet > .extensions .extension-view-header .monaco-action-bar { margin-right: 4px; } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index f1233e5517..8ef2ff420e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -592,7 +592,7 @@ export class FilesFilter implements ITreeFilter { // Hide those that match Hidden Patterns const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); - if (cached && cached.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) { + if ((cached && cached.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) || stat.parent?.isExcluded) { stat.isExcluded = true; const editors = this.editorService.visibleEditors; const editor = editors.filter(e => e.resource && isEqualOrParent(e.resource, stat.resource)).pop(); diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index 9196f850da..d0bc9ccdc0 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -6,7 +6,7 @@ export const INSERT_CODE_CELL_ABOVE_COMMAND_ID = 'workbench.notebook.code.insertCellAbove'; export const INSERT_CODE_CELL_BELOW_COMMAND_ID = 'workbench.notebook.code.insertCellBelow'; export const INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID = 'workbench.notebook.markdown.insertCellAbove'; -export const INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID = 'workbench.notebook.markdown.insertCellAbove'; +export const INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID = 'workbench.notebook.markdown.insertCellBelow'; export const EDIT_CELL_COMMAND_ID = 'workbench.notebook.cell.edit'; export const SAVE_CELL_COMMAND_ID = 'workbench.notebook.cell.save'; @@ -24,3 +24,7 @@ export const CELL_MARGIN = 32; export const EDITOR_TOP_PADDING = 8; export const EDITOR_BOTTOM_PADDING = 8; export const EDITOR_TOOLBAR_HEIGHT = 22; +export const RUN_BUTTON_WIDTH = 20; + +// Context Keys +export const NOTEBOOK_CELL_TYPE_CONTEXT_KEY = 'notebookCellType'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts index f43ef1a7b3..fe687a9a69 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts @@ -11,8 +11,8 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { InputFocusedContext, InputFocusedContextKey, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { DELETE_CELL_COMMAND_ID, EDIT_CELL_COMMAND_ID, INSERT_CODE_CELL_ABOVE_COMMAND_ID, INSERT_CODE_CELL_BELOW_COMMAND_ID, INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, MOVE_CELL_DOWN_COMMAND_ID, MOVE_CELL_UP_COMMAND_ID, SAVE_CELL_COMMAND_ID, COPY_CELL_UP_COMMAND_ID, COPY_CELL_DOWN_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/constants'; -import { INotebookEditor, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, ICellViewModel, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { COPY_CELL_DOWN_COMMAND_ID, COPY_CELL_UP_COMMAND_ID, DELETE_CELL_COMMAND_ID, EDIT_CELL_COMMAND_ID, EXECUTE_CELL_COMMAND_ID, INSERT_CODE_CELL_ABOVE_COMMAND_ID, INSERT_CODE_CELL_BELOW_COMMAND_ID, INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, MOVE_CELL_DOWN_COMMAND_ID, MOVE_CELL_UP_COMMAND_ID, SAVE_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellRenderTemplate, CellState, ICellViewModel, INotebookEditor, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -20,7 +20,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.action.executeNotebookCell', + id: EXECUTE_CELL_COMMAND_ID, title: localize('notebookActions.execute', "Execute Notebook Cell"), keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext), @@ -33,11 +33,36 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise { - runActiveCell(accessor); + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + runCell(accessor, context); } }); +export class ExecuteCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: EXECUTE_CELL_COMMAND_ID, + title: localize('notebookActions.executeCell', "Execute Cell"), + icon: { id: 'codicon/play' } + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + registerAction2(class extends Action2 { constructor() { super({ @@ -53,7 +78,7 @@ registerAction2(class extends Action2 { async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); - const activeCell = runActiveCell(accessor); + const activeCell = await runActiveCell(accessor); if (!activeCell) { return; } @@ -93,7 +118,7 @@ registerAction2(class extends Action2 { async run(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); - const activeCell = runActiveCell(accessor); + const activeCell = await runActiveCell(accessor); if (!activeCell) { return; } @@ -273,7 +298,7 @@ function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor return activeEditorPane?.isNotebookEditor ? activeEditorPane : undefined; } -function runActiveCell(accessor: ServicesAccessor): ICellViewModel | undefined { +async function runActiveCell(accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const notebookService = accessor.get(INotebookService); @@ -303,11 +328,41 @@ function runActiveCell(accessor: ServicesAccessor): ICellViewModel | undefined { } const viewType = notebookProviders[0].id; - notebookService.executeNotebookActiveCell(viewType, resource); + await notebookService.executeNotebookActiveCell(viewType, resource); return activeCell; } +async function runCell(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + const progress = context.cellTemplate!.progressBar!; + progress.infinite().show(500); + + const editorService = accessor.get(IEditorService); + const notebookService = accessor.get(INotebookService); + + const resource = editorService.activeEditor?.resource; + if (!resource) { + return; + } + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const notebookProviders = notebookService.getContributedNotebookProviders(resource); + if (!notebookProviders.length) { + return; + } + + // Need to make active, maybe TODO + editor.focusNotebookCell(context.cell, false); + + const viewType = notebookProviders[0].id; + await notebookService.executeNotebookActiveCell(viewType, resource); + progress.hide(); +} + async function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor): Promise { const editorService = accessor.get(IEditorService); const editor = getActiveNotebookEditor(editorService); @@ -341,6 +396,7 @@ async function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor } export interface INotebookCellActionContext { + cellTemplate?: CellRenderTemplate; cell: ICellViewModel; notebookEditor: INotebookEditor; } @@ -413,7 +469,7 @@ registerAction2(class extends InsertCellCommand { constructor() { super( { - id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + id: INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"), }, CellKind.Markdown, @@ -428,7 +484,7 @@ registerAction2(class extends InsertCellCommand { id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), }, - CellKind.Code, + CellKind.Markdown, 'below'); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.css b/src/vs/workbench/contrib/notebook/browser/notebook.css index b6765b347d..6a5401784d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/notebook.css @@ -17,9 +17,9 @@ white-space: initial; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-scrollable-element { +/* .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-scrollable-element { overflow: visible !important; -} +} */ .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-list-rows { min-height: 100%; @@ -30,6 +30,10 @@ position: relative; } +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .cell { + display: flex; +} + .monaco-workbench .part.editor > .content .notebook-editor .notebook-content-widgets { position: absolute; top: 0; @@ -122,11 +126,29 @@ cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .monaco-toolbar { +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > .monaco-toolbar { visibility: hidden; margin-right: 24px; } +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .cell .run-button-container .monaco-toolbar { + margin-top: 8px; + visibility: hidden; +} + +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .cell .run-button-container .monaco-toolbar, +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.focused .cell .run-button-container .monaco-toolbar { + visibility: visible; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell .cell-editor-container { + position: relative; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell .monaco-progress-container { + top: 0px; +} + .monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.focused .monaco-toolbar, .monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .monaco-toolbar { visibility: visible; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 9cb4243d64..58e8f107c4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -16,6 +16,7 @@ import { Range } from 'vs/editor/common/core/range'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); @@ -44,6 +45,8 @@ export interface INotebookEditor { */ viewModel: NotebookViewModel | undefined; + isNotebookEditor: boolean; + /** * Focus the notebook editor cell list */ @@ -121,6 +124,11 @@ export interface INotebookEditor { */ removeInset(output: IOutput): void; + /** + * Send message to the webview for outputs. + */ + postMessage(message: any): void; + /** * Trigger the editor to scroll from scroll event programmatically */ @@ -195,12 +203,14 @@ export interface INotebookEditor { export interface CellRenderTemplate { container: HTMLElement; cellContainer: HTMLElement; - menuContainer?: HTMLElement; + editorContainer?: HTMLElement; toolbar: ToolBar; focusIndicator?: HTMLElement; + runToolbar?: ToolBar; editingContainer?: HTMLElement; outputContainer?: HTMLElement; editor?: CodeEditorWidget; + progressBar?: ProgressBar; disposables: DisposableStore; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index f42c05b9fa..8563e504fe 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -42,7 +42,7 @@ import { NotebookViewModel, INotebookEditorViewState, IModelDecorationsChangeAcc import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; import { Range } from 'vs/editor/common/core/range'; -import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CELL_MARGIN, RUN_BUTTON_WIDTH } from 'vs/workbench/contrib/notebook/browser/constants'; import { Color, RGBA } from 'vs/base/common/color'; const $ = DOM.$; @@ -219,6 +219,11 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.control = new NotebookCodeEditors(this.list, this.renderedEditors); this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this, this.environmentSerice); + this._register(this.webview.onMessage(message => { + if (this.viewModel) { + this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.viewModel.uri, message); + } + })); this.list.rowsContainer.appendChild(this.webview.element); this._register(this.list); } @@ -697,7 +702,17 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return this.outputRenderer; } + postMessage(message: any) { + this.webview?.webview.sendMessage(message); + } + //#endregion + + toJSON(): any { + return { + notebookHandle: this.viewModel?.handle + }; + } } const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; @@ -717,12 +732,12 @@ registerThemingParticipant((theme, collector) => { } const link = theme.getColor(textLinkForeground); if (link) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell a { color: ${link}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .output a { color: ${link}; }`); } const activeLink = theme.getColor(textLinkActiveForeground); if (activeLink) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell a:hover, - .monaco-workbench .part.editor > .content .notebook-editor .cell a:active { color: ${activeLink}; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .output a:hover, + .monaco-workbench .part.editor > .content .notebook-editor .cell .output a:active { color: ${activeLink}; }`); } const shortcut = theme.getColor(textPreformatForeground); if (shortcut) { @@ -756,5 +771,7 @@ registerThemingParticipant((theme, collector) => { // Cell Margin collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell { padding: 8px ${CELL_MARGIN}px 8px ${CELL_MARGIN}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { margin: 8px ${CELL_MARGIN}px; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { margin: 8px ${CELL_MARGIN}px 8px ${CELL_MARGIN + RUN_BUTTON_WIDTH}px }`); + + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .cell-editor-container { width: calc(100% - ${RUN_BUTTON_WIDTH}px); }`); }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookService.ts index 35e753cf25..4f78bf3861 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookService.ts @@ -29,7 +29,8 @@ export interface IMainNotebookController { updateNotebookActiveCell(uri: URI, cellHandle: number): void; createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise; deleteCell(uri: URI, index: number): Promise - executeNotebookActiveCell(uri: URI): void; + executeNotebookActiveCell(uri: URI): Promise; + onDidReceiveMessage(uri: URI, message: any): void; destoryNotebookDocument(notebook: INotebookTextModel): Promise; save(uri: URI): Promise; } @@ -54,6 +55,7 @@ export interface INotebookService { destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; updateActiveNotebookDocument(viewType: string, resource: URI): void; save(viewType: string, resource: URI): Promise; + onDidReceiveMessage(viewType: string, uri: URI, message: any): void; } export class NotebookProviderInfoStore { @@ -325,6 +327,14 @@ export class NotebookService extends Disposable implements INotebookService { return false; } + onDidReceiveMessage(viewType: string, uri: URI, message: any): void { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.onDidReceiveMessage(uri, message); + } + } + private _onWillDispose(model: INotebookTextModel): void { let modelId = MODEL_ID(model.uri); let modelData = this._models[modelId]; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index b2f8bf84c8..5c242eb94f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -16,8 +16,10 @@ import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/br import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; +import { Emitter, Event } from 'vs/base/common/event'; export interface IDimentionMessage { + __vscode_notebook_message: boolean; type: 'dimension'; id: string; data: DOM.Dimension; @@ -25,6 +27,7 @@ export interface IDimentionMessage { export interface IScrollAckMessage { + __vscode_notebook_message: boolean; type: 'scroll-ack'; data: { top: number }; version: number; @@ -78,6 +81,9 @@ export class BackLayerWebView extends Disposable { preloadsCache: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; + private readonly _onMessage = this._register(new Emitter()); + public readonly onMessage: Event = this._onMessage.event; + constructor(public webviewService: IWebviewService, public notebookService: INotebookService, public notebookEditor: INotebookEditor, public environmentSerice: IEnvironmentService) { super(); @@ -154,6 +160,7 @@ export class BackLayerWebView extends Disposable { for (let entry of entries) { if (entry.target.id === id && entry.contentRect) { vscode.postMessage({ + __vscode_notebook_message: true, type: 'dimension', id: id, data: { @@ -198,6 +205,7 @@ export class BackLayerWebView extends Disposable { resizeObserve(outputNode, outputId); vscode.postMessage({ + __vscode_notebook_message: true, type: 'dimension', id: outputId, data: { @@ -255,27 +263,32 @@ export class BackLayerWebView extends Disposable { })); this._register(this.webview.onMessage((data: IMessage) => { - if (data.type === 'dimension') { - let output = this.reversedInsetMapping.get(data.id); + if (data.__vscode_notebook_message) { + if (data.type === 'dimension') { + let output = this.reversedInsetMapping.get(data.id); - if (!output) { - return; + if (!output) { + return; + } + + let cell = this.insetMapping.get(output)!.cell; + let height = data.data.height; + let outputHeight = height === 0 ? 0 : height + 16; + + if (cell) { + let outputIndex = cell.outputs.indexOf(output); + cell.updateOutputHeight(outputIndex, outputHeight); + this.notebookEditor.layoutNotebookCell(cell, cell.getCellTotalHeight()); + } + } else if (data.type === 'scroll-ack') { + // const date = new Date(); + // const top = data.data.top; + // console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); } - - let cell = this.insetMapping.get(output)!.cell; - let height = data.data.height; - let outputHeight = height === 0 ? 0 : height + 16; - - if (cell) { - let outputIndex = cell.outputs.indexOf(output); - cell.updateOutputHeight(outputIndex, outputHeight); - this.notebookEditor.layoutNotebookCell(cell, cell.getCellTotalHeight()); - } - } else if (data.type === 'scroll-ack') { - // const date = new Date(); - // const top = data.data.top; - // console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); + return; } + + this._onMessage.fire(data); })); } @@ -284,6 +297,7 @@ export class BackLayerWebView extends Disposable { const webview = webviewService.createWebviewElement('' + UUID.generateUuid(), { enableFindWidget: false, }, { + allowMultipleAPIAcquire: true, allowScripts: true, localResourceRoots: this.localResourceRootsCache }); @@ -396,6 +410,7 @@ export class BackLayerWebView extends Disposable { const mixedResourceRoots = [...(this.localResourceRootsCache || []), ...this.rendererRootsCache]; this.webview.contentOptions = { + allowMultipleAPIAcquire: true, allowScripts: true, enableCommandUris: true, localResourceRoots: mixedResourceRoots diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts new file mode 100644 index 0000000000..fcd1f92184 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { IAction } from 'vs/base/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; + +export class CellMenus implements IDisposable { + constructor( + @IMenuService private readonly menuService: IMenuService, + @IContextMenuService private readonly contextMenuService: IContextMenuService + ) { } + + getCellTitleActions(contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.NotebookCellTitle, contextKeyService); + } + + private getMenu(menuId: MenuId, contextKeyService: IContextKeyService): IMenu { + const menu = this.menuService.createMenu(menuId, contextKeyService); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); + + return menu; + } + + dispose(): void { + + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index c0d336ccdf..aa23dfbac1 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -15,22 +15,27 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { InsertCodeCellAboveAction, INotebookCellActionContext, InsertCodeCellBelowAction, InsertMarkdownCellAboveAction, InsertMarkdownCellBelowAction, EditCellAction, SaveCellAction, DeleteCellAction, MoveCellUpAction, MoveCellDownAction } from 'vs/workbench/contrib/notebook/browser/contrib/notebookActions'; -import { CellRenderTemplate, INotebookEditor, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, NOTEBOOK_CELL_TYPE_CONTEXT_KEY } from 'vs/workbench/contrib/notebook/browser/constants'; +import { DeleteCellAction, EditCellAction, ExecuteCellAction, INotebookCellActionContext, InsertCodeCellBelowAction, MoveCellDownAction, MoveCellUpAction, SaveCellAction, InsertCodeCellAboveAction, InsertMarkdownCellAboveAction, InsertMarkdownCellBelowAction } from 'vs/workbench/contrib/notebook/browser/contrib/notebookActions'; +import { CellRenderTemplate, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellViewModel } from '../../viewModel/notebookCellViewModel'; -import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { MenuItemAction } from 'vs/platform/actions/common/actions'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; -export class NotebookCellListDelegate implements IListVirtualDelegate { +const $ = DOM.$; + +export class NotebookCellListDelegate implements IListVirtualDelegate { private _lineHeight: number; private _toolbarHeight = EDITOR_TOOLBAR_HEIGHT; @@ -68,6 +73,7 @@ abstract class AbstractCellRenderer { private readonly configurationService: IConfigurationService, private readonly keybindingService: IKeybindingService, private readonly notificationService: INotificationService, + protected readonly contextKeyService: IContextKeyService, language: string, ) { const editorOptions = deepClone(this.configurationService.getValue('editor', { overrideIdentifier: language })); @@ -108,6 +114,13 @@ abstract class AbstractCellRenderer { return toolbar; } + protected createMenu(): CellMenus { + const menu = this.instantiationService.createInstance(CellMenus); + return menu; + } + + abstract getCellToolbarActions(element: CellViewModel): IAction[]; + showContextMenu(listIndex: number | undefined, element: CellViewModel, x: number, y: number) { const actions: IAction[] = [ this.instantiationService.createInstance(InsertCodeCellAboveAction), @@ -150,8 +163,9 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, + @IContextKeyService contextKeyService: IContextKeyService ) { - super(instantiationService, notehookEditor, contextMenuService, configurationService, keybindingService, notificationService, 'markdown'); + super(instantiationService, notehookEditor, contextMenuService, configurationService, keybindingService, notificationService, contextKeyService, 'markdown'); } get templateId() { @@ -165,14 +179,6 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const disposables = new DisposableStore(); const toolbar = this.createToolbar(container); - toolbar.setActions([ - this.instantiationService.createInstance(MoveCellUpAction), - this.instantiationService.createInstance(MoveCellDownAction), - this.instantiationService.createInstance(InsertCodeCellBelowAction), - this.instantiationService.createInstance(EditCellAction), - this.instantiationService.createInstance(SaveCellAction), - this.instantiationService.createInstance(DeleteCellAction) - ])(); disposables.add(toolbar); container.appendChild(codeInnerContent); @@ -181,16 +187,11 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR DOM.addClasses(innerContent, 'cell', 'markdown'); container.appendChild(innerContent); - const action = document.createElement('div'); - DOM.addClasses(action, 'menu', 'codicon-settings-gear', 'codicon'); - container.appendChild(action); - DOM.append(container, DOM.$('.notebook-cell-focus-indicator')); return { container: container, cellContainer: innerContent, - menuContainer: action, editingContainer: codeInnerContent, disposables, toolbar @@ -212,32 +213,67 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR } let elementDisposable = this.disposables.get(element); - elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, 'mousedown', e => { - const { top, height } = DOM.getDomNodePagePosition(templateData.menuContainer!); - e.preventDefault(); - - const listIndexAttr = templateData.menuContainer?.parentElement?.getAttribute('data-index'); - const listIndex = listIndexAttr ? Number(listIndexAttr) : undefined; - this.showContextMenu(listIndex, element, e.posx, top + height); - })); - - elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_LEAVE, e => { - templateData.menuContainer?.classList.remove('mouseover'); - })); - - elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_ENTER, e => { - templateData.menuContainer?.classList.add('mouseover'); - })); - elementDisposable!.add(new StatefullMarkdownCell(this.notebookEditor, element, templateData, this.editorOptions, this.instantiationService)); + + const contextKeyService = this.contextKeyService.createScoped(templateData.container); + contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'markdown'); + const toolbarActions = this.getCellToolbarActions(element); + templateData.toolbar!.setActions(toolbarActions)(); + + if (templateData.focusIndicator) { + if (!toolbarActions.length) { + templateData.focusIndicator.style.top = `8px`; + } else { + templateData.focusIndicator.style.top = `24px`; + } + } } templateData.toolbar!.context = { cell: element, - notebookEditor: this.notebookEditor + notebookEditor: this.notebookEditor, + $mid: 12 }; } + getCellToolbarActions(element: CellViewModel): IAction[] { + const viewModel = this.notebookEditor.viewModel; + + if (!viewModel) { + return []; + } + + const menu = this.createMenu().getCellTitleActions(this.contextKeyService); + const actions: IAction[] = []; + for (let [, actions] of menu.getActions({ shouldForwardArgs: true })) { + actions.push(...actions); + } + + const metadata = viewModel.metadata; + + if (!metadata || metadata.editable) { + actions.push( + this.instantiationService.createInstance(MoveCellUpAction), + this.instantiationService.createInstance(MoveCellDownAction), + this.instantiationService.createInstance(InsertCodeCellBelowAction) + ); + } + + const cellMetadata = element.metadata; + if (!cellMetadata || cellMetadata.editable) { + actions.push( + this.instantiationService.createInstance(EditCellAction), + this.instantiationService.createInstance(SaveCellAction) + ); + } + + if (!metadata || metadata.editable) { + this.instantiationService.createInstance(DeleteCellAction); + } + + return actions; + } + getAdditionalContextMenuActions(): IAction[] { return [ this.instantiationService.createInstance(EditCellAction), @@ -269,8 +305,9 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende @IInstantiationService instantiationService: IInstantiationService, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, + @IContextKeyService contextKeyService: IContextKeyService ) { - super(instantiationService, notebookEditor, contextMenuService, configurationService, keybindingService, notificationService, 'python'); + super(instantiationService, notebookEditor, contextMenuService, configurationService, keybindingService, notificationService, contextKeyService, 'python'); } get templateId() { @@ -279,9 +316,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende renderTemplate(container: HTMLElement): CellRenderTemplate { const disposables = new DisposableStore(); - const toolbarContainer = document.createElement('div'); - container.appendChild(toolbarContainer); - DOM.addClasses(toolbarContainer, 'menu', 'codicon-settings-gear', 'codicon'); const toolbar = this.createToolbar(container); toolbar.setActions([ this.instantiationService.createInstance(MoveCellUpAction), @@ -291,19 +325,22 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende ])(); disposables.add(toolbar); - const cellContainer = document.createElement('div'); - DOM.addClasses(cellContainer, 'cell', 'code'); - container.appendChild(cellContainer); - const editor = this.instantiationService.createInstance(CodeEditorWidget, cellContainer, { + const cellContainer = DOM.append(container, $('.cell.code')); + const runButtonContainer = DOM.append(cellContainer, $('.run-button-container')); + const runToolbar = this.createToolbar(runButtonContainer); + runToolbar.setActions([ + this.instantiationService.createInstance(ExecuteCellAction) + ])(); + disposables.add(runToolbar); + + const editorContainer = DOM.append(cellContainer, $('.cell-editor-container')); + const editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { ...this.editorOptions, dimension: { width: 0, height: 0 } }, {}); - const menuContainer = document.createElement('div'); - DOM.addClasses(menuContainer, 'menu', 'codicon-settings-gear', 'codicon'); - container.appendChild(menuContainer); const focusIndicator = DOM.append(container, DOM.$('.notebook-cell-focus-indicator')); @@ -311,12 +348,18 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende DOM.addClasses(outputContainer, 'output'); container.appendChild(outputContainer); + const progressBar = new ProgressBar(editorContainer); + progressBar.hide(); + disposables.add(progressBar); + return { container, cellContainer, - menuContainer, + editorContainer, + progressBar, focusIndicator, toolbar, + runToolbar, outputContainer, editor, disposables @@ -339,24 +382,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const elementDisposable = this.disposables.get(element); - elementDisposable?.add(DOM.addStandardDisposableListener(templateData.menuContainer!, 'mousedown', e => { - let { top, height } = DOM.getDomNodePagePosition(templateData.menuContainer!); - e.preventDefault(); - - const listIndexAttr = templateData.menuContainer?.parentElement?.getAttribute('data-index'); - const listIndex = listIndexAttr ? Number(listIndexAttr) : undefined; - - this.showContextMenu(listIndex, element, e.posx, top + height); - })); - - elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_LEAVE, e => { - templateData.menuContainer?.classList.remove('mouseover'); - })); - - elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_ENTER, e => { - templateData.menuContainer?.classList.add('mouseover'); - })); - elementDisposable?.add(this.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData)); this.renderedEditors.set(element, templateData.editor); @@ -364,12 +389,58 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende templateData.focusIndicator!.style.height = `${element.getIndicatorHeight()}px`; })); - templateData.toolbar!.context = { + const toolbarContext = { cell: element, - notebookEditor: this.notebookEditor + cellTemplate: templateData, + notebookEditor: this.notebookEditor, + $mid: 12 }; + + const contextKeyService = this.contextKeyService.createScoped(templateData.container); + contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'code'); + const toolbarActions = this.getCellToolbarActions(element); + templateData.toolbar!.setActions(toolbarActions)(); + templateData.toolbar!.context = toolbarContext; + templateData.runToolbar!.context = toolbarContext; + + if (templateData.focusIndicator) { + if (!toolbarActions.length) { + templateData.focusIndicator.style.top = `8px`; + } else { + templateData.focusIndicator.style.top = `24px`; + } + } } + + getCellToolbarActions(element: CellViewModel): IAction[] { + const viewModel = this.notebookEditor.viewModel; + + if (!viewModel) { + return []; + } + + const menu = this.createMenu().getCellTitleActions(this.contextKeyService); + const actions: IAction[] = []; + for (let [, actions] of menu.getActions({ shouldForwardArgs: true })) { + actions.push(...actions); + } + + const metadata = viewModel.metadata; + + if (!metadata || metadata.editable) { + actions.push( + this.instantiationService.createInstance(MoveCellUpAction), + this.instantiationService.createInstance(MoveCellDownAction), + this.instantiationService.createInstance(InsertCodeCellBelowAction), + this.instantiationService.createInstance(DeleteCellAction) + ); + } + + return actions; + } + + getAdditionalContextMenuActions(): IAction[] { return []; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index 7992f47778..85aea975de 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -14,7 +14,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; -import { CELL_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CELL_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING, RUN_BUTTON_WIDTH } from 'vs/workbench/contrib/notebook/browser/constants'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -34,7 +34,8 @@ export class CodeCell extends Disposable { let width: number; const listDimension = notebookEditor.getLayoutInfo(); - width = listDimension.width - CELL_MARGIN * 2; + width = listDimension.width - CELL_MARGIN * 2 - RUN_BUTTON_WIDTH; + const lineNum = viewCell.lineCount; const lineHeight = notebookEditor.getLayoutInfo().fontInfo.lineHeight; const totalHeight = lineNum * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; @@ -59,7 +60,7 @@ export class CodeCell extends Disposable { let realContentHeight = templateData.editor?.getContentHeight(); let width: number; const listDimension = notebookEditor.getLayoutInfo(); - width = listDimension.width - CELL_MARGIN * 2; + width = listDimension.width - CELL_MARGIN * 2 - RUN_BUTTON_WIDTH; if (realContentHeight !== undefined && realContentHeight !== totalHeight) { templateData.editor?.layout( @@ -84,7 +85,7 @@ export class CodeCell extends Disposable { } })); - let cellWidthResizeObserver = getResizesObserver(templateData.cellContainer, { + let cellWidthResizeObserver = getResizesObserver(templateData.editorContainer!, { width: width, height: totalHeight }, () => { @@ -267,7 +268,7 @@ export class CodeCell extends Disposable { let clientHeight = outputItemDiv.clientHeight; let listDimension = this.notebookEditor.getLayoutInfo(); let dimension = listDimension ? { - width: listDimension.width - CELL_MARGIN * 2, + width: listDimension.width - CELL_MARGIN * 2 - RUN_BUTTON_WIDTH, height: clientHeight } : undefined; const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts index 6ae91908ae..e783b3c71c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts @@ -55,6 +55,10 @@ export class CellViewModel extends Disposable implements ICellViewModel { return this.cell.outputs; } + get metadata() { + return this.cell.metadata; + } + private _state: CellState = CellState.Preview; get state(): CellState { @@ -519,4 +523,10 @@ export class CellViewModel extends Disposable implements ICellViewModel { this._outputsTop = new PrefixSumComputer(values); } } + + toJSON(): any { + return { + handle: this.handle + }; + } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 4a5b1104ac..047ee2ecc0 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -81,6 +81,10 @@ export class NotebookViewModel extends Disposable { return this._model.notebook.uri; } + get metadata() { + return this._model.notebook.metadata; + } + private readonly _onDidChangeViewCells = new Emitter(); get onDidChangeViewCells(): Event { return this._onDidChangeViewCells.event; } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index cb7ed47fa7..83588fc9b5 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellsSplice, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class NotebookTextModel extends Disposable implements INotebookTextModel { private readonly _onWillDispose: Emitter = this._register(new Emitter()); @@ -21,6 +21,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel cells: NotebookCellTextModel[]; activeCell: NotebookCellTextModel | undefined; languages: string[] = []; + metadata: NotebookDocumentMetadata | undefined = undefined; renderers = new Set(); constructor( @@ -36,6 +37,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this.languages = languages; } + updateNotebookMetadata(metadata: NotebookDocumentMetadata | undefined) { + this.metadata = metadata; + } + updateRenderers(renderers: number[]) { renderers.forEach(render => { this.renderers.add(render); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 357b89c732..4f3a9ae74c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -36,6 +36,14 @@ export const NOTEBOOK_DISPLAY_ORDER = [ 'text/plain' ]; +export interface NotebookDocumentMetadata { + editable: boolean; +} + +export interface NotebookCellMetadata { + editable: boolean; +} + export interface INotebookDisplayOrder { defaultOrder: string[]; userOrder?: string[]; @@ -122,6 +130,7 @@ export interface ICell { language: string; cellKind: CellKind; outputs: IOutput[]; + metadata?: NotebookCellMetadata; onDidChangeOutputs?: Event; resolveTextBufferFactory(): PieceTreeTextBufferFactory; // TODO@rebornix it should be later on replaced by moving textmodel resolution into CellTextModel diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 791cf3c161..d74879d05d 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -69,6 +69,12 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + isNotebookEditor = true; + + postMessage(message: any): void { + throw new Error('Method not implemented.'); + } + setCellSelection(cell: CellViewModel, selection: Range): void { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 5ebf55b654..44cddca169 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -10,7 +10,7 @@ import { prepareQuery, IPreparedQuery, compareItemsByScore, scoreItem, ScorerCac import { IFileQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { getOutOfWorkspaceEditorResources, extractRangeFromFilter, IWorkbenchSearchConfiguration } from 'vs/workbench/contrib/search/common/search'; -import { ISearchService, IFileMatch } from 'vs/workbench/services/search/common/search'; +import { ISearchService } from 'vs/workbench/services/search/common/search'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { untildify } from 'vs/base/common/labels'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; @@ -54,13 +54,24 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + protected getPicks(originalFilter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicksType | null { // Find a suitable range from the pattern looking for ":", "#" or "," - let range: IRange | undefined = undefined; - const filterWithRange = extractRangeFromFilter(filter); + const filterWithRange = extractRangeFromFilter(originalFilter); + + // Update filter with normalized values + let filter: string; if (filterWithRange) { filter = filterWithRange.filter; - range = filterWithRange.range; + } else { + filter = originalFilter; } + // Remember as last range + this.pickState.lastRange = filterWithRange?.range; + + // If the original filter value has changed but the normalized + // one has not, we return early with a `null` result indicating + // that the results should preserve because the range information + // (::) does not need to trigger any re-sorting. + if (originalFilter !== this.pickState.lastOriginalFilter && filter === this.pickState.lastFilter) { + return null; + } + + // Remember as last filter + this.pickState.lastOriginalFilter = originalFilter; + this.pickState.lastFilter = filter; + const query = prepareQuery(filter); - const historyEditorPicks = this.getEditorHistoryPicks(query, range); + const historyEditorPicks = this.getEditorHistoryPicks(query); return { @@ -139,7 +169,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider, token: CancellationToken): Promise> { + private async getAdditionalPicks(query: IPreparedQuery, excludes: ResourceMap, token: CancellationToken): Promise> { // Resolve file and symbol picks (if enabled) const [filePicks, symbolPicks] = await Promise.all([ - this.getFilePicks(query, range, excludes, token), - this.getSymbolPicks(query, range, token) + this.getFilePicks(query, excludes, token), + this.getSymbolPicks(query, token) ]); if (token.isCancellationRequested) { @@ -193,11 +223,12 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + protected getEditorHistoryPicks(query: IPreparedQuery): Array { + const configuration = this.configuration; // Just return all history entries if not searching if (!query.value) { - return this.historyService.getHistory().map(editor => this.createAnythingPick(editor, range)); + return this.historyService.getHistory().map(editor => this.createAnythingPick(editor, configuration)); } if (!this.configuration.includeHistory) { @@ -215,7 +246,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY)); + private fileQueryDelayer = this._register(new ThrottledDelayer(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY)); private fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder); @@ -251,7 +282,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider, token: CancellationToken): Promise> { + protected async getFilePicks(query: IPreparedQuery, excludes: ResourceMap, token: CancellationToken): Promise> { if (!query.value) { return []; } @@ -263,9 +294,9 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider>; + let fileMatches: Array; if (absolutePathResult) { - fileMatches = [{ resource: absolutePathResult }]; + fileMatches = [absolutePathResult]; } // Otherwise run the file search (with a delayer if cache is not ready yet) @@ -288,23 +319,37 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider !excludes.has(fileMatch.resource)) - .map(fileMatch => this.createAnythingPick(fileMatch.resource, range)); + .filter(resource => !excludes.has(resource)) + .map(resource => this.createAnythingPick(resource, configuration)); } - private async doFileSearch(query: IPreparedQuery, token: CancellationToken): Promise { - const { results } = await this.searchService.fileSearch( - this.fileQueryBuilder.file( - this.contextService.getWorkspace().folders, - this.getFileQueryOptions({ - filePattern: query.original, - cacheKey: this.pickState.fileQueryCache?.cacheKey, - maxResults: AnythingQuickAccessProvider.MAX_RESULTS - }) - ), token); + private async doFileSearch(query: IPreparedQuery, token: CancellationToken): Promise { + const [fileSearchResults, relativePathFileResults] = await Promise.all([ - return results; + // File search: this is a search over all files of the workspace using the provided pattern + this.searchService.fileSearch( + this.fileQueryBuilder.file( + this.contextService.getWorkspace().folders, + this.getFileQueryOptions({ + filePattern: query.original, + cacheKey: this.pickState.fileQueryCache?.cacheKey, + maxResults: AnythingQuickAccessProvider.MAX_RESULTS + }) + ), token), + + // Relative path search: we also want to consider results that match files inside the workspace + // by looking for relative paths that the user typed as query. This allows to return even excluded + // results into the picker if found (e.g. helps for opening compilation results that are otherwise + // excluded) + this.getRelativePathFileResults(query, token) + ]); + + return [ + ...fileSearchResults.results.map(result => result.resource), + ...(relativePathFileResults || []) + ]; } private getFileQueryOptions(input: { filePattern?: string, cacheKey?: string, maxResults?: number }): IFileQueryBuilderOptions { @@ -321,7 +366,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { - const detildifiedQuery = untildify(query.original, (await this.remotePathService.userHome).path); + if (!query.containsPathSeparator) { + return undefined; // {{SQL CARBON EDIT}} strict-null + } + + const detildifiedQuery = untildify(query.value, (await this.remotePathService.userHome).path); if (token.isCancellationRequested) { return undefined; // {{SQL CARBON EDIT}} strict-null } @@ -342,15 +391,52 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + if (!query.containsPathSeparator) { + return undefined; // {{SQL CARBON EDIT}} strict-null + } + + // Convert relative paths to absolute paths over all folders of the workspace + // and return them as results if the absolute paths exist + const isAbsolutePathQuery = (await this.remotePathService.path).isAbsolute(query.value); + if (!isAbsolutePathQuery) { + const resources: URI[] = []; + for (const folder of this.contextService.getWorkspace().folders) { + if (token.isCancellationRequested) { + break; + } + + const resource = toLocalResource( + folder.toResource(query.value), + this.environmentService.configuration.remoteAuthority + ); + + try { + if ((await this.fileService.resolve(resource)).isFile) { + resources.push(resource); + } + } catch (error) { + // ignore if file does not exist + } + } + + return resources; + } + + return undefined; // {{SQL CARBON EDIT}} strict-null + } + //#endregion @@ -358,18 +444,23 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> { + protected async getSymbolPicks(query: IPreparedQuery, token: CancellationToken): Promise> { + const configuration = this.configuration; if ( - !query.value || // we need a value for search for - !this.configuration.includeSymbols || // we need to enable symbols in search - range // a range is an indicator for just searching for files + !query.value || // we need a value for search for + !configuration.includeSymbols || // we need to enable symbols in search + this.pickState.lastRange // a range is an indicator for just searching for files ) { return []; } // Delegate to the existing symbols quick access // but skip local results and also do not sort - return this.symbolsQuickAccess.getSymbolPicks(query.value, { skipLocal: true, skipSorting: true, delay: AnythingQuickAccessProvider.TYPING_SEARCH_DELAY }, token); + return this.symbolsQuickAccess.getSymbolPicks(query.value, { + skipLocal: configuration.workspaceSymbolsFilter !== 'all', + skipSorting: true, + delay: AnythingQuickAccessProvider.TYPING_SEARCH_DELAY + }, token); } //#endregion @@ -377,7 +468,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { - const openSideBySideDirection = this.configuration.openSideBySideDirection; + const openSideBySideDirection = configuration.openSideBySideDirection; const buttons: IQuickInputButton[] = []; // Open to side / below @@ -431,7 +522,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider this.openAnything(resourceOrEditor, { keyMods, range, preserveFocus: event.inBackground }) + accept: (keyMods, event) => this.openAnything(resourceOrEditor, { keyMods, range: this.pickState.lastRange, preserveFocus: event.inBackground }) }; } diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 3d467fd350..71098712ba 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -734,6 +734,17 @@ configurationRegistry.registerConfiguration({ description: nls.localize('search.quickOpen.includeSymbols', "Whether to include results from a global symbol search in the file results for Quick Open."), default: false }, + 'search.quickOpen.workspaceSymbolsFilter': { + type: 'string', + enum: ['default', 'reduced', 'all'], + markdownEnumDescriptions: [ + nls.localize('search.quickOpen.workspaceSymbolsFilter.default', "All symbols including local variables are included in the specific workspace symbols picker but excluded from the files picker when `#search.quickOpen.includeSymbols#` is enabled."), + nls.localize('search.quickOpen.workspaceSymbolsFilter.reduced', "Some symbols like local variables are excluded in all pickers."), + nls.localize('search.quickOpen.workspaceSymbolsFilter.all', "All symbols including local variables are included in all pickers.") + ], + default: 'default', + description: nls.localize('search.quickOpen.workspaceSymbolsFilter', "Controls the filter to apply for the workspace symbols search in quick open. Depending on the setting, some symbols like local variables will be excluded to reduce the total number of results."), + }, 'search.quickOpen.includeHistory': { type: 'boolean', description: nls.localize('search.quickOpen.includeHistory', "Whether to include results from recently opened files in the file results for Quick Open."), @@ -766,7 +777,7 @@ configurationRegistry.registerConfiguration({ type: 'string', enum: ['auto', 'alwaysCollapse', 'alwaysExpand'], enumDescriptions: [ - 'Files with less than 10 results are expanded. Others are collapsed.', + nls.localize('search.collapseResults.auto', "Files with less than 10 results are expanded. Others are collapsed."), '', '' ], diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index a4a1619e06..e283f72bbe 100644 --- a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -10,8 +10,8 @@ import { stripWildcards } from 'vs/base/common/strings'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ThrottledDelayer } from 'vs/base/common/async'; -import { getWorkspaceSymbols, IWorkspaceSymbol, IWorkspaceSymbolProvider } from 'vs/workbench/contrib/search/common/search'; -import { SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; +import { getWorkspaceSymbols, IWorkspaceSymbol, IWorkspaceSymbolProvider, IWorkbenchSearchConfiguration } from 'vs/workbench/contrib/search/common/search'; +import { SymbolKinds, SymbolTag, SymbolKind } from 'vs/editor/common/modes'; import { ILabelService } from 'vs/platform/label/common/label'; import { Schemas } from 'vs/base/common/network'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -37,6 +37,16 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider([ + SymbolKind.Class, + SymbolKind.Enum, + SymbolKind.File, + SymbolKind.Interface, + SymbolKind.Namespace, + SymbolKind.Package, + SymbolKind.Module + ]); + private delayer = this._register(new ThrottledDelayer(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY)); private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService)); @@ -53,18 +63,20 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider().workbench.editor; + const searchConfig = this.configurationService.getValue(); return { openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection + openSideBySideDirection: editorConfig.openSideBySideDirection, + workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter }; } protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { - return this.getSymbolPicks(filter, undefined, token); + return this.getSymbolPicks(filter, { skipLocal: this.configuration.workspaceSymbolsFilter === 'reduced' }, token); } - async getSymbolPicks(filter: string, options: { skipLocal: boolean, skipSorting: boolean, delay: number } | undefined, token: CancellationToken): Promise> { + async getSymbolPicks(filter: string, options: { skipLocal?: boolean, skipSorting?: boolean, delay?: number } | undefined, token: CancellationToken): Promise> { return this.delayer.trigger(async () => { if (token.isCancellationRequested) { return []; @@ -74,7 +86,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider> { + private async doGetSymbolPicks(filter: string, options: { skipLocal?: boolean, skipSorting?: boolean } | undefined, token: CancellationToken): Promise> { const workspaceSymbols = await getWorkspaceSymbols(filter, token); if (token.isCancellationRequested) { return []; @@ -92,8 +104,12 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider(); for (const [provider, symbols] of workspaceSymbols) { for (const symbol of symbols) { - if (options?.skipLocal && !!symbol.containerName) { - continue; // ignore local symbols if we are told so + + // Depending on the workspace symbols filter setting, skip over symbols that: + // - do not have a container + // - and are not treated explicitly as global symbols (e.g. classes) + if (options?.skipLocal && !SymbolsQuickAccessProvider.TREAT_AS_GLOBAL_SYMBOL_TYPES.has(symbol.kind) && !!symbol.containerName) { + continue; } // Score by symbol label diff --git a/src/vs/workbench/contrib/search/common/search.ts b/src/vs/workbench/contrib/search/common/search.ts index a9db9a2b3b..cd510a3e49 100644 --- a/src/vs/workbench/contrib/search/common/search.ts +++ b/src/vs/workbench/contrib/search/common/search.ts @@ -77,6 +77,7 @@ export interface IWorkbenchSearchConfigurationProperties extends ISearchConfigur quickOpen: { includeSymbols: boolean; includeHistory: boolean; + workspaceSymbolsFilter: 'default' | 'reduced' | 'all'; }; } @@ -102,7 +103,12 @@ export function getOutOfWorkspaceEditorResources(accessor: ServicesAccessor): UR // Supports patterns of <#|:|(><#|:|,> const LINE_COLON_PATTERN = /\s?[#:\(](\d*)([#:,](\d*))?\)?\s*$/; -export function extractRangeFromFilter(filter: string): { filter: string, range: IRange } | undefined { +export interface IFilterAndRange { + filter: string; + range: IRange; +} + +export function extractRangeFromFilter(filter: string): IFilterAndRange | undefined { if (!filter) { return undefined; } @@ -151,7 +157,7 @@ export function extractRangeFromFilter(filter: string): { filter: string, range: if (patternMatch && range) { return { filter: filter.substr(0, patternMatch.index), // clear range suffix from search value - range: range + range }; } diff --git a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts index 516611ebd7..655eca2457 100644 --- a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts @@ -87,7 +87,7 @@ async function computePicks(snippetService: ISnippetsService, envService: IEnvir } } - const dir = joinPath(envService.userRoamingDataHome, 'snippets'); + const dir = envService.snippetsHome; for (const mode of modeService.getRegisteredModes()) { const label = modeService.getLanguageName(mode); if (label && !seen.has(mode)) { @@ -219,7 +219,7 @@ CommandsRegistry.registerCommand(id, async (accessor): Promise => { const globalSnippetPicks: SnippetPick[] = [{ scope: nls.localize('new.global_scope', 'global'), label: nls.localize('new.global', "New Global Snippets file..."), - uri: joinPath(envService.userRoamingDataHome, 'snippets') + uri: envService.snippetsHome }]; const workspaceSnippetPicks: SnippetPick[] = []; diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index b5c0313bd6..8e8350aca0 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -289,7 +289,7 @@ class SnippetsService implements ISnippetsService { } private _initUserSnippets(): Promise { - const userSnippetsFolder = resources.joinPath(this._environmentService.userRoamingDataHome, 'snippets'); + const userSnippetsFolder = this._environmentService.snippetsHome; return this._fileService.createFolder(userSnippetsFolder).then(() => this._initFolderSnippets(SnippetSource.User, userSnippetsFolder, this._disposables)); } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 751140c1bc..5a3b5a6429 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -368,11 +368,7 @@ export class TerminalTaskSystem implements ITaskSystem { }); } - private removeFromActiveTasks(task: Task): void { - if (!this.activeTasks[task.getMapKey()]) { - return; - } - delete this.activeTasks[task.getMapKey()]; + private removeInstances(task: Task) { let commonKey = task._id.split('|')[0]; if (this.instances[commonKey]) { this.instances[commonKey].removeInstance(); @@ -382,6 +378,14 @@ export class TerminalTaskSystem implements ITaskSystem { } } + private removeFromActiveTasks(task: Task): void { + if (!this.activeTasks[task.getMapKey()]) { + return; + } + delete this.activeTasks[task.getMapKey()]; + this.removeInstances(task); + } + public terminate(task: Task): Promise { let activeTerminal = this.activeTasks[task.getMapKey()]; if (!activeTerminal) { @@ -466,6 +470,7 @@ export class TerminalTaskSystem implements ITaskSystem { return Promise.all(promises).then((summaries): Promise | ITaskSummary => { for (let summary of summaries) { if (summary.exitCode !== 0) { + this.removeInstances(task); return { exitCode: summary.exitCode }; } } diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 52e5c46138..73dd901545 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -20,7 +20,7 @@ import { configurationTelemetry } from 'vs/platform/telemetry/common/telemetryUt import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ITextFileService, ITextFileSaveEvent, ITextFileLoadEvent } from 'vs/workbench/services/textfile/common/textfiles'; -import { extname, basename, isEqual, isEqualOrParent, joinPath } from 'vs/base/common/resources'; +import { extname, basename, isEqual, isEqualOrParent } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { guessMimeTypes } from 'vs/base/common/mime'; @@ -175,7 +175,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr } // Check for snippets - if (isEqualOrParent(resource, joinPath(this.environmentService.userRoamingDataHome, 'snippets'))) { + if (isEqualOrParent(resource, this.environmentService.snippetsHome)) { return 'snippets'; } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 7d26e63598..eb4f396e3e 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -9,7 +9,7 @@ import { canceled, isPromiseCanceledError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; @@ -32,7 +32,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, CONTEXT_SYNC_ENABLEMENT, - SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview, getSyncResourceFromRemotePreview + SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview } from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; @@ -69,6 +69,7 @@ function getSyncAreaLabel(source: SyncResource): string { switch (source) { case SyncResource.Settings: return localize('settings', "Settings"); case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); + case SyncResource.Snippets: return localize('snippets', "User Snippets"); case SyncResource.Extensions: return localize('extensions', "Extensions"); case SyncResource.GlobalState: return localize('ui state label', "UI State"); } @@ -100,6 +101,7 @@ const signInCommand = { id: 'workbench.userData.actions.signin', title: localize const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Sync: Turn off Sync"), authenticationProviderId, account, authenticationService); } }; const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolveSettingsConflicts', title: localize('showConflicts', "Sync: Show Settings Conflicts") }; const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Sync: Show Keybindings Conflicts") }; +const resolveSnippetsConflictsCommand = { id: 'workbench.userData.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Sync: Show User Snippets Conflicts") }; const configureSyncCommand = { id: 'workbench.userData.actions.configureSync', title: localize('configure sync', "Sync: Configure") }; const showSyncActivityCommand = { id: 'workbench.userData.actions.showSyncActivity', title(userDataSyncService: IUserDataSyncService): string { @@ -291,6 +293,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (conflicts.length) { const conflictsSources: SyncResource[] = conflicts.map(conflict => conflict.syncResource); this.conflictsSources.set(conflictsSources.join(',')); + if (conflictsSources.indexOf(SyncResource.Snippets) !== -1) { + this.registerShowSnippetsConflictsAction(); + } // Clear and dispose conflicts those were cleared this.conflictsDisposables.forEach((disposable, conflictsSource) => { @@ -301,8 +306,19 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); for (const { syncResource, conflicts } of this.userDataSyncService.conflicts) { - const conflictsEditorInput = this.getConflictsEditorInput(syncResource); - if (!conflictsEditorInput && !this.conflictsDisposables.has(syncResource)) { + const conflictsEditorInputs = this.getConflictsEditorInputs(syncResource); + + // close stale conflicts editor previews + if (conflictsEditorInputs.length) { + conflictsEditorInputs.forEach(input => { + if (!conflicts.some(({ local }) => isEqual(local, input.master.resource))) { + input.dispose(); + } + }); + } + + // Show conflicts notification if not shown before + else if (!this.conflictsDisposables.has(syncResource)) { const conflictsArea = getSyncAreaLabel(syncResource); const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts in {0}. Please resolve them to continue.", conflictsArea.toLowerCase()), [ @@ -338,9 +354,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo handle.close(); // close opened conflicts editor previews - const conflictsEditorInput = this.getConflictsEditorInput(syncResource); - if (conflictsEditorInput) { - conflictsEditorInput.dispose(); + const conflictsEditorInputs = this.getConflictsEditorInputs(syncResource); + if (conflictsEditorInputs.length) { + conflictsEditorInputs.forEach(input => input.dispose()); } this.conflictsDisposables.delete(syncResource); @@ -496,7 +512,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.authenticationState.get() === AuthStatus.SignedOut) { badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync")); } else if (this.userDataSyncService.conflicts.length) { - badge = new NumberBadge(this.userDataSyncService.conflicts.length, () => localize('has conflicts', "Sync: Conflicts Detected")); + badge = new NumberBadge(this.userDataSyncService.conflicts.reduce((result, syncResourceConflict) => { return result + syncResourceConflict.conflicts.length; }, 0), () => localize('has conflicts', "Sync: Conflicts Detected")); } if (badge) { @@ -605,6 +621,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }, { id: SyncResource.Keybindings, label: getSyncAreaLabel(SyncResource.Keybindings) + }, { + id: SyncResource.Snippets, + label: getSyncAreaLabel(SyncResource.Snippets) }, { id: SyncResource.Extensions, label: getSyncAreaLabel(SyncResource.Extensions) @@ -712,6 +731,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo switch (source) { case SyncResource.Settings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Settings, false); case SyncResource.Keybindings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Keybindings, false); + case SyncResource.Snippets: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Snippets, false); case SyncResource.Extensions: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Extensions, false); case SyncResource.GlobalState: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.GlobalState, false); } @@ -727,8 +747,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private getConflictsEditorInput(syncResource: SyncResource): IEditorInput | undefined { - return this.editorService.editors.filter(input => input instanceof DiffEditorInput && getSyncResourceFromLocalPreview(input.master.resource!, this.workbenchEnvironmentService) === syncResource)[0]; + private getConflictsEditorInputs(syncResource: SyncResource): DiffEditorInput[] { + return this.editorService.editors.filter(input => { + const resource = input instanceof DiffEditorInput ? input.master.resource : input.resource; + return getSyncResourceFromLocalPreview(resource!, this.workbenchEnvironmentService) === syncResource; + }) as DiffEditorInput[]; } private getAllConflictsEditorInputs(): IEditorInput[] { @@ -752,6 +775,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo label = localize('settings conflicts preview', "Settings Conflicts (Remote ↔ Local)"); } else if (syncResource === SyncResource.Keybindings) { label = localize('keybindings conflicts preview', "Keybindings Conflicts (Remote ↔ Local)"); + } else if (syncResource === SyncResource.Snippets) { + label = localize('snippets conflicts preview', "User Snippet Conflicts (Remote ↔ Local) - {0}", basename(conflict.local)); } await this.editorService.openEditor({ leftResource: conflict.remote, @@ -775,6 +800,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.registerSignInAction(); this.registerShowSettingsConflictsAction(); this.registerShowKeybindingsConflictsAction(); + this.registerShowSnippetsConflictsAction(); this.registerSyncStatusAction(); this.registerTurnOffSyncAction(); @@ -894,7 +920,36 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo command: resolveKeybindingsConflictsCommand, when: resolveKeybindingsConflictsWhenContext, }); + } + private _snippetsConflictsActionsDisposable: DisposableStore = new DisposableStore(); + private registerShowSnippetsConflictsAction(): void { + this._snippetsConflictsActionsDisposable.clear(); + const resolveSnippetsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*snippets.*/i); + const conflicts: Conflict[] | undefined = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === SyncResource.Snippets)[0]?.conflicts; + this._snippetsConflictsActionsDisposable.add(CommandsRegistry.registerCommand(resolveSnippetsConflictsCommand.id, () => this.handleSyncResourceConflicts(SyncResource.Snippets))); + this._snippetsConflictsActionsDisposable.add(MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '5_sync', + command: { + id: resolveSnippetsConflictsCommand.id, + title: localize('resolveSnippetsConflicts_global', "Sync: Show User Snippets Conflicts ({0})", conflicts?.length || 1), + }, + when: resolveSnippetsConflictsWhenContext, + order: 2 + })); + this._snippetsConflictsActionsDisposable.add(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '5_sync', + command: { + id: resolveSnippetsConflictsCommand.id, + title: localize('resolveSnippetsConflicts_global', "Sync: Show User Snippets Conflicts ({0})", conflicts?.length || 1), + }, + when: resolveSnippetsConflictsWhenContext, + order: 2 + })); + this._snippetsConflictsActionsDisposable.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: resolveSnippetsConflictsCommand, + when: resolveSnippetsConflictsWhenContext, + })); } private registerSyncStatusAction(): void { @@ -938,6 +993,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case SyncResource.Keybindings: items.push({ id: resolveKeybindingsConflictsCommand.id, label: resolveKeybindingsConflictsCommand.title }); break; + case SyncResource.Snippets: + items.push({ id: resolveSnippetsConflictsCommand.id, label: resolveSnippetsConflictsCommand.title }); + break; } } items.push({ type: 'separator' }); @@ -1074,7 +1132,6 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio constructor( private editor: ICodeEditor, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, @@ -1088,7 +1145,8 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio } private registerListeners(): void { - this._register(this.editor.onDidChangeModel(e => this.update())); + this._register(this.editor.onDidChangeModel(() => this.update())); + this._register(this.userDataSyncService.onDidChangeConflicts(() => this.update())); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('diffEditor.renderSideBySide'))(() => this.update())); } @@ -1107,11 +1165,16 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio return false; // we need a model } - if (getSyncResourceFromLocalPreview(model.uri, this.environmentService) !== undefined) { + const syncResourceConflicts = this.getSyncResourceConflicts(model.uri); + if (!syncResourceConflicts) { + return false; + } + + if (syncResourceConflicts.conflicts.some(({ local }) => isEqual(local, model.uri))) { return true; } - if (getSyncResourceFromRemotePreview(model.uri, this.environmentService) !== undefined) { + if (syncResourceConflicts.conflicts.some(({ remote }) => isEqual(remote, model.uri))) { return this.configurationService.getValue('diffEditor.renderSideBySide'); } @@ -1121,16 +1184,17 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio private createAcceptChangesWidgetRenderer(): void { if (!this.acceptChangesButton) { - const isRemote = getSyncResourceFromRemotePreview(this.editor.getModel()!.uri, this.environmentService) !== undefined; + const resource = this.editor.getModel()!.uri; + const syncResourceConflicts = this.getSyncResourceConflicts(resource)!; + const isRemote = syncResourceConflicts.conflicts.some(({ remote }) => isEqual(remote, resource)); const acceptRemoteLabel = localize('accept remote', "Accept Remote"); const acceptLocalLabel = localize('accept local', "Accept Local"); this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptLocalLabel, null); this._register(this.acceptChangesButton.onClick(async () => { const model = this.editor.getModel(); if (model) { - const conflictsSource = (getSyncResourceFromLocalPreview(model.uri, this.environmentService) || getSyncResourceFromRemotePreview(model.uri, this.environmentService))!; - this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: isRemote ? 'acceptRemote' : 'acceptLocal' }); - const syncAreaLabel = getSyncAreaLabel(conflictsSource); + this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: syncResourceConflicts.syncResource, action: isRemote ? 'acceptRemote' : 'acceptLocal' }); + const syncAreaLabel = getSyncAreaLabel(syncResourceConflicts.syncResource); const result = await this.dialogService.confirm({ type: 'info', title: isRemote @@ -1146,7 +1210,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio await this.userDataSyncService.acceptConflict(model.uri, model.getValue()); } catch (e) { if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.LocalPreconditionFailed) { - const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === conflictsSource)[0]; + const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === syncResourceConflicts.syncResource)[0]; if (syncResourceCoflicts && syncResourceCoflicts.conflicts.some(conflict => isEqual(conflict.local, model.uri) || isEqual(conflict.remote, model.uri))) { this.notificationService.warn(localize('update conflicts', "Could not resolve conflicts as there is new local version available. Please try again.")); } @@ -1162,6 +1226,10 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio } } + private getSyncResourceConflicts(resource: URI): SyncResourceConflicts | undefined { + return this.userDataSyncService.conflicts.filter(({ conflicts }) => conflicts.some(({ local, remote }) => isEqual(local, resource) || isEqual(remote, resource)))[0]; + } + private disposeAcceptChangesWidgetRenderer(): void { dispose(this.acceptChangesButton); this.acceptChangesButton = undefined; diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index c73b55ca73..c916da1db7 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -125,10 +125,11 @@ }`; /** + * @param {boolean} allowMultipleAPIAcquire * @param {*} [state] * @return {string} */ - function getVsCodeApiScript(state) { + function getVsCodeApiScript(allowMultipleAPIAcquire, state) { return ` const acquireVsCodeApi = (function() { const originalPostMessage = window.parent.postMessage.bind(window.parent); @@ -138,7 +139,7 @@ let state = ${state ? `JSON.parse(${JSON.stringify(state)})` : undefined}; return () => { - if (acquired) { + if (acquired && !${allowMultipleAPIAcquire}) { throw new Error('An instance of the VS Code API has already been acquired'); } acquired = true; @@ -325,7 +326,7 @@ if (options.allowScripts) { const defaultScript = newDocument.createElement('script'); defaultScript.id = '_vscodeApiScript'; - defaultScript.textContent = getVsCodeApiScript(data.state); + defaultScript.textContent = getVsCodeApiScript(options.allowMultipleAPIAcquire, data.state); newDocument.head.prepend(defaultScript); } diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index f68282d0c1..4a793bca73 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -59,6 +59,7 @@ export interface WebviewOptions { } export interface WebviewContentOptions { + readonly allowMultipleAPIAcquire?: boolean; readonly allowScripts?: boolean; readonly localResourceRoots?: ReadonlyArray; readonly portMapping?: ReadonlyArray; diff --git a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts index ece8934d0b..6a46abce92 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts @@ -36,6 +36,7 @@ export function areWebviewInputOptionsEqual(a: WebviewInputOptions, b: WebviewIn return a.enableCommandUris === b.enableCommandUris && a.enableFindWidget === b.enableFindWidget && a.allowScripts === b.allowScripts + && a.allowMultipleAPIAcquire === b.allowMultipleAPIAcquire && a.retainContextWhenHidden === b.retainContextWhenHidden && a.tryRestoreScrollPosition === b.tryRestoreScrollPosition && equals(a.localResourceRoots, b.localResourceRoots, isEqual) diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 2af3d16ff9..22939154fe 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -102,6 +102,9 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); } + @memoize + get snippetsHome(): URI { return joinPath(this.userRoamingDataHome, 'snippets'); } + @memoize get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); }