Merge from vscode 5d18ad4c5902e3bddbc9f78da82dfc2ac349e908 (#9683)

This commit is contained in:
Anthony Dresser
2020-03-20 01:17:27 -07:00
committed by GitHub
parent 1520441b84
commit dd8fb9433b
89 changed files with 3095 additions and 445 deletions
+2 -1
View File
@@ -341,7 +341,8 @@ export class AzureActiveDirectoryService {
const query = parseQuery(uri); const query = parseQuery(uri);
const code = query.code; 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.'); throw new Error('State does not match.');
} }
@@ -360,7 +360,6 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
// Match quickOpen outline styles - ignore for disabled options // Match quickOpen outline styles - ignore for disabled options
if (this.styles.listFocusOutline) { 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; }`); 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) { if (this.styles.listHoverOutline) {
@@ -55,6 +55,12 @@
margin-bottom: -2px; 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 { .quick-input-and-message {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -126,6 +132,10 @@
margin-top: 6px; 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 { .quick-input-list .monaco-list {
overflow: hidden; overflow: hidden;
max-height: calc(20 * 22px); max-height: calc(20 * 22px);
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/quickInput'; 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 * as dom from 'vs/base/browser/dom';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { QuickInputList } from './quickInputList'; import { QuickInputList } from './quickInputList';
@@ -125,6 +125,7 @@ type Visibilities = {
list?: boolean; list?: boolean;
ok?: boolean; ok?: boolean;
customButton?: boolean; customButton?: boolean;
progressBar?: boolean;
}; };
class QuickInput extends Disposable implements IQuickInput { class QuickInput extends Disposable implements IQuickInput {
@@ -406,8 +407,16 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
private _customButton = false; private _customButton = false;
private _customButtonLabel: string | undefined; private _customButtonLabel: string | undefined;
private _customButtonHover: 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() { get value() {
return this._value; return this._value;
@@ -451,6 +460,10 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
set items(items: Array<T | IQuickPickSeparator>) { set items(items: Array<T | IQuickPickSeparator>) {
this._items = items; this._items = items;
this.itemsUpdated = true; this.itemsUpdated = true;
if (this._items.length === 0) {
// quick-navigate requires at least 1 item
this._quickNavigate = undefined;
}
this.update(); this.update();
} }
@@ -540,6 +553,13 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
} }
get keyMods() { 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; return this.ui.keyMods;
} }
@@ -622,8 +642,10 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
return; return;
} }
this._value = value; this._value = value;
this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); const didFilter = this.ui.list.filter(this.filterValue(this.ui.inputBox.value));
if (didFilter) {
this.trySelectFirst(); this.trySelectFirst();
}
this.onDidChangeValueEmitter.fire(value); this.onDidChangeValueEmitter.fire(value);
})); }));
this.visibleDisposables.add(this.ui.inputBox.onMouseDown(event => { this.visibleDisposables.add(this.ui.inputBox.onMouseDown(event => {
@@ -796,8 +818,12 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
if (!this.visible) { if (!this.visible) {
return; return;
} }
dom.toggleClass(this.ui.container, 'quick-navigate-mode', !!this._quickNavigate);
const ok = this.ok === 'default' ? this.canSelectMany : this.ok; 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(); super.update();
if (this.ui.inputBox.value !== this.value) { if (this.ui.inputBox.value !== this.value) {
this.ui.inputBox.value = this.value; this.ui.inputBox.value = this.value;
@@ -818,12 +844,18 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.ui.list.sortByLabel = this.sortByLabel; this.ui.list.sortByLabel = this.sortByLabel;
if (this.itemsUpdated) { if (this.itemsUpdated) {
this.itemsUpdated = false; this.itemsUpdated = false;
const previousItemCount = this.ui.list.getElementsCount();
this.ui.list.setElements(this.items); this.ui.list.setElements(this.items);
this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); this.ui.list.filter(this.filterValue(this.ui.inputBox.value));
this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked();
this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); this.ui.visibleCount.setCount(this.ui.list.getVisibleCount());
this.ui.count.setCount(this.ui.list.getCheckedCount()); this.ui.count.setCount(this.ui.list.getCheckedCount());
this.trySelectFirst(); 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.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) {
if (this.canSelectMany) { if (this.canSelectMany) {
@@ -862,6 +894,11 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.ui.customButton.label = this.customLabel || ''; this.ui.customButton.label = this.customLabel || '';
this.ui.customButton.element.title = this.customHover || ''; this.ui.customButton.element.title = this.customHover || '';
this.ui.setComboboxAccessibility(true); 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.okContainer.style.display = visibilities.ok ? '' : 'none';
ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none'; ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none';
ui.message.style.display = visibilities.message ? '' : 'none'; ui.message.style.display = visibilities.message ? '' : 'none';
ui.progressBar.getContainer().style.display = visibilities.progressBar ? '' : 'none';
ui.list.display(!!visibilities.list); ui.list.display(!!visibilities.list);
ui.container.classList[visibilities.checkAll ? 'add' : 'remove']('show-checkboxes'); ui.container.classList[visibilities.checkAll ? 'add' : 'remove']('show-checkboxes');
this.updateLayout(); // TODO this.updateLayout(); // TODO
@@ -416,6 +416,10 @@ export class QuickInputList {
this._onChangedVisibleCount.fire(this.elements.length); this._onChangedVisibleCount.fire(this.elements.length);
} }
getElementsCount(): number {
return this.inputElements.length;
}
getFocusedElements() { getFocusedElements() {
return this.list.getFocusedElements() return this.list.getFocusedElements()
.map(e => e.item); .map(e => e.item);
@@ -498,10 +502,10 @@ export class QuickInputList {
this.list.layout(); this.list.layout();
} }
filter(query: string) { filter(query: string): boolean {
if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) { if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) {
this.list.layout(); this.list.layout();
return; return false;
} }
query = query.trim(); query = query.trim();
@@ -559,6 +563,8 @@ export class QuickInputList {
this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked());
this._onChangedVisibleCount.fire(shownElements.length); this._onChangedVisibleCount.fire(shownElements.length);
return true;
} }
toggleCheckbox() { toggleCheckbox() {
@@ -49,6 +49,8 @@ export interface IKeyMods {
readonly alt: boolean; readonly alt: boolean;
} }
export const NO_KEY_MODS: IKeyMods = { ctrlCmd: false, alt: false };
export interface IQuickNavigateConfiguration { export interface IQuickNavigateConfiguration {
keybindings: ResolvedKeybinding[]; keybindings: ResolvedKeybinding[];
} }
+9 -4
View File
@@ -747,13 +747,13 @@ export class DiffReview extends Disposable {
let ariaLabel: string = ''; let ariaLabel: string = '';
switch (type) { switch (type) {
case DiffEntryType.Equal: 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; break;
case DiffEntryType.Insert: case DiffEntryType.Insert:
ariaLabel = nls.localize('insertLine', "+ modified {0}: {1}", modifiedLine, lineContent); ariaLabel = nls.localize('insertLine', "+ {0} modified line {1}", lineContent, modifiedLine);
break; break;
case DiffEntryType.Delete: case DiffEntryType.Delete:
ariaLabel = nls.localize('deleteLine', "- original {0}: {1}", originalLine, lineContent); ariaLabel = nls.localize('deleteLine', "- {0} original line {1}", lineContent, originalLine);
break; break;
} }
row.setAttribute('aria-label', ariaLabel); row.setAttribute('aria-label', ariaLabel);
@@ -869,9 +869,14 @@ class DiffReviewPrev extends EditorAction {
function findFocusedDiffEditor(accessor: ServicesAccessor): DiffEditorWidget | null { function findFocusedDiffEditor(accessor: ServicesAccessor): DiffEditorWidget | null {
const codeEditorService = accessor.get(ICodeEditorService); const codeEditorService = accessor.get(ICodeEditorService);
const diffEditors = codeEditorService.listDiffEditors(); const diffEditors = codeEditorService.listDiffEditors();
const activeCodeEditor = codeEditorService.getActiveCodeEditor();
if (!activeCodeEditor) {
return null;
}
for (let i = 0, len = diffEditors.length; i < len; i++) { for (let i = 0, len = diffEditors.length; i < len; i++) {
const diffEditor = <DiffEditorWidget>diffEditors[i]; const diffEditor = <DiffEditorWidget>diffEditors[i];
if (diffEditor.hasWidgetFocus()) { if (diffEditor.getModifiedEditor().getId() === activeCodeEditor.getId() || diffEditor.getOriginalEditor().getId() === activeCodeEditor.getId()) {
return diffEditor; return diffEditor;
} }
} }
@@ -2841,6 +2841,14 @@ export interface ISuggestOptions {
* Show typeParameter-suggestions. * Show typeParameter-suggestions.
*/ */
showTypeParameters?: boolean; showTypeParameters?: boolean;
/**
* Show issue-suggestions.
*/
showIssues?: boolean;
/**
* Show user-suggestions.
*/
showUsers?: boolean;
/** /**
* Show snippet-suggestions. * Show snippet-suggestions.
*/ */
@@ -2895,6 +2903,8 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
showFolders: true, showFolders: true,
showTypeParameters: true, showTypeParameters: true,
showSnippets: true, showSnippets: true,
showUsers: true,
showIssues: true,
statusBar: { statusBar: {
visible: false visible: false
} }
@@ -3083,6 +3093,16 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
default: true, default: true,
markdownDescription: nls.localize('editor.suggest.showSnippets', "When enabled IntelliSense shows `snippet`-suggestions.") markdownDescription: nls.localize('editor.suggest.showSnippets', "When enabled IntelliSense shows `snippet`-suggestions.")
}, },
'editor.suggest.showUsers': {
type: 'boolean',
default: true,
markdownDescription: nls.localize('editor.suggest.showUsers', "When enabled IntelliSense shows `user`-suggestions.")
},
'editor.suggest.showIssues': {
type: 'boolean',
default: true,
markdownDescription: nls.localize('editor.suggest.showIssues', "When enabled IntelliSense shows `issues`-suggestions.")
},
'editor.suggest.statusBar.visible': { 'editor.suggest.statusBar.visible': {
type: 'boolean', type: 'boolean',
default: false, default: false,
@@ -3131,6 +3151,8 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
showFolders: EditorBooleanOption.boolean(input.showFolders, this.defaultValue.showFolders), showFolders: EditorBooleanOption.boolean(input.showFolders, this.defaultValue.showFolders),
showTypeParameters: EditorBooleanOption.boolean(input.showTypeParameters, this.defaultValue.showTypeParameters), showTypeParameters: EditorBooleanOption.boolean(input.showTypeParameters, this.defaultValue.showTypeParameters),
showSnippets: EditorBooleanOption.boolean(input.showSnippets, this.defaultValue.showSnippets), showSnippets: EditorBooleanOption.boolean(input.showSnippets, this.defaultValue.showSnippets),
showUsers: EditorBooleanOption.boolean(input.showUsers, this.defaultValue.showUsers),
showIssues: EditorBooleanOption.boolean(input.showIssues, this.defaultValue.showIssues),
statusBar: { statusBar: {
visible: EditorBooleanOption.boolean(input.statusBar?.visible, !!this.defaultValue.statusBar.visible) visible: EditorBooleanOption.boolean(input.statusBar?.visible, !!this.defaultValue.statusBar.visible)
} }
+32 -27
View File
@@ -319,6 +319,8 @@ export const enum CompletionItemKind {
Customcolor, Customcolor,
Folder, Folder,
TypeParameter, TypeParameter,
User,
Issue,
Snippet, // <- highest value (used for compare!) Snippet, // <- highest value (used for compare!)
} }
@@ -327,32 +329,34 @@ export const enum CompletionItemKind {
*/ */
export const completionKindToCssClass = (function () { export const completionKindToCssClass = (function () {
let data = Object.create(null); let data = Object.create(null);
data[CompletionItemKind.Method] = 'method'; data[CompletionItemKind.Method] = 'symbol-method';
data[CompletionItemKind.Function] = 'function'; data[CompletionItemKind.Function] = 'symbol-function';
data[CompletionItemKind.Constructor] = 'constructor'; data[CompletionItemKind.Constructor] = 'symbol-constructor';
data[CompletionItemKind.Field] = 'field'; data[CompletionItemKind.Field] = 'symbol-field';
data[CompletionItemKind.Variable] = 'variable'; data[CompletionItemKind.Variable] = 'symbol-variable';
data[CompletionItemKind.Class] = 'class'; data[CompletionItemKind.Class] = 'symbol-class';
data[CompletionItemKind.Struct] = 'struct'; data[CompletionItemKind.Struct] = 'symbol-struct';
data[CompletionItemKind.Interface] = 'interface'; data[CompletionItemKind.Interface] = 'symbol-interface';
data[CompletionItemKind.Module] = 'module'; data[CompletionItemKind.Module] = 'symbol-module';
data[CompletionItemKind.Property] = 'property'; data[CompletionItemKind.Property] = 'symbol-property';
data[CompletionItemKind.Event] = 'event'; data[CompletionItemKind.Event] = 'symbol-event';
data[CompletionItemKind.Operator] = 'operator'; data[CompletionItemKind.Operator] = 'symbol-operator';
data[CompletionItemKind.Unit] = 'unit'; data[CompletionItemKind.Unit] = 'symbol-unit';
data[CompletionItemKind.Value] = 'value'; data[CompletionItemKind.Value] = 'symbol-value';
data[CompletionItemKind.Constant] = 'constant'; data[CompletionItemKind.Constant] = 'symbol-constant';
data[CompletionItemKind.Enum] = 'enum'; data[CompletionItemKind.Enum] = 'symbol-enum';
data[CompletionItemKind.EnumMember] = 'enum-member'; data[CompletionItemKind.EnumMember] = 'symbol-enum-member';
data[CompletionItemKind.Keyword] = 'keyword'; data[CompletionItemKind.Keyword] = 'symbol-keyword';
data[CompletionItemKind.Snippet] = 'snippet'; data[CompletionItemKind.Snippet] = 'symbol-snippet';
data[CompletionItemKind.Text] = 'text'; data[CompletionItemKind.Text] = 'symbol-text';
data[CompletionItemKind.Color] = 'color'; data[CompletionItemKind.Color] = 'symbol-color';
data[CompletionItemKind.File] = 'file'; data[CompletionItemKind.File] = 'symbol-file';
data[CompletionItemKind.Reference] = 'reference'; data[CompletionItemKind.Reference] = 'symbol-reference';
data[CompletionItemKind.Customcolor] = 'customcolor'; data[CompletionItemKind.Customcolor] = 'symbol-customcolor';
data[CompletionItemKind.Folder] = 'folder'; data[CompletionItemKind.Folder] = 'symbol-folder';
data[CompletionItemKind.TypeParameter] = 'type-parameter'; data[CompletionItemKind.TypeParameter] = 'symbol-type-parameter';
data[CompletionItemKind.User] = 'account';
data[CompletionItemKind.Issue] = 'issues';
return function (kind: CompletionItemKind) { return function (kind: CompletionItemKind) {
return data[kind] || 'property'; return data[kind] || 'property';
@@ -395,7 +399,8 @@ export let completionKindFromString: {
data['folder'] = CompletionItemKind.Folder; data['folder'] = CompletionItemKind.Folder;
data['type-parameter'] = CompletionItemKind.TypeParameter; data['type-parameter'] = CompletionItemKind.TypeParameter;
data['typeParameter'] = CompletionItemKind.TypeParameter; data['typeParameter'] = CompletionItemKind.TypeParameter;
data['account'] = CompletionItemKind.User;
data['issue'] = CompletionItemKind.Issue;
return function (value: string, strict?: true) { return function (value: string, strict?: true) {
let res = data[value]; let res = data[value];
if (typeof res === 'undefined' && !strict) { if (typeof res === 'undefined' && !strict) {
@@ -53,7 +53,9 @@ export enum CompletionItemKind {
Customcolor = 22, Customcolor = 22,
Folder = 23, Folder = 23,
TypeParameter = 24, TypeParameter = 24,
Snippet = 25 User = 25,
Issue = 26,
Snippet = 27
} }
export enum CompletionItemTag { export enum CompletionItemTag {
@@ -13,7 +13,7 @@ import { IQuickPick, IQuickPickItem, IKeyMods } from 'vs/platform/quickinput/com
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { IDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { IDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { isDiffEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { withNullAsUndefined } from 'vs/base/common/types'; import { withNullAsUndefined } from 'vs/base/common/types';
import { once } from 'vs/base/common/functional'; import { once } from 'vs/base/common/functional';
@@ -59,12 +59,24 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu
// Restore any view state if this picker was closed // Restore any view state if this picker was closed
// without actually going to a line // without actually going to a line
const lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); 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)(() => { once(token.onCancellationRequested)(() => {
if (lastKnownEditorViewState) { if (lastKnownEditorViewState) {
editor.restoreViewState(lastKnownEditorViewState); editor.restoreViewState(lastKnownEditorViewState);
} }
}); });
}
// Clean up decorations on dispose // Clean up decorations on dispose
disposables.add(toDisposable(() => this.clearDecorations(editor))); disposables.add(toDisposable(() => this.clearDecorations(editor)));
@@ -98,9 +110,9 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu
*/ */
protected abstract provideWithoutTextEditor(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable; protected abstract provideWithoutTextEditor(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable;
protected gotoLocation(editor: IEditor, range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean): void { protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean }): void {
editor.setSelection(range); editor.setSelection(options.range);
editor.revealRangeInCenter(range, ScrollType.Smooth); editor.revealRangeInCenter(options.range, ScrollType.Smooth);
editor.focus(); editor.focus();
} }
@@ -38,7 +38,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
return; 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(); picker.hide();
} }
@@ -95,7 +95,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
disposables.add(picker.onDidAccept(() => { disposables.add(picker.onDidAccept(() => {
const [item] = picker.selectedItems; const [item] = picker.selectedItems;
if (item && item.range) { if (item && item.range) {
this.gotoLocation(editor, item.range.selection, picker.keyMods); this.gotoLocation(editor, { range: item.range.selection, keyMods: picker.keyMods });
picker.hide(); picker.hide();
} }
@@ -104,7 +104,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
// Goto symbol side by side if enabled // Goto symbol side by side if enabled
disposables.add(picker.onDidTriggerItemButton(({ item }) => { disposables.add(picker.onDidTriggerItemButton(({ item }) => {
if (item && item.range) { 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(); picker.hide();
} }
@@ -195,7 +195,6 @@ class ItemRenderer implements IListRenderer<CompletionItem, ISuggestionTemplateD
const textLabel = typeof suggestion.label === 'string' ? suggestion.label : suggestion.label.name; const textLabel = typeof suggestion.label === 'string' ? suggestion.label : suggestion.label.name;
data.root.id = getAriaId(index); data.root.id = getAriaId(index);
data.icon.className = 'icon ' + completionKindToCssClass(suggestion.kind);
data.colorspan.style.backgroundColor = ''; data.colorspan.style.backgroundColor = '';
const labelOptions: IIconLabelValueOptions = { const labelOptions: IIconLabelValueOptions = {
@@ -230,7 +229,7 @@ class ItemRenderer implements IListRenderer<CompletionItem, ISuggestionTemplateD
// normal icon // normal icon
data.icon.className = 'icon hide'; data.icon.className = 'icon hide';
data.iconContainer.className = ''; data.iconContainer.className = '';
addClasses(data.iconContainer, `suggest-icon codicon codicon-symbol-${completionKindToCssClass(suggestion.kind)}`); addClasses(data.iconContainer, `suggest-icon codicon codicon-${completionKindToCssClass(suggestion.kind)}`);
} }
if (suggestion.tags && suggestion.tags.indexOf(CompletionItemTag.Deprecated) >= 0) { if (suggestion.tags && suggestion.tags.indexOf(CompletionItemTag.Deprecated) >= 0) {
@@ -360,10 +360,8 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider {
const lineHeight = this.editor.getOption(EditorOption.lineHeight); const lineHeight = this.editor.getOption(EditorOption.lineHeight);
// adjust heightInLines to viewport // adjust heightInLines to viewport
const maxHeightInLines = (this.editor.getLayoutInfo().height / lineHeight) * 0.8; const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8);
if (heightInLines >= maxHeightInLines) { heightInLines = Math.min(heightInLines, maxHeightInLines);
heightInLines = maxHeightInLines;
}
let arrowHeight = 0; let arrowHeight = 0;
let frameThickness = 0; let frameThickness = 0;
+11 -1
View File
@@ -3771,6 +3771,14 @@ declare namespace monaco.editor {
* Show typeParameter-suggestions. * Show typeParameter-suggestions.
*/ */
showTypeParameters?: boolean; showTypeParameters?: boolean;
/**
* Show issue-suggestions.
*/
showIssues?: boolean;
/**
* Show user-suggestions.
*/
showUsers?: boolean;
/** /**
* Show snippet-suggestions. * Show snippet-suggestions.
*/ */
@@ -5393,7 +5401,9 @@ declare namespace monaco.languages {
Customcolor = 22, Customcolor = 22,
Folder = 23, Folder = 23,
TypeParameter = 24, TypeParameter = 24,
Snippet = 25 User = 25,
Issue = 26,
Snippet = 27
} }
export interface CompletionItemLabel { export interface CompletionItemLabel {
@@ -114,6 +114,7 @@ export class MenuId {
static readonly CommentThreadActions = new MenuId('CommentThreadActions'); static readonly CommentThreadActions = new MenuId('CommentThreadActions');
static readonly CommentTitle = new MenuId('CommentTitle'); static readonly CommentTitle = new MenuId('CommentTitle');
static readonly CommentActions = new MenuId('CommentActions'); static readonly CommentActions = new MenuId('CommentActions');
static readonly NotebookCellTitle = new MenuId('NotebookCellTitle');
static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditTitle = new MenuId('BulkEditTitle');
static readonly BulkEditContext = new MenuId('BulkEditContext'); static readonly BulkEditContext = new MenuId('BulkEditContext');
static readonly ObjectExplorerItemContext = new MenuId('ObjectExplorerItemContext'); // {{SQL CARBON EDIT}} static readonly ObjectExplorerItemContext = new MenuId('ObjectExplorerItemContext'); // {{SQL CARBON EDIT}}
@@ -132,6 +132,7 @@ export interface IEnvironmentService extends IUserHomeProvider {
keybindingsResource: URI; keybindingsResource: URI;
keyboardLayoutResource: URI; keyboardLayoutResource: URI;
argvResource: URI; argvResource: URI;
snippetsHome: URI;
// sync resources // sync resources
userDataSyncLogResource: URI; userDataSyncLogResource: URI;
@@ -142,6 +142,9 @@ export class EnvironmentService implements IEnvironmentService {
return URI.file(path.join(this.userHome, product.dataFolderName, 'argv.json')); return URI.file(path.join(this.userHome, product.dataFolderName, 'argv.json'));
} }
@memoize
get snippetsHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'snippets'); }
@memoize @memoize
get isExtensionDevelopment(): boolean { return !!this._args.extensionDevelopmentPath; } get isExtensionDevelopment(): boolean { return !!this._args.extensionDevelopmentPath; }
@@ -97,7 +97,14 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
// Collect picks and support both long running and short or combined // Collect picks and support both long running and short or combined
const picksToken = picksCts.token; const picksToken = picksCts.token;
const res = this.getPicks(picker.value.substr(this.prefix.length).trim(), disposables.add(new DisposableStore()), picksToken); const res = this.getPicks(picker.value.substr(this.prefix.length).trim(), disposables.add(new DisposableStore()), picksToken);
if (isFastAndSlowPicksType(res)) {
// No Picks
if (res === null) {
// Ignore
}
// Fast and Slow Picks
else if (isFastAndSlowPicksType(res)) {
let fastPicksHandlerDone = false; let fastPicksHandlerDone = false;
let slowPicksHandlerDone = false; let slowPicksHandlerDone = false;
@@ -122,7 +129,6 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
} }
})(), })(),
// Slow Picks: we await the slow picks and then set them at // Slow Picks: we await the slow picks and then set them at
// once together with the fast picks, but only if we actually // once together with the fast picks, but only if we actually
// have additional results. // have additional results.
@@ -227,6 +233,7 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
* @param token for long running tasks, implementors need to check on cancellation * @param token for long running tasks, implementors need to check on cancellation
* through this token. * through this token.
* @returns the picks either directly, as promise or combined fast and slow results. * @returns the picks either directly, as promise or combined fast and slow results.
* Pickers can return `null` to signal that no change in picks is needed.
*/ */
protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array<T | IQuickPickSeparator> | Promise<Array<T | IQuickPickSeparator>> | FastAndSlowPicksType<T>; protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array<T | IQuickPickSeparator> | Promise<Array<T | IQuickPickSeparator>> | FastAndSlowPicksType<T> | null;
} }
@@ -5,7 +5,7 @@
import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; 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 { Registry } from 'vs/platform/registry/common/platform';
import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@@ -25,7 +25,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
super(); super();
} }
show(value = ''): void { show(value = '', options?: IQuickAccessOptions): void {
const disposables = new DisposableStore(); const disposables = new DisposableStore();
// Hide any previous picker if any // Hide any previous picker if any
@@ -39,7 +39,8 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
const picker = disposables.add(this.quickInputService.createQuickPick()); const picker = disposables.add(this.quickInputService.createQuickPick());
picker.placeholder = descriptor?.placeholder; picker.placeholder = descriptor?.placeholder;
picker.value = value; 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.contextKey = descriptor?.contextKey;
picker.filterValue = (value: string) => value.substring(descriptor ? descriptor.prefix.length : 0); picker.filterValue = (value: string) => value.substring(descriptor ? descriptor.prefix.length : 0);
@@ -3,19 +3,32 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * 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 { CancellationToken } from 'vs/base/common/cancellation';
import { Registry } from 'vs/platform/registry/common/platform'; import { Registry } from 'vs/platform/registry/common/platform';
import { first, coalesce } from 'vs/base/common/arrays'; import { first, coalesce } from 'vs/base/common/arrays';
import { startsWith } from 'vs/base/common/strings'; import { startsWith } from 'vs/base/common/strings';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; 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 { export interface IQuickAccessController {
/** /**
* Open the quick access picker with the optional value prefilled. * Open the quick access picker with the optional value prefilled.
*/ */
show(value?: string): void; show(value?: string, options?: IQuickAccessOptions): void;
} }
export interface IQuickAccessProvider { export interface IQuickAccessProvider {
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle'; 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 { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri'; 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'; 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<void> { async sync(ref?: string): Promise<void> {
if (!this.isEnabled()) { 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.`); this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`);
return; return;
} }
@@ -264,6 +267,7 @@ export abstract class AbstractSynchroniser extends Disposable {
protected abstract readonly version: number; protected abstract readonly version: number;
protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>; protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>;
abstract stop(): Promise<void>;
} }
export interface IFileSyncPreviewResult { export interface IFileSyncPreviewResult {
@@ -299,7 +303,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
async stop(): Promise<void> { async stop(): Promise<void> {
this.cancel(); this.cancel();
this.logService.trace(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`); this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`);
try { try {
await this.fileService.del(this.localPreviewResource); await this.fileService.del(this.localPreviewResource);
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
@@ -339,7 +343,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false }); await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false });
} }
} catch (e) { } 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)) { (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) {
throw new UserDataSyncError(e.message, UserDataSyncErrorCode.LocalPreconditionFailed); throw new UserDataSyncError(e.message, UserDataSyncErrorCode.LocalPreconditionFailed);
} else { } else {
@@ -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<string>;
updated: IStringDictionary<string>;
removed: string[];
conflicts: string[];
remote: IStringDictionary<string> | null;
}
export function merge(local: IStringDictionary<string>, remote: IStringDictionary<string> | null, base: IStringDictionary<string> | null, resolvedConflicts: IStringDictionary<string | null> = {}): IMergeResult {
const added: IStringDictionary<string> = {};
const updated: IStringDictionary<string> = {};
const removed: Set<string> = new Set<string>();
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<string> = deepClone(remote);
const conflicts: Set<string> = new Set<string>();
const handledConflicts: Set<string> = new Set<string>();
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<string> | null, to: IStringDictionary<string> | null): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
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<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();
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<string>, b: IStringDictionary<string>): boolean {
const { added, removed, updated } = compare(a, b);
return added.size === 0 && removed.size === 0 && updated.size === 0;
}
@@ -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<IFileContent>;
readonly remoteUserData: IRemoteUserData;
readonly lastSyncUserData: IRemoteUserData | null;
readonly added: IStringDictionary<string>;
readonly updated: IStringDictionary<string>;
readonly removed: string[];
readonly conflicts: Conflict[];
readonly resolvedConflicts: IStringDictionary<string | null>;
readonly remote: IStringDictionary<string> | null;
}
export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
protected readonly version: number = 1;
private readonly snippetsFolder: URI;
private readonly snippetsPreviewFolder: URI;
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | 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<void> {
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<ISyncPreviewResult>({
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<void> {
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<ISyncPreviewResult>({
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<void> {
await this.clearConflicts();
this.cancel();
this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.syncResourceLogLabel}.`);
this.setStatus(SyncStatus.Idle);
}
async getConflictContent(conflictResource: URI): Promise<string | null> {
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<string | null> {
const content = await super.getRemoteContent(ref);
if (content !== null && fragment) {
return this.getFragment(content, fragment);
}
return content;
}
async getLocalBackupContent(ref?: string, fragment?: string): Promise<string | null> {
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<void> {
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<boolean> {
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<SyncStatus> {
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<ISyncPreviewResult> {
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<void> {
if (this.conflicts.length) {
await Promise.all(this.conflicts.map(({ local }) => this.fileService.del(local)));
this.setConflicts([]);
}
}
private async generatePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null>, token: CancellationToken): Promise<ISyncPreviewResult> {
const localSnippets = this.toSnippetsContents(local);
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
const lastSyncSnippets: IStringDictionary<string> | 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<void> {
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<string>, removed: string[], updated: IStringDictionary<string>, local: IStringDictionary<IFileContent>): Promise<void> {
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<string> {
return JSON.parse(syncData.content);
}
private toSnippetsContents(snippetsFileContents: IStringDictionary<IFileContent>): IStringDictionary<string> {
const snippets: IStringDictionary<string> = {};
for (const key of Object.keys(snippetsFileContents)) {
snippets[key] = snippetsFileContents[key].value.toString();
}
return snippets;
}
private async getSnippetsFileContents(): Promise<IStringDictionary<IFileContent>> {
const snippets: IStringDictionary<IFileContent> = {};
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;
}
}
@@ -138,10 +138,11 @@ export function getUserDataSyncStore(productService: IProductService, configurat
export const enum SyncResource { export const enum SyncResource {
Settings = 'settings', Settings = 'settings',
Keybindings = 'keybindings', Keybindings = 'keybindings',
Snippets = 'snippets',
Extensions = 'extensions', Extensions = 'extensions',
GlobalState = 'globalState' 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 { export interface IUserDataManifest {
latest?: Record<SyncResource, string> latest?: Record<SyncResource, string>
@@ -373,10 +374,3 @@ export function getSyncResourceFromLocalPreview(localPreview: URI, environmentSe
localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme }); localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme });
return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0]; 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];
}
@@ -18,6 +18,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync';
import { isEqual } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/resources';
import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync';
type SyncErrorClassification = { type SyncErrorClassification = {
source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -55,6 +56,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
private readonly settingsSynchroniser: SettingsSynchroniser; private readonly settingsSynchroniser: SettingsSynchroniser;
private readonly keybindingsSynchroniser: KeybindingsSynchroniser; private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
private readonly snippetsSynchroniser: SnippetsSynchroniser;
private readonly extensionsSynchroniser: ExtensionsSynchroniser; private readonly extensionsSynchroniser: ExtensionsSynchroniser;
private readonly globalStateSynchroniser: GlobalStateSynchroniser; private readonly globalStateSynchroniser: GlobalStateSynchroniser;
@@ -68,9 +70,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
super(); super();
this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser)); this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser));
this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); 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.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser));
this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser)); 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(); this.updateStatus();
if (this.userDataSyncStoreService.userDataSyncStore) { if (this.userDataSyncStoreService.userDataSyncStore) {
@@ -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": [
"<div>",
"",
"</div>"
],
"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": [
"<div>",
"",
"</div>"
],
"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 });
});
});
@@ -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": [
"<div>",
"",
"</div>"
],
"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": [
"<div>",
"",
"</div>"
],
"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": [
"<div>",
"",
"</div>"
],
"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<string> {
const syncData: ISyncData = JSON.parse(content);
return JSON.parse(syncData.content);
}
async function updateSnippet(name: string, content: string, client: UserDataSyncClient): Promise<void> {
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<void> {
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<string | null> {
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() })));
}
});
@@ -44,7 +44,7 @@ class TestSynchroniser extends AbstractSynchroniser {
await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } }); await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } });
} }
stop(): void { async stop(): Promise<void> {
this.cancelled = true; this.cancelled = true;
this.syncBarrier.open(); this.syncBarrier.open();
} }
@@ -53,6 +53,7 @@ export class UserDataSyncClient extends Disposable {
userDataSyncHome, userDataSyncHome,
settingsResource: joinPath(userDataDirectory, 'settings.json'), settingsResource: joinPath(userDataDirectory, 'settings.json'),
keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'), keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'),
snippetsHome: joinPath(userDataDirectory, 'snippets'),
argvResource: joinPath(userDataDirectory, 'argv.json'), argvResource: joinPath(userDataDirectory, 'argv.json'),
args: {} args: {}
}); });
@@ -108,6 +109,7 @@ export class UserDataSyncClient extends Disposable {
if (!empty) { if (!empty) {
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({}))); await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({})));
await fileService.writeFile(environmentService.keybindingsResource, 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 fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' })));
} }
await configurationService.reloadConfiguration(); await configurationService.reloadConfiguration();
@@ -201,16 +203,13 @@ export class UserDataSyncTestServer implements IRequestService {
} }
private async writeData(resource: string, content: string = '', headers: IHeaders = {}): Promise<IRequestContext> { private async writeData(resource: string, content: string = '', headers: IHeaders = {}): Promise<IRequestContext> {
if (!headers['If-Match']) {
return this.toResponse(428);
}
if (!this.session) { if (!this.session) {
this.session = generateUuid(); this.session = generateUuid();
} }
const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource); const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource);
if (resourceKey) { if (resourceKey) {
const data = this.data.get(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); return this.toResponse(412);
} }
const ref = `${parseInt(data?.ref || '0') + 1}`; const ref = `${parseInt(data?.ref || '0') + 1}`;
@@ -10,6 +10,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
import { IFileService } from 'vs/platform/files/common/files'; import { IFileService } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { VSBuffer } from 'vs/base/common/buffer'; import { VSBuffer } from 'vs/base/common/buffer';
import { joinPath } from 'vs/base/common/resources';
suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing tests suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing tests
@@ -36,6 +37,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Keybindings // Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, { 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 // Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, { 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: {} }, { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
// Keybindings // Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { 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 // Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, { 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: {} }, { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
// Keybindings // Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
// Snippets
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
// Global state // Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
// Extensions // Extensions
@@ -140,6 +149,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
// Keybindings // Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
// Snippets
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
// Global state // Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
// Extensions // Extensions
@@ -174,6 +185,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
// Keybindings // Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
// Snippets
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
// Global state // Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
// Extensions // 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.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.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); 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); const testObject = testClient.instantiationService.get(IUserDataSyncService);
// Sync (merge) from the test client // Sync (merge) from the test client
@@ -215,6 +229,9 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
// Keybindings // Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, { 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 // Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, { 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); const environmentService = client.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); 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.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' }))); await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
// Sync from the client // 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' } }, { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
// Keybindings // Keybindings
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, { 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 // Global state
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, { 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); const environmentService = client.instantiationService.get(IEnvironmentService);
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); 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.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 fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
await client.instantiationService.get(IUserDataSyncService).sync(); 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' } }, { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: { 'If-None-Match': '1' } },
// Keybindings // Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } }, { 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 // Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: { 'If-None-Match': '1' } }, { 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 // Keybindings
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, { 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 // Global state
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
{ type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, { 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(); await testObject.sync();
disposable.dispose(); 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 () => { test('test sync conflicts status', async () => {
+8
View File
@@ -6020,6 +6020,14 @@ declare module 'vscode' {
* @param messageOrUri Message or uri. * @param messageOrUri Message or uri.
*/ */
constructor(messageOrUri?: string | 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;
} }
/** /**
+33 -16
View File
@@ -1635,6 +1635,10 @@ declare module 'vscode' {
export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput;
export interface NotebookCellMetadata {
editable: boolean;
}
export interface NotebookCell { export interface NotebookCell {
readonly uri: Uri; readonly uri: Uri;
handle: number; handle: number;
@@ -1642,6 +1646,11 @@ declare module 'vscode' {
cellKind: CellKind; cellKind: CellKind;
outputs: CellOutput[]; outputs: CellOutput[];
getContent(): string; getContent(): string;
metadata?: NotebookCellMetadata;
}
export interface NotebookDocumentMetadata {
editable: boolean;
} }
export interface NotebookDocument { export interface NotebookDocument {
@@ -1651,15 +1660,29 @@ declare module 'vscode' {
languages: string[]; languages: string[];
cells: NotebookCell[]; cells: NotebookCell[];
displayOrder?: GlobPattern[]; displayOrder?: GlobPattern[];
metadata?: NotebookDocumentMetadata;
} }
export interface NotebookEditor { export interface NotebookEditor {
readonly document: NotebookDocument; readonly document: NotebookDocument;
viewColumn?: ViewColumn; viewColumn?: ViewColumn;
/**
* Fired when the output hosting webview posts a message.
*/
readonly onDidReceiveMessage: Event<any>;
/**
* 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<boolean>;
/** /**
* 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) * 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 { export interface NotebookProvider {
@@ -2023,21 +2046,6 @@ declare module 'vscode' {
//#endregion //#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 //#region https://github.com/microsoft/vscode/issues/90208
export namespace Uri { export namespace Uri {
@@ -2053,4 +2061,13 @@ declare module 'vscode' {
//#endregion //#endregion
//#region https://github.com/microsoft/vscode/issues/91541
export enum CompletionItemKind {
User = 25,
Issue = 26,
}
//#endregion
} }
@@ -8,10 +8,12 @@ import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IEx
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri'; import { URI, UriComponents } from 'vs/base/common/uri';
import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; 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 { export class MainThreadNotebookDocument extends Disposable {
private _textModel: NotebookTextModel; private _textModel: NotebookTextModel;
@@ -54,7 +56,9 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
constructor( constructor(
extHostContext: IExtHostContext, extHostContext: IExtHostContext,
@INotebookService private _notebookService: INotebookService, @INotebookService private _notebookService: INotebookService,
@IConfigurationService private readonly configurationService: IConfigurationService @IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
) { ) {
super(); super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); 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<void> {
let controller = this._notebookProviders.get(viewType);
if (controller) {
controller.updateNotebookMetadata(resource, metadata);
}
}
async resolveNotebook(viewType: string, uri: URI): Promise<number | undefined> { async resolveNotebook(viewType: string, uri: URI): Promise<number | undefined> {
let handle = await this._proxy.$resolveNotebook(viewType, uri); let handle = await this._proxy.$resolveNotebook(viewType, uri);
return handle; return handle;
@@ -141,6 +153,21 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
async executeNotebook(viewType: string, uri: URI): Promise<void> { async executeNotebook(viewType: string, uri: URI): Promise<void> {
return this._proxy.$executeNotebook(viewType, uri, undefined); return this._proxy.$executeNotebook(viewType, uri, undefined);
} }
async $postMessage(handle: number, value: any): Promise<boolean> {
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 { export class MainThreadNotebookController implements IMainNotebookController {
@@ -186,6 +213,10 @@ export class MainThreadNotebookController implements IMainNotebookController {
this._mainThreadNotebook.executeNotebook(viewType, uri); this._mainThreadNotebook.executeNotebook(viewType, uri);
} }
onDidReceiveMessage(uri: UriComponents, message: any): void {
this._proxy.$onDidReceiveMessage(uri, message);
}
// Methods for ExtHost // Methods for ExtHost
async createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void> { async createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void> {
let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource)); let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource));
@@ -197,6 +228,11 @@ export class MainThreadNotebookController implements IMainNotebookController {
document?.textModel.updateLanguages(languages); 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 { updateNotebookRenderers(resource: UriComponents, renderers: number[]): void {
let document = this._mapping.get(URI.from(resource).toString()); let document = this._mapping.get(URI.from(resource).toString());
document?.textModel.updateRenderers(renderers); document?.textModel.updateRenderers(renderers);
@@ -227,11 +263,11 @@ export class MainThreadNotebookController implements IMainNotebookController {
return false; return false;
} }
executeNotebookActiveCell(uri: URI): void { async executeNotebookActiveCell(uri: URI): Promise<void> {
let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); let mainthreadNotebook = this._mapping.get(URI.from(uri).toString());
if (mainthreadNotebook && mainthreadNotebook.textModel.activeCell) { 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);
} }
} }
@@ -132,7 +132,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol));
const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress)));
const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol)); 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 extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol));
const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol));
const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands));
@@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel';
import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline';
import { revive } from 'vs/base/common/marshalling'; 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 { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
import { Dto } from 'vs/base/common/types'; import { Dto } from 'vs/base/common/types';
@@ -669,6 +669,7 @@ export interface ICellDto {
language: string; language: string;
cellKind: CellKind; cellKind: CellKind;
outputs: IOutput[]; outputs: IOutput[];
metadata?: NotebookCellMetadata;
} }
export type NotebookCellsSplice = [ export type NotebookCellsSplice = [
@@ -690,8 +691,10 @@ export interface MainThreadNotebookShape extends IDisposable {
$unregisterNotebookRenderer(handle: number): Promise<void>; $unregisterNotebookRenderer(handle: number): Promise<void>;
$createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void>; $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void>;
$updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void>; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void>;
$updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata | undefined): Promise<void>;
$spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise<void>; $spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise<void>;
$spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise<void>; $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise<void>;
$postMessage(handle: number, value: any): Promise<boolean>;
} }
export interface MainThreadUrlsShape extends IDisposable { export interface MainThreadUrlsShape extends IDisposable {
@@ -1531,6 +1534,7 @@ export interface ExtHostNotebookShape {
$updateActiveEditor(viewType: string, uri: UriComponents): Promise<void>; $updateActiveEditor(viewType: string, uri: UriComponents): Promise<void>;
$destoryNotebookDocument(viewType: string, uri: UriComponents): Promise<boolean>; $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise<boolean>;
$acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void;
$onDidReceiveMessage(uri: UriComponents, message: any): void;
} }
export interface ExtHostStorageShape { export interface ExtHostStorageShape {
+64 -9
View File
@@ -14,6 +14,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; 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 { 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 { ISplice } from 'vs/base/common/sequence';
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
export class ExtHostCell implements vscode.NotebookCell { export class ExtHostCell implements vscode.NotebookCell {
@@ -31,7 +32,8 @@ export class ExtHostCell implements vscode.NotebookCell {
private _content: string, private _content: string,
public cellKind: CellKind, public cellKind: CellKind,
public language: string, public language: string,
outputs: any[] outputs: any[],
public metadata: vscode.NotebookCellMetadata | undefined,
) { ) {
this.source = this._content.split(/\r|\n|\r\n/g); this.source = this._content.split(/\r|\n|\r\n/g);
this._outputs = outputs; this._outputs = outputs;
@@ -129,6 +131,17 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); 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[] = []; private _displayOrder: string[] = [];
get displayOrder() { get displayOrder() {
@@ -330,11 +343,14 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor { export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor {
private _viewColumn: vscode.ViewColumn | undefined; private _viewColumn: vscode.ViewColumn | undefined;
private static _cellhandlePool: number = 0; private static _cellhandlePool: number = 0;
onDidReceiveMessage: vscode.Event<any> = this._onDidReceiveMessage.event;
constructor( constructor(
viewType: string, viewType: string,
readonly id: string, readonly id: string,
public uri: URI, public uri: URI,
private _proxy: MainThreadNotebookShape,
private _onDidReceiveMessage: Emitter<any>,
public document: ExtHostNotebookDocument, public document: ExtHostNotebookDocument,
private _documentsAndEditors: ExtHostDocumentsAndEditors 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 handle = ExtHostNotebookEditor._cellhandlePool++;
const uri = CellUri.generate(this.document.uri, handle); 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; return cell;
} }
@@ -376,6 +392,11 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook
set viewColumn(value) { set viewColumn(value) {
throw readonly('viewColumn'); throw readonly('viewColumn');
} }
async postMessage(message: any): Promise<boolean> {
return this._proxy.$postMessage(this.document.handle, message);
}
} }
export class ExtHostNotebookOutputRenderer { export class ExtHostNotebookOutputRenderer {
@@ -415,9 +436,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
private static _handlePool: number = 0; private static _handlePool: number = 0;
private readonly _proxy: MainThreadNotebookShape; private readonly _proxy: MainThreadNotebookShape;
private readonly _notebookProviders = new Map<string, { readonly provider: vscode.NotebookProvider, readonly extension: IExtensionDescription }>(); private readonly _notebookProviders = new Map<string, { readonly provider: vscode.NotebookProvider, readonly extension: IExtensionDescription; }>();
private readonly _documents = new Map<string, ExtHostNotebookDocument>(); private readonly _documents = new Map<string, ExtHostNotebookDocument>();
private readonly _editors = new Map<string, ExtHostNotebookEditor>(); private readonly _editors = new Map<string, { editor: ExtHostNotebookEditor, onDidReceiveMessage: Emitter<any> }>();
private readonly _notebookOutputRenderers = new Map<number, ExtHostNotebookOutputRenderer>(); private readonly _notebookOutputRenderers = new Map<number, ExtHostNotebookOutputRenderer>();
private _outputDisplayOrder: INotebookDisplayOrder | undefined; private _outputDisplayOrder: INotebookDisplayOrder | undefined;
@@ -431,8 +452,28 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
return this._activeNotebookDocument; return this._activeNotebookDocument;
} }
constructor(mainContext: IMainContext, private _documentsAndEditors: ExtHostDocumentsAndEditors) { constructor(mainContext: IMainContext, commands: ExtHostCommands, private _documentsAndEditors: ExtHostDocumentsAndEditors) {
this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); 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( registerNotebookOutputRenderer(
@@ -494,15 +535,19 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
this._documents.set(URI.revive(uri).toString(), document); this._documents.set(URI.revive(uri).toString(), document);
} }
const onDidReceiveMessage = new Emitter<any>();
let editor = new ExtHostNotebookEditor( let editor = new ExtHostNotebookEditor(
viewType, viewType,
`${ExtHostNotebookController._handlePool++}`, `${ExtHostNotebookController._handlePool++}`,
URI.revive(uri), URI.revive(uri),
this._proxy,
onDidReceiveMessage,
this._documents.get(URI.revive(uri).toString())!, this._documents.get(URI.revive(uri).toString())!,
this._documentsAndEditors 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 provider.provider.resolveNotebook(editor);
// await editor.document.$updateCells(); // await editor.document.$updateCells();
return editor.document.handle; return editor.document.handle;
@@ -535,7 +580,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
let editor = this._editors.get(URI.revive(uri).toString()); let editor = this._editors.get(URI.revive(uri).toString());
let document = this._documents.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!); document?.insertCell(index, rawCell!);
let allDocuments = this._documentsAndEditors.allDocuments(); let allDocuments = this._documentsAndEditors.allDocuments();
@@ -553,6 +598,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
source: rawCell.source, source: rawCell.source,
language: rawCell.language, language: rawCell.language,
cellKind: rawCell.cellKind, cellKind: rawCell.cellKind,
metadata: rawCell.metadata,
outputs: [] outputs: []
}; };
} }
@@ -608,7 +654,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
let editor = this._editors.get(URI.revive(uri).toString()); let editor = this._editors.get(URI.revive(uri).toString());
if (editor) { if (editor) {
editor.dispose(); editor.editor.dispose();
editor.onDidReceiveMessage.dispose();
this._editors.delete(URI.revive(uri).toString()); this._editors.delete(URI.revive(uri).toString());
} }
@@ -618,4 +665,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
$acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void { $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void {
this._outputDisplayOrder = displayOrder; this._outputDisplayOrder = displayOrder;
} }
$onDidReceiveMessage(uri: UriComponents, message: any): void {
let editor = this._editors.get(URI.revive(uri).toString());
if (editor) {
editor.onDidReceiveMessage.fire(message);
}
}
} }
@@ -835,6 +835,8 @@ export namespace CompletionItemKind {
case types.CompletionItemKind.Event: return modes.CompletionItemKind.Event; case types.CompletionItemKind.Event: return modes.CompletionItemKind.Event;
case types.CompletionItemKind.Operator: return modes.CompletionItemKind.Operator; case types.CompletionItemKind.Operator: return modes.CompletionItemKind.Operator;
case types.CompletionItemKind.TypeParameter: return modes.CompletionItemKind.TypeParameter; 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; return modes.CompletionItemKind.Property;
} }
@@ -866,6 +868,8 @@ export namespace CompletionItemKind {
case modes.CompletionItemKind.Event: return types.CompletionItemKind.Event; case modes.CompletionItemKind.Event: return types.CompletionItemKind.Event;
case modes.CompletionItemKind.Operator: return types.CompletionItemKind.Operator; case modes.CompletionItemKind.Operator: return types.CompletionItemKind.Operator;
case modes.CompletionItemKind.TypeParameter: return types.CompletionItemKind.TypeParameter; 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; return types.CompletionItemKind.Property;
} }
+3 -1
View File
@@ -1348,7 +1348,9 @@ export enum CompletionItemKind {
Struct = 21, Struct = 21,
Event = 22, Event = 22,
Operator = 23, Operator = 23,
TypeParameter = 24 TypeParameter = 24,
User = 25,
Issue = 26
} }
export enum CompletionItemTag { export enum CompletionItemTag {
@@ -56,6 +56,7 @@ namespace schema {
case 'comments/commentThread/context': return MenuId.CommentThreadActions; case 'comments/commentThread/context': return MenuId.CommentThreadActions;
case 'comments/comment/title': return MenuId.CommentTitle; case 'comments/comment/title': return MenuId.CommentTitle;
case 'comments/comment/context': return MenuId.CommentActions; case 'comments/comment/context': return MenuId.CommentActions;
case 'notebook/cell/title': return MenuId.NotebookCellTitle;
case 'extension/context': return MenuId.ExtensionContext; case 'extension/context': return MenuId.ExtensionContext;
case 'timeline/title': return MenuId.TimelineTitle; case 'timeline/title': return MenuId.TimelineTitle;
case 'timeline/item/context': return MenuId.TimelineItemContext; case 'timeline/item/context': return MenuId.TimelineItemContext;
@@ -217,6 +218,11 @@ namespace schema {
type: 'array', type: 'array',
items: menuItem items: menuItem
}, },
'notebook/cell/title': {
description: localize('notebook.cell.title', "The contributed notebook cell title menu"),
type: 'array',
items: menuItem
},
'extension/context': { 'extension/context': {
description: localize('menus.extensionContext', "The extension context menu"), description: localize('menus.extensionContext', "The extension context menu"),
type: 'array', type: 'array',
@@ -16,7 +16,7 @@ import { IsFullscreenContext } from 'vs/workbench/browser/contextkeys';
import { IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; 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 { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ILabelService } from 'vs/platform/label/common/label'; import { ILabelService } from 'vs/platform/label/common/label';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; 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 { getIconClasses } from 'vs/editor/common/services/getIconClasses';
import { FileKind } from 'vs/platform/files/common/files'; import { FileKind } from 'vs/platform/files/common/files';
import { splitName } from 'vs/base/common/labels'; import { splitName } from 'vs/base/common/labels';
import { IKeyMods } from 'vs/base/parts/quickopen/common/quickOpen';
import { isMacintosh } from 'vs/base/common/platform'; import { isMacintosh } from 'vs/base/common/platform';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { inQuickOpenContext, getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { inQuickOpenContext, getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen';
+9 -5
View File
@@ -8,16 +8,20 @@
overflow: hidden; 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 { .monaco-workbench .part > .drop-block-overlay {
visibility: hidden; /* use visibility to ensure transitions */ display: none;
transition-property: opacity;
transition-timing-function: linear;
transition-duration: 250ms;
width: 100%; width: 100%;
height: 100%; height: 100%;
position: absolute; position: absolute;
top: 0; top: 0;
opacity: 0;
pointer-events: none; pointer-events: none;
} }
@@ -7,12 +7,6 @@
width: 48px; width: 48px;
} }
.monaco-workbench .part > .drop-block-overlay.visible {
visibility: visible;
backdrop-filter: brightness(97%) blur(2px);
opacity: 1;
}
.monaco-workbench .activitybar > .content { .monaco-workbench .activitybar > .content {
height: 100%; height: 100%;
display: flex; display: flex;
@@ -26,7 +26,7 @@ import { EditorInput, IWorkbenchEditorConfiguration, IEditorInput } from 'vs/wor
import { Component } from 'vs/workbench/common/component'; import { Component } from 'vs/workbench/common/component';
import { Event, Emitter } from 'vs/base/common/event'; import { Event, Emitter } from 'vs/base/common/event';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; 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 * as errors from 'vs/base/common/errors';
import { IQuickOpenService, IShowOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { IQuickOpenService, IShowOptions } from 'vs/platform/quickOpen/common/quickOpen';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -86,6 +86,10 @@ export class QuickOpenController extends Component implements IQuickOpenService
private editorHistoryHandler: EditorHistoryHandler; private editorHistoryHandler: EditorHistoryHandler;
private pendingGetResultsInvocation: CancellationTokenSource | null = null; private pendingGetResultsInvocation: CancellationTokenSource | null = null;
private get useNewExperimentalVersion() {
return this.configurationService.getValue(ENABLE_EXPERIMENTAL_VERSION_CONFIG) === true;
}
constructor( constructor(
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@INotificationService private readonly notificationService: INotificationService, @INotificationService private readonly notificationService: INotificationService,
@@ -95,7 +99,8 @@ export class QuickOpenController extends Component implements IQuickOpenService
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IEnvironmentService private readonly environmentService: IEnvironmentService, @IEnvironmentService private readonly environmentService: IEnvironmentService,
@IThemeService themeService: IThemeService, @IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService @IStorageService storageService: IStorageService,
@IQuickInputService private readonly quickInputService: IQuickInputService
) { ) {
super(QuickOpenController.ID, themeService, storageService); super(QuickOpenController.ID, themeService, storageService);
@@ -125,28 +130,44 @@ export class QuickOpenController extends Component implements IQuickOpenService
} }
navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void { navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void {
if (this.useNewExperimentalVersion) {
// already handled
} else {
if (this.quickOpenWidget) { if (this.quickOpenWidget) {
this.quickOpenWidget.navigate(next, quickNavigate); this.quickOpenWidget.navigate(next, quickNavigate);
} }
} }
}
accept(): void { accept(): void {
if (this.useNewExperimentalVersion) {
// already handled
} else {
if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) {
this.quickOpenWidget.accept(); this.quickOpenWidget.accept();
} }
} }
}
focus(): void { focus(): void {
if (this.useNewExperimentalVersion) {
// already handled
} else {
if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) {
this.quickOpenWidget.focus(); this.quickOpenWidget.focus();
} }
} }
}
close(): void { close(): void {
if (this.useNewExperimentalVersion) {
// already handled
} else {
if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) {
this.quickOpenWidget.hide(HideReason.CANCELED); this.quickOpenWidget.hide(HideReason.CANCELED);
} }
} }
}
private emitQuickOpenVisibilityChange(isVisible: boolean): void { private emitQuickOpenVisibilityChange(isVisible: boolean): void {
if (isVisible) { if (isVisible) {
@@ -157,6 +178,12 @@ export class QuickOpenController extends Component implements IQuickOpenService
} }
show(prefix?: string, options?: IShowOptions): Promise<void> { show(prefix?: string, options?: IShowOptions): Promise<void> {
if (this.useNewExperimentalVersion) {
this.quickInputService.quickAccess.show(prefix, options);
return Promise.resolve();
}
let quickNavigateConfiguration = options ? options.quickNavigateConfiguration : undefined; let quickNavigateConfiguration = options ? options.quickNavigateConfiguration : undefined;
let inputSelection = options ? options.inputSelection : undefined; let inputSelection = options ? options.inputSelection : undefined;
let autoFocus = options ? options.autoFocus : undefined; let autoFocus = options ? options.autoFocus : undefined;
@@ -336,7 +336,7 @@ export abstract class ViewPane extends Pane implements IView {
} }
if (this.progressIndicator === undefined) { 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; return this.progressIndicator;
} }
+4
View File
@@ -21,6 +21,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
export const CLOSE_ON_FOCUS_LOST_CONFIG = 'workbench.quickOpen.closeOnFocusLost'; export const CLOSE_ON_FOCUS_LOST_CONFIG = 'workbench.quickOpen.closeOnFocusLost';
export const PRESERVE_INPUT_CONFIG = 'workbench.quickOpen.preserveInput'; 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 const SEARCH_EDITOR_HISTORY = 'search.quickOpen.includeHistory';
export interface IWorkbenchQuickOpenConfiguration { export interface IWorkbenchQuickOpenConfiguration {
@@ -28,6 +29,9 @@ export interface IWorkbenchQuickOpenConfiguration {
commandPalette: { commandPalette: {
history: number; history: number;
preserveInput: boolean; preserveInput: boolean;
},
quickOpen: {
enableExperimentalNewVersion: boolean;
} }
}; };
} }
+10 -10
View File
@@ -14,18 +14,22 @@ import { isSafari, isStandalone } from 'vs/base/browser/browser';
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
// Icon defaults
const iconForegroundColor = theme.getColor(iconForeground);
if (iconForegroundColor) {
collector.addRule(`.monaco-workbench .codicon { color: ${iconForegroundColor}; }`);
}
// Foreground // Foreground
const windowForeground = theme.getColor(foreground); const windowForeground = theme.getColor(foreground);
if (windowForeground) { if (windowForeground) {
collector.addRule(`.monaco-workbench { color: ${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 // Selection
const windowSelectionBackground = theme.getColor(selectionBackground); const windowSelectionBackground = theme.getColor(selectionBackground);
if (windowSelectionBackground) { 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 // Scrollbars
const scrollbarShadowColor = theme.getColor(scrollbarShadow); const scrollbarShadowColor = theme.getColor(scrollbarShadow);
if (scrollbarShadowColor) { if (scrollbarShadowColor) {
@@ -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."), '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 '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': { 'workbench.settings.openDefaultSettings': {
'type': 'boolean', 'type': 'boolean',
'description': nls.localize('openDefaultSettings', "Controls whether opening settings also opens an editor showing all default settings."), 'description': nls.localize('openDefaultSettings', "Controls whether opening settings also opens an editor showing all default settings."),
+3 -1
View File
@@ -323,6 +323,9 @@ export class Workbench extends Layout {
private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void { private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void {
// ARIA
this.container.setAttribute('role', 'application');
// State specific classes // State specific classes
const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac'; const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac';
const workbenchClasses = coalesce([ const workbenchClasses = coalesce([
@@ -335,7 +338,6 @@ export class Workbench extends Layout {
addClasses(this.container, ...workbenchClasses); addClasses(this.container, ...workbenchClasses);
addClass(document.body, platformClass); // used by our fonts addClass(document.body, platformClass); // used by our fonts
this.container.setAttribute('role', 'application');
if (isWeb) { if (isWeb) {
addClass(document.body, 'web'); addClass(document.body, 'web');
@@ -37,19 +37,19 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv
return this.editorService.activeTextEditorControl; 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 // 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, { this.editorService.openEditor(this.editorService.activeEditor, {
selection: range, selection: options.range,
pinned: keyMods.alt || this.configuration.openEditorPinned pinned: options.keyMods.alt || this.configuration.openEditorPinned
}, SIDE_GROUP); }, SIDE_GROUP);
} }
// Otherwise let parent handle it // Otherwise let parent handle it
else { else {
super.gotoLocation(editor, range, keyMods); super.gotoLocation(editor, options);
} }
} }
} }
@@ -40,19 +40,19 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess
return this.editorService.activeTextEditorControl; 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 // 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, { this.editorService.openEditor(this.editorService.activeEditor, {
selection: range, selection: options.range,
pinned: keyMods.alt || this.configuration.openEditorPinned pinned: options.keyMods.alt || this.configuration.openEditorPinned
}, SIDE_GROUP); }, SIDE_GROUP);
} }
// Otherwise let parent handle it // Otherwise let parent handle it
else { else {
super.gotoLocation(editor, range, keyMods); super.gotoLocation(editor, options);
} }
} }
} }
@@ -162,6 +162,15 @@ export class ConfigurationManager implements IConfigurationManager {
return Promise.resolve(undefined); 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<void> { get onDidRegisterDebugger(): Event<void> {
return this._onDidRegisterDebugger.event; return this._onDidRegisterDebugger.event;
} }
@@ -34,12 +34,31 @@ export class DebugProgressContribution implements IWorkbenchContribution {
}); });
this.progressService.withProgress({ location: VIEWLET_ID }, () => promise); this.progressService.withProgress({ location: VIEWLET_ID }, () => promise);
const source = this.debugService.getConfigurationManager().getDebuggerLabel(session);
this.progressService.withProgress({ this.progressService.withProgress({
location: ProgressLocation.Notification, location: ProgressLocation.Notification,
title: progressStartEvent.body.title, title: progressStartEvent.body.title,
cancellable: progressStartEvent.body.cancellable, cancellable: progressStartEvent.body.cancellable,
silent: true silent: true,
}, () => promise, () => session.cancel(progressStartEvent.body.progressId)); 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));
}); });
} }
}; };
@@ -55,6 +55,7 @@ export class DebugSession implements IDebugSession {
private readonly _onDidLoadedSource = new Emitter<LoadedSourceEvent>(); private readonly _onDidLoadedSource = new Emitter<LoadedSourceEvent>();
private readonly _onDidCustomEvent = new Emitter<DebugProtocol.Event>(); private readonly _onDidCustomEvent = new Emitter<DebugProtocol.Event>();
private readonly _onDidProgressStart = new Emitter<DebugProtocol.ProgressStartEvent>(); private readonly _onDidProgressStart = new Emitter<DebugProtocol.ProgressStartEvent>();
private readonly _onDidProgressUpdate = new Emitter<DebugProtocol.ProgressUpdateEvent>();
private readonly _onDidProgressEnd = new Emitter<DebugProtocol.ProgressEndEvent>(); private readonly _onDidProgressEnd = new Emitter<DebugProtocol.ProgressEndEvent>();
private readonly _onDidChangeREPLElements = new Emitter<void>(); private readonly _onDidChangeREPLElements = new Emitter<void>();
@@ -190,6 +191,10 @@ export class DebugSession implements IDebugSession {
return this._onDidProgressStart.event; return this._onDidProgressStart.event;
} }
get onDidProgressUpdate(): Event<DebugProtocol.ProgressUpdateEvent> {
return this._onDidProgressUpdate.event;
}
get onDidProgressEnd(): Event<DebugProtocol.ProgressEndEvent> { get onDidProgressEnd(): Event<DebugProtocol.ProgressEndEvent> {
return this._onDidProgressEnd.event; return this._onDidProgressEnd.event;
} }
@@ -935,6 +940,9 @@ export class DebugSession implements IDebugSession {
this.rawListeners.push(this.raw.onDidProgressStart(event => { this.rawListeners.push(this.raw.onDidProgressStart(event => {
this._onDidProgressStart.fire(event); this._onDidProgressStart.fire(event);
})); }));
this.rawListeners.push(this.raw.onDidProgressUpdate(event => {
this._onDidProgressUpdate.fire(event);
}));
this.rawListeners.push(this.raw.onDidProgressEnd(event => { this.rawListeners.push(this.raw.onDidProgressEnd(event => {
this._onDidProgressEnd.fire(event); this._onDidProgressEnd.fire(event);
})); }));
@@ -66,6 +66,7 @@ export class RawDebugSession implements IDisposable {
private readonly _onDidBreakpoint = new Emitter<DebugProtocol.BreakpointEvent>(); private readonly _onDidBreakpoint = new Emitter<DebugProtocol.BreakpointEvent>();
private readonly _onDidLoadedSource = new Emitter<DebugProtocol.LoadedSourceEvent>(); private readonly _onDidLoadedSource = new Emitter<DebugProtocol.LoadedSourceEvent>();
private readonly _onDidProgressStart = new Emitter<DebugProtocol.ProgressStartEvent>(); private readonly _onDidProgressStart = new Emitter<DebugProtocol.ProgressStartEvent>();
private readonly _onDidProgressUpdate = new Emitter<DebugProtocol.ProgressUpdateEvent>();
private readonly _onDidProgressEnd = new Emitter<DebugProtocol.ProgressEndEvent>(); private readonly _onDidProgressEnd = new Emitter<DebugProtocol.ProgressEndEvent>();
private readonly _onDidCustomEvent = new Emitter<DebugProtocol.Event>(); private readonly _onDidCustomEvent = new Emitter<DebugProtocol.Event>();
private readonly _onDidEvent = new Emitter<DebugProtocol.Event>(); private readonly _onDidEvent = new Emitter<DebugProtocol.Event>();
@@ -142,6 +143,9 @@ export class RawDebugSession implements IDisposable {
case 'progressStart': case 'progressStart':
this._onDidProgressStart.fire(event as DebugProtocol.ProgressStartEvent); this._onDidProgressStart.fire(event as DebugProtocol.ProgressStartEvent);
break; break;
case 'progressUpdate':
this._onDidProgressUpdate.fire(event as DebugProtocol.ProgressUpdateEvent);
break;
case 'progressEnd': case 'progressEnd':
this._onDidProgressEnd.fire(event as DebugProtocol.ProgressEndEvent); this._onDidProgressEnd.fire(event as DebugProtocol.ProgressEndEvent);
break; break;
@@ -217,6 +221,10 @@ export class RawDebugSession implements IDisposable {
return this._onDidProgressStart.event; return this._onDidProgressStart.event;
} }
get onDidProgressUpdate(): Event<DebugProtocol.ProgressUpdateEvent> {
return this._onDidProgressUpdate.event;
}
get onDidProgressEnd(): Event<DebugProtocol.ProgressEndEvent> { get onDidProgressEnd(): Event<DebugProtocol.ProgressEndEvent> {
return this._onDidProgressEnd.event; return this._onDidProgressEnd.event;
} }
@@ -200,6 +200,7 @@ export interface IDebugSession extends ITreeElement {
readonly onDidLoadedSource: Event<LoadedSourceEvent>; readonly onDidLoadedSource: Event<LoadedSourceEvent>;
readonly onDidCustomEvent: Event<DebugProtocol.Event>; readonly onDidCustomEvent: Event<DebugProtocol.Event>;
readonly onDidProgressStart: Event<DebugProtocol.ProgressStartEvent>; readonly onDidProgressStart: Event<DebugProtocol.ProgressStartEvent>;
readonly onDidProgressUpdate: Event<DebugProtocol.ProgressUpdateEvent>;
readonly onDidProgressEnd: Event<DebugProtocol.ProgressEndEvent>; readonly onDidProgressEnd: Event<DebugProtocol.ProgressEndEvent>;
// DAP request // DAP request
@@ -662,6 +663,7 @@ export interface IConfigurationManager {
resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: any, token: CancellationToken): Promise<any>; resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: any, token: CancellationToken): Promise<any>;
getDebugAdapterDescriptor(session: IDebugSession): Promise<IAdapterDescriptor | undefined>; getDebugAdapterDescriptor(session: IDebugSession): Promise<IAdapterDescriptor | undefined>;
getDebuggerLabel(session: IDebugSession): string | undefined;
registerDebugAdapterFactory(debugTypes: string[], debugAdapterFactory: IDebugAdapterFactory): IDisposable; registerDebugAdapterFactory(debugTypes: string[], debugAdapterFactory: IDebugAdapterFactory): IDisposable;
createDebugAdapter(session: IDebugSession): IDebugAdapter | undefined; createDebugAdapter(session: IDebugSession): IDebugAdapter | undefined;
+207 -72
View File
@@ -72,12 +72,15 @@ declare module DebugProtocol {
/** Cancel request; value of command field is 'cancel'. /** Cancel request; value of command field is 'cancel'.
The 'cancel' request is used by the frontend in two situations: 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 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. 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. 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. 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 request that got canceled still needs to send a response back. This can either be a normal result ('success' attribute true)
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. 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 { export interface CancelRequest extends Request {
// command: 'cancel'; // command: 'cancel';
@@ -86,9 +89,13 @@ declare module DebugProtocol {
/** Arguments for 'cancel' request. */ /** Arguments for 'cancel' request. */
export interface CancelArguments { 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; 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; progressId?: string;
} }
@@ -309,11 +316,14 @@ declare module DebugProtocol {
The event signals that a long running operation is about to start and 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. 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. 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 { export interface ProgressStartEvent extends Event {
// event: 'progressStart'; // event: 'progressStart';
body: { 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; progressId: string;
/** Mandatory (short) title of the progress reporting. Shown in the UI to describe the long running operation. */ /** Mandatory (short) title of the progress reporting. Shown in the UI to describe the long running operation. */
title: string; title: string;
@@ -337,6 +347,7 @@ declare module DebugProtocol {
/** Event message for 'progressUpdate' event type. /** Event message for 'progressUpdate' event type.
The event signals that the progress reporting needs to updated with a new message and/or percentage. 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. 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 { export interface ProgressUpdateEvent extends Event {
// event: 'progressUpdate'; // event: 'progressUpdate';
@@ -352,6 +363,7 @@ declare module DebugProtocol {
/** Event message for 'progressEnd' event type. /** Event message for 'progressEnd' event type.
The event signals the end of the progress reporting with an optional final message. 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 { export interface ProgressEndEvent extends Event {
// event: 'progressEnd'; // event: 'progressEnd';
@@ -364,7 +376,9 @@ declare module DebugProtocol {
} }
/** RunInTerminal request; value of command field is 'runInTerminal'. /** 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 { export interface RunInTerminalRequest extends Request {
// command: 'runInTerminal'; // command: 'runInTerminal';
@@ -396,8 +410,10 @@ declare module DebugProtocol {
} }
/** Initialize request; value of command field is 'initialize'. /** 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. The 'initialize' request is sent as the first request from the client to 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. 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. The 'initialize' request may only be sent once.
*/ */
export interface InitializeRequest extends Request { export interface InitializeRequest extends Request {
@@ -442,7 +458,9 @@ declare module DebugProtocol {
} }
/** ConfigurationDone request; value of command field is 'configurationDone'. /** 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 { export interface ConfigurationDoneRequest extends Request {
// command: 'configurationDone'; // command: 'configurationDone';
@@ -458,7 +476,8 @@ declare module DebugProtocol {
} }
/** Launch request; value of command field is 'launch'. /** 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 { export interface LaunchRequest extends Request {
// command: 'launch'; // command: 'launch';
@@ -481,7 +500,8 @@ declare module DebugProtocol {
} }
/** Attach request; value of command field is 'attach'. /** 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 { export interface AttachRequest extends Request {
// command: 'attach'; // command: 'attach';
@@ -502,10 +522,8 @@ declare module DebugProtocol {
} }
/** Restart request; value of command field is 'restart'. /** Restart request; value of command field is 'restart'.
Restarts a debug session. If the capability 'supportsRestartRequest' is missing or has the value false, Restarts a debug session. Clients should only call this request if the capability 'supportsRestartRequest' is true.
the client will implement 'restart' by terminating the debug adapter first and then launching it anew. 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.
A debug adapter can override this default behaviour by implementing a restart request
and setting the capability 'supportsRestartRequest' to true.
*/ */
export interface RestartRequest extends Request { export interface RestartRequest extends Request {
// command: 'restart'; // command: 'restart';
@@ -521,7 +539,11 @@ declare module DebugProtocol {
} }
/** Disconnect request; value of command field is 'disconnect'. /** 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 { export interface DisconnectRequest extends Request {
// command: 'disconnect'; // command: 'disconnect';
@@ -534,7 +556,7 @@ declare module DebugProtocol {
restart?: boolean; restart?: boolean;
/** Indicates whether the debuggee should be terminated when the debugger is disconnected. /** 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. 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; terminateDebuggee?: boolean;
} }
@@ -545,6 +567,7 @@ declare module DebugProtocol {
/** Terminate request; value of command field is 'terminate'. /** 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. 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 { export interface TerminateRequest extends Request {
// command: 'terminate'; // command: 'terminate';
@@ -563,6 +586,7 @@ declare module DebugProtocol {
/** BreakpointLocations request; value of command field is 'breakpointLocations'. /** BreakpointLocations request; value of command field is 'breakpointLocations'.
The 'breakpointLocations' request returns all possible locations for source breakpoints in a given range. 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 { export interface BreakpointLocationsRequest extends Request {
// command: 'breakpointLocations'; // command: 'breakpointLocations';
@@ -623,7 +647,9 @@ declare module DebugProtocol {
*/ */
export interface SetBreakpointsResponse extends Response { export interface SetBreakpointsResponse extends Response {
body: { 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[]; breakpoints: Breakpoint[];
}; };
} }
@@ -632,6 +658,7 @@ declare module DebugProtocol {
Replaces all existing function breakpoints with new function breakpoints. Replaces all existing function breakpoints with new function breakpoints.
To clear all function breakpoints, specify an empty array. To clear all function breakpoints, specify an empty array.
When a function breakpoint is hit, a 'stopped' event (with reason 'function breakpoint') is generated. 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 { export interface SetFunctionBreakpointsRequest extends Request {
// command: 'setFunctionBreakpoints'; // command: 'setFunctionBreakpoints';
@@ -655,7 +682,9 @@ declare module DebugProtocol {
} }
/** SetExceptionBreakpoints request; value of command field is 'setExceptionBreakpoints'. /** 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 { export interface SetExceptionBreakpointsRequest extends Request {
// command: 'setExceptionBreakpoints'; // command: 'setExceptionBreakpoints';
@@ -666,7 +695,9 @@ declare module DebugProtocol {
export interface SetExceptionBreakpointsArguments { export interface SetExceptionBreakpointsArguments {
/** IDs of checked exception options. The set of IDs is returned via the 'exceptionBreakpointFilters' capability. */ /** IDs of checked exception options. The set of IDs is returned via the 'exceptionBreakpointFilters' capability. */
filters: string[]; 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[]; exceptionOptions?: ExceptionOptions[];
} }
@@ -676,6 +707,7 @@ declare module DebugProtocol {
/** DataBreakpointInfo request; value of command field is 'dataBreakpointInfo'. /** DataBreakpointInfo request; value of command field is 'dataBreakpointInfo'.
Obtains information on a possible data breakpoint that could be set on an expression or variable. 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 { export interface DataBreakpointInfoRequest extends Request {
// command: 'dataBreakpointInfo'; // command: 'dataBreakpointInfo';
@@ -686,7 +718,9 @@ declare module DebugProtocol {
export interface DataBreakpointInfoArguments { export interface DataBreakpointInfoArguments {
/** Reference to the Variable container if the data breakpoint is requested for a child of the container. */ /** Reference to the Variable container if the data breakpoint is requested for a child of the container. */
variablesReference?: number; variablesReference?: number;
/** The name of the Variable's child to obtain data breakpoint information for. If variableReference isnt provided, this can be an expression. */ /** The name of the Variable's child to obtain data breakpoint information for.
If variableReference isnt provided, this can be an expression.
*/
name: string; name: string;
} }
@@ -708,6 +742,7 @@ declare module DebugProtocol {
Replaces all existing data breakpoints with new data breakpoints. Replaces all existing data breakpoints with new data breakpoints.
To clear all data breakpoints, specify an empty array. To clear all data breakpoints, specify an empty array.
When a data breakpoint is hit, a 'stopped' event (with reason 'data breakpoint') is generated. 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 { export interface SetDataBreakpointsRequest extends Request {
// command: 'setDataBreakpoints'; // command: 'setDataBreakpoints';
@@ -740,14 +775,18 @@ declare module DebugProtocol {
/** Arguments for 'continue' request. */ /** Arguments for 'continue' request. */
export interface ContinueArguments { 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; threadId: number;
} }
/** Response to 'continue' request. */ /** Response to 'continue' request. */
export interface ContinueResponse extends Response { export interface ContinueResponse extends Response {
body: { 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; allThreadsContinued?: boolean;
}; };
} }
@@ -817,7 +856,8 @@ declare module DebugProtocol {
/** StepBack request; value of command field is 'stepBack'. /** StepBack request; value of command field is 'stepBack'.
The request starts the debuggee to run one step backwards. 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 { export interface StepBackRequest extends Request {
// command: 'stepBack'; // command: 'stepBack';
@@ -835,7 +875,8 @@ declare module DebugProtocol {
} }
/** ReverseContinue request; value of command field is 'reverseContinue'. /** 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 { export interface ReverseContinueRequest extends Request {
// command: 'reverseContinue'; // command: 'reverseContinue';
@@ -855,6 +896,7 @@ declare module DebugProtocol {
/** RestartFrame request; value of command field is 'restartFrame'. /** RestartFrame request; value of command field is 'restartFrame'.
The request restarts execution of the specified stackframe. 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. 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 { export interface RestartFrameRequest extends Request {
// command: 'restartFrame'; // command: 'restartFrame';
@@ -876,6 +918,7 @@ declare module DebugProtocol {
This makes it possible to skip the execution of code or to executed code again. 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 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'. 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 { export interface GotoRequest extends Request {
// command: 'goto'; // command: 'goto';
@@ -929,7 +972,9 @@ declare module DebugProtocol {
startFrame?: number; startFrame?: number;
/** The maximum number of frames to return. If levels is not specified or 0, all frames are returned. */ /** The maximum number of frames to return. If levels is not specified or 0, all frames are returned. */
levels?: number; 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; format?: StackFrameFormat;
} }
@@ -986,7 +1031,9 @@ declare module DebugProtocol {
start?: number; start?: number;
/** The number of variables to return. If count is missing or 0, all variables are returned. */ /** The number of variables to return. If count is missing or 0, all variables are returned. */
count?: number; 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; format?: ValueFormat;
} }
@@ -999,7 +1046,7 @@ declare module DebugProtocol {
} }
/** SetVariable request; value of command field is 'setVariable'. /** 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 { export interface SetVariableRequest extends Request {
// command: 'setVariable'; // command: 'setVariable';
@@ -1025,14 +1072,18 @@ declare module DebugProtocol {
value: string; value: string;
/** The type of the new value. Typically shown in the UI when hovering over the value. */ /** The type of the new value. Typically shown in the UI when hovering over the value. */
type?: string; 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; variablesReference?: number;
/** The number of named child variables. /** 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; namedVariables?: number;
/** The number of indexed child variables. /** 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; indexedVariables?: number;
}; };
@@ -1050,7 +1101,9 @@ declare module DebugProtocol {
export interface SourceArguments { export interface SourceArguments {
/** Specifies the source content to load. Either source.path or source.sourceReference must be specified. */ /** Specifies the source content to load. Either source.path or source.sourceReference must be specified. */
source?: Source; 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; sourceReference: number;
} }
@@ -1081,6 +1134,7 @@ declare module DebugProtocol {
/** TerminateThreads request; value of command field is 'terminateThreads'. /** TerminateThreads request; value of command field is 'terminateThreads'.
The request terminates the threads with the given ids. 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 { export interface TerminateThreadsRequest extends Request {
// command: 'terminateThreads'; // command: 'terminateThreads';
@@ -1098,7 +1152,8 @@ declare module DebugProtocol {
} }
/** Modules request; value of command field is 'modules'. /** 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 { export interface ModulesRequest extends Request {
// command: 'modules'; // command: 'modules';
@@ -1125,6 +1180,7 @@ declare module DebugProtocol {
/** LoadedSources request; value of command field is 'loadedSources'. /** LoadedSources request; value of command field is 'loadedSources'.
Retrieves the set of all sources currently loaded by the debugged process. 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 { export interface LoadedSourcesRequest extends Request {
// command: 'loadedSources'; // command: 'loadedSources';
@@ -1164,10 +1220,13 @@ declare module DebugProtocol {
'repl': evaluate is run from REPL console. 'repl': evaluate is run from REPL console.
'hover': evaluate is run from a data hover. 'hover': evaluate is run from a data hover.
'clipboard': evaluate is run to generate the value that will be stored in the clipboard. '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. etc.
*/ */
context?: string; 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; format?: ValueFormat;
} }
@@ -1176,21 +1235,30 @@ declare module DebugProtocol {
body: { body: {
/** The result of the evaluate request. */ /** The result of the evaluate request. */
result: string; 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; type?: string;
/** Properties of a evaluate result that can be used to determine how to render the result in the UI. */ /** Properties of a evaluate result that can be used to determine how to render the result in the UI. */
presentationHint?: VariablePresentationHint; 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; variablesReference: number;
/** The number of named child variables. /** 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; namedVariables?: number;
/** The number of indexed child variables. /** 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; 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; memoryReference?: string;
}; };
} }
@@ -1198,6 +1266,7 @@ declare module DebugProtocol {
/** SetExpression request; value of command field is 'setExpression'. /** 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. 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. 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 { export interface SetExpressionRequest extends Request {
// command: 'setExpression'; // command: 'setExpression';
@@ -1221,18 +1290,24 @@ declare module DebugProtocol {
body: { body: {
/** The new value of the expression. */ /** The new value of the expression. */
value: string; 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; type?: string;
/** Properties of a value that can be used to determine how to render the result in the UI. */ /** Properties of a value that can be used to determine how to render the result in the UI. */
presentationHint?: VariablePresentationHint; 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; variablesReference?: number;
/** The number of named child variables. /** 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; namedVariables?: number;
/** The number of indexed child variables. /** 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; indexedVariables?: number;
}; };
@@ -1242,6 +1317,7 @@ declare module DebugProtocol {
This request retrieves the possible stepIn targets for the specified stack frame. This request retrieves the possible stepIn targets for the specified stack frame.
These targets can be used in the 'stepIn' request. These targets can be used in the 'stepIn' request.
The StepInTargets may only be called if the 'supportsStepInTargetsRequest' capability exists and is true. 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 { export interface StepInTargetsRequest extends Request {
// command: 'stepInTargets'; // command: 'stepInTargets';
@@ -1265,7 +1341,7 @@ declare module DebugProtocol {
/** GotoTargets request; value of command field is 'gotoTargets'. /** GotoTargets request; value of command field is 'gotoTargets'.
This request retrieves the possible goto targets for the specified source location. This request retrieves the possible goto targets for the specified source location.
These targets can be used in the 'goto' request. 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 { export interface GotoTargetsRequest extends Request {
// command: 'gotoTargets'; // command: 'gotoTargets';
@@ -1292,7 +1368,7 @@ declare module DebugProtocol {
/** Completions request; value of command field is 'completions'. /** Completions request; value of command field is 'completions'.
Returns a list of possible completions for a given caret position and text. 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 { export interface CompletionsRequest extends Request {
// command: 'completions'; // command: 'completions';
@@ -1321,6 +1397,7 @@ declare module DebugProtocol {
/** ExceptionInfo request; value of command field is 'exceptionInfo'. /** ExceptionInfo request; value of command field is 'exceptionInfo'.
Retrieves the details of the exception that caused this event to be raised. 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 { export interface ExceptionInfoRequest extends Request {
// command: 'exceptionInfo'; // command: 'exceptionInfo';
@@ -1349,6 +1426,7 @@ declare module DebugProtocol {
/** ReadMemory request; value of command field is 'readMemory'. /** ReadMemory request; value of command field is 'readMemory'.
Reads bytes from memory at the provided location. 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 { export interface ReadMemoryRequest extends Request {
// command: 'readMemory'; // command: 'readMemory';
@@ -1368,9 +1446,13 @@ declare module DebugProtocol {
/** Response to 'readMemory' request. */ /** Response to 'readMemory' request. */
export interface ReadMemoryResponse extends Response { export interface ReadMemoryResponse extends Response {
body?: { 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; 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; unreadableBytes?: number;
/** The bytes read from memory, encoded using base64. */ /** The bytes read from memory, encoded using base64. */
data?: string; data?: string;
@@ -1379,6 +1461,7 @@ declare module DebugProtocol {
/** Disassemble request; value of command field is 'disassemble'. /** Disassemble request; value of command field is 'disassemble'.
Disassembles code stored at the provided location. Disassembles code stored at the provided location.
Clients should only call this request if the capability 'supportsDisassembleRequest' is true.
*/ */
export interface DisassembleRequest extends Request { export interface DisassembleRequest extends Request {
// command: 'disassemble'; // command: 'disassemble';
@@ -1393,7 +1476,9 @@ declare module DebugProtocol {
offset?: number; offset?: number;
/** Optional offset (in instructions) to be applied after the byte offset (if any) before disassembling. Can be negative. */ /** Optional offset (in instructions) to be applied after the byte offset (if any) before disassembling. Can be negative. */
instructionOffset?: number; 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; instructionCount: number;
/** If true, the adapter should attempt to resolve memory addresses and other values to symbolic names. */ /** If true, the adapter should attempt to resolve memory addresses and other values to symbolic names. */
resolveSymbols?: boolean; resolveSymbols?: boolean;
@@ -1543,7 +1628,8 @@ declare module DebugProtocol {
addressRange?: string; 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. It is only used if the underlying UI actually supports this level of customization.
*/ */
export interface ColumnDescriptor { export interface ColumnDescriptor {
@@ -1574,21 +1660,34 @@ declare module DebugProtocol {
name: string; 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 { 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; 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; 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; 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'; presentationHint?: 'normal' | 'emphasize' | 'deemphasize';
/** The (optional) origin of this source: possible values 'internal module', 'inlined content from source map', etc. */ /** The (optional) origin of this source: possible values 'internal module', 'inlined content from source map', etc. */
origin?: string; origin?: string;
/** An optional list of sources that are related to this source. These may be the source that generated this source. */ /** An optional list of sources that are related to this source. These may be the source that generated this source. */
sources?: 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; adapterData?: any;
/** The checksums associated with this file. */ /** The checksums associated with this file. */
checksums?: Checksum[]; checksums?: Checksum[];
@@ -1596,7 +1695,9 @@ declare module DebugProtocol {
/** A Stackframe contains the source location. */ /** A Stackframe contains the source location. */
export interface StackFrame { 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; id: number;
/** The name of the stack frame, typically a method name. */ /** The name of the stack frame, typically a method name. */
name: string; name: string;
@@ -1614,7 +1715,9 @@ declare module DebugProtocol {
instructionPointerReference?: string; instructionPointerReference?: string;
/** The module associated with this frame, if any. */ /** The module associated with this frame, if any. */
moduleId?: number | string; 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'; presentationHint?: 'normal' | 'label' | 'subtle';
} }
@@ -1666,7 +1769,9 @@ declare module DebugProtocol {
name: string; name: string;
/** The variable's value. This can be a multi-line text, e.g. for a function the body of a function. */ /** The variable's value. This can be a multi-line text, e.g. for a function the body of a function. */
value: string; 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; type?: string;
/** Properties of a variable that can be used to determine how to render the variable in the UI. */ /** Properties of a variable that can be used to determine how to render the variable in the UI. */
presentationHint?: VariablePresentationHint; 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. The client can use this optional information to present the children in a paged UI and fetch them in chunks.
*/ */
indexedVariables?: number; 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; memoryReference?: string;
} }
@@ -1699,7 +1806,8 @@ declare module DebugProtocol {
'innerClass': Indicates that the object is an inner class. 'innerClass': Indicates that the object is an inner class.
'interface': Indicates that the object is an interface. 'interface': Indicates that the object is an interface.
'mostDerivedClass': Indicates that the object is the most derived class. '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. 'dataBreakpoint': Indicates that a data breakpoint is registered for the object.
etc. etc.
*/ */
@@ -1740,11 +1848,19 @@ declare module DebugProtocol {
line: number; line: number;
/** An optional source column of the breakpoint. */ /** An optional source column of the breakpoint. */
column?: number; 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; 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; 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; logMessage?: string;
} }
@@ -1752,9 +1868,14 @@ declare module DebugProtocol {
export interface FunctionBreakpoint { export interface FunctionBreakpoint {
/** The name of the function. */ /** The name of the function. */
name: string; 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; 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; hitCondition?: string;
} }
@@ -1769,7 +1890,9 @@ declare module DebugProtocol {
accessType?: DataBreakpointAccessType; accessType?: DataBreakpointAccessType;
/** An optional expression for conditional breakpoints. */ /** An optional expression for conditional breakpoints. */
condition?: string; 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; hitCondition?: string;
} }
@@ -1779,7 +1902,9 @@ declare module DebugProtocol {
id?: number; id?: number;
/** If true breakpoint could be set (but not necessarily at the desired location). */ /** If true breakpoint could be set (but not necessarily at the desired location). */
verified: boolean; 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; message?: string;
/** The source where the breakpoint is located. */ /** The source where the breakpoint is located. */
source?: Source; source?: Source;
@@ -1789,7 +1914,9 @@ declare module DebugProtocol {
column?: number; column?: number;
/** An optional end line of the actual range covered by the breakpoint. */ /** An optional end line of the actual range covered by the breakpoint. */
endLine?: number; 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; endColumn?: number;
} }
@@ -1891,7 +2018,9 @@ declare module DebugProtocol {
/** An ExceptionOptions assigns configuration options to a set of exceptions. */ /** An ExceptionOptions assigns configuration options to a set of exceptions. */
export interface ExceptionOptions { 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[]; path?: ExceptionPathSegment[];
/** Condition when a thrown exception should result in a break. */ /** Condition when a thrown exception should result in a break. */
breakMode: ExceptionBreakMode; breakMode: ExceptionBreakMode;
@@ -1905,7 +2034,10 @@ declare module DebugProtocol {
*/ */
export type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled'; 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 { export interface ExceptionPathSegment {
/** If false or missing this segment matches the names provided, otherwise it matches anything except the names provided. */ /** If false or missing this segment matches the names provided, otherwise it matches anything except the names provided. */
negate?: boolean; negate?: boolean;
@@ -1939,7 +2071,10 @@ declare module DebugProtocol {
instruction: string; instruction: string;
/** Name of the symbol that corresponds with the location of this instruction, if any. */ /** Name of the symbol that corresponds with the location of this instruction, if any. */
symbol?: string; 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; location?: Source;
/** The line within the source location that corresponds to this instruction, if any. */ /** The line within the source location that corresponds to this instruction, if any. */
line?: number; line?: number;
@@ -230,6 +230,10 @@ export class MockSession implements IDebugSession {
throw new Error('not implemented'); throw new Error('not implemented');
} }
get onDidProgressUpdate(): Event<DebugProtocol.ProgressUpdateEvent> {
throw new Error('not implemented');
}
get onDidProgressEnd(): Event<DebugProtocol.ProgressEndEvent> { get onDidProgressEnd(): Event<DebugProtocol.ProgressEndEvent> {
throw new Error('not implemented'); throw new Error('not implemented');
} }
@@ -28,6 +28,7 @@
height: calc(100% - 41px); height: calc(100% - 41px);
} }
.extensions-viewlet > .extensions .extension-view-header .count-badge-wrapper,
.extensions-viewlet > .extensions .extension-view-header .monaco-action-bar { .extensions-viewlet > .extensions .extension-view-header .monaco-action-bar {
margin-right: 4px; margin-right: 4px;
} }
@@ -592,7 +592,7 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
// Hide those that match Hidden Patterns // Hide those that match Hidden Patterns
const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); 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; stat.isExcluded = true;
const editors = this.editorService.visibleEditors; const editors = this.editorService.visibleEditors;
const editor = editors.filter(e => e.resource && isEqualOrParent(e.resource, stat.resource)).pop(); const editor = editors.filter(e => e.resource && isEqualOrParent(e.resource, stat.resource)).pop();
@@ -6,7 +6,7 @@
export const INSERT_CODE_CELL_ABOVE_COMMAND_ID = 'workbench.notebook.code.insertCellAbove'; 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_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_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 EDIT_CELL_COMMAND_ID = 'workbench.notebook.cell.edit';
export const SAVE_CELL_COMMAND_ID = 'workbench.notebook.cell.save'; 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_TOP_PADDING = 8;
export const EDITOR_BOTTOM_PADDING = 8; export const EDITOR_BOTTOM_PADDING = 8;
export const EDITOR_TOOLBAR_HEIGHT = 22; export const EDITOR_TOOLBAR_HEIGHT = 22;
export const RUN_BUTTON_WIDTH = 20;
// Context Keys
export const NOTEBOOK_CELL_TYPE_CONTEXT_KEY = 'notebookCellType';
@@ -11,8 +11,8 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo
import { InputFocusedContext, InputFocusedContextKey, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { InputFocusedContext, InputFocusedContextKey, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; 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 { 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 { INotebookEditor, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, ICellViewModel, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; 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 { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; 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 { registerAction2(class extends Action2 {
constructor() { constructor() {
super({ super({
id: 'workbench.action.executeNotebookCell', id: EXECUTE_CELL_COMMAND_ID,
title: localize('notebookActions.execute', "Execute Notebook Cell"), title: localize('notebookActions.execute', "Execute Notebook Cell"),
keybinding: { keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext), when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext),
@@ -33,11 +33,36 @@ registerAction2(class extends Action2 {
}); });
} }
async run(accessor: ServicesAccessor): Promise<void> { async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise<void> {
runActiveCell(accessor); 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 { registerAction2(class extends Action2 {
constructor() { constructor() {
super({ super({
@@ -53,7 +78,7 @@ registerAction2(class extends Action2 {
async run(accessor: ServicesAccessor): Promise<void> { async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(IEditorService); const editorService = accessor.get(IEditorService);
const activeCell = runActiveCell(accessor); const activeCell = await runActiveCell(accessor);
if (!activeCell) { if (!activeCell) {
return; return;
} }
@@ -93,7 +118,7 @@ registerAction2(class extends Action2 {
async run(accessor: ServicesAccessor): Promise<void> { async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(IEditorService); const editorService = accessor.get(IEditorService);
const activeCell = runActiveCell(accessor); const activeCell = await runActiveCell(accessor);
if (!activeCell) { if (!activeCell) {
return; return;
} }
@@ -273,7 +298,7 @@ function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor
return activeEditorPane?.isNotebookEditor ? activeEditorPane : undefined; return activeEditorPane?.isNotebookEditor ? activeEditorPane : undefined;
} }
function runActiveCell(accessor: ServicesAccessor): ICellViewModel | undefined { async function runActiveCell(accessor: ServicesAccessor): Promise<ICellViewModel | undefined> {
const editorService = accessor.get(IEditorService); const editorService = accessor.get(IEditorService);
const notebookService = accessor.get(INotebookService); const notebookService = accessor.get(INotebookService);
@@ -303,11 +328,41 @@ function runActiveCell(accessor: ServicesAccessor): ICellViewModel | undefined {
} }
const viewType = notebookProviders[0].id; const viewType = notebookProviders[0].id;
notebookService.executeNotebookActiveCell(viewType, resource); await notebookService.executeNotebookActiveCell(viewType, resource);
return activeCell; return activeCell;
} }
async function runCell(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise<void> {
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<void> { async function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(IEditorService); const editorService = accessor.get(IEditorService);
const editor = getActiveNotebookEditor(editorService); const editor = getActiveNotebookEditor(editorService);
@@ -341,6 +396,7 @@ async function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor
} }
export interface INotebookCellActionContext { export interface INotebookCellActionContext {
cellTemplate?: CellRenderTemplate;
cell: ICellViewModel; cell: ICellViewModel;
notebookEditor: INotebookEditor; notebookEditor: INotebookEditor;
} }
@@ -413,7 +469,7 @@ registerAction2(class extends InsertCellCommand {
constructor() { constructor() {
super( super(
{ {
id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, id: INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID,
title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"), title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"),
}, },
CellKind.Markdown, CellKind.Markdown,
@@ -428,7 +484,7 @@ registerAction2(class extends InsertCellCommand {
id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID,
title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"),
}, },
CellKind.Code, CellKind.Markdown,
'below'); 'below');
} }
}); });
@@ -17,9 +17,9 @@
white-space: initial; 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; overflow: visible !important;
} } */
.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-list-rows { .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-list-rows {
min-height: 100%; min-height: 100%;
@@ -30,6 +30,10 @@
position: relative; 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 { .monaco-workbench .part.editor > .content .notebook-editor .notebook-content-widgets {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -122,11 +126,29 @@
cursor: pointer; 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; visibility: hidden;
margin-right: 24px; 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.focused .monaco-toolbar,
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .monaco-toolbar { .monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .monaco-toolbar {
visibility: visible; visibility: visible;
@@ -16,6 +16,7 @@ import { Range } from 'vs/editor/common/core/range';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { DisposableStore } from 'vs/base/common/lifecycle'; import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri'; 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<boolean>('notebookFindWidgetFocused', false); export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey<boolean>('notebookFindWidgetFocused', false);
@@ -44,6 +45,8 @@ export interface INotebookEditor {
*/ */
viewModel: NotebookViewModel | undefined; viewModel: NotebookViewModel | undefined;
isNotebookEditor: boolean;
/** /**
* Focus the notebook editor cell list * Focus the notebook editor cell list
*/ */
@@ -121,6 +124,11 @@ export interface INotebookEditor {
*/ */
removeInset(output: IOutput): void; removeInset(output: IOutput): void;
/**
* Send message to the webview for outputs.
*/
postMessage(message: any): void;
/** /**
* Trigger the editor to scroll from scroll event programmatically * Trigger the editor to scroll from scroll event programmatically
*/ */
@@ -195,12 +203,14 @@ export interface INotebookEditor {
export interface CellRenderTemplate { export interface CellRenderTemplate {
container: HTMLElement; container: HTMLElement;
cellContainer: HTMLElement; cellContainer: HTMLElement;
menuContainer?: HTMLElement; editorContainer?: HTMLElement;
toolbar: ToolBar; toolbar: ToolBar;
focusIndicator?: HTMLElement; focusIndicator?: HTMLElement;
runToolbar?: ToolBar;
editingContainer?: HTMLElement; editingContainer?: HTMLElement;
outputContainer?: HTMLElement; outputContainer?: HTMLElement;
editor?: CodeEditorWidget; editor?: CodeEditorWidget;
progressBar?: ProgressBar;
disposables: DisposableStore; disposables: DisposableStore;
} }
@@ -42,7 +42,7 @@ import { NotebookViewModel, INotebookEditorViewState, IModelDecorationsChangeAcc
import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor';
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
import { Range } from 'vs/editor/common/core/range'; 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'; import { Color, RGBA } from 'vs/base/common/color';
const $ = DOM.$; const $ = DOM.$;
@@ -219,6 +219,11 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor {
this.control = new NotebookCodeEditors(this.list, this.renderedEditors); this.control = new NotebookCodeEditors(this.list, this.renderedEditors);
this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this, this.environmentSerice); 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.list.rowsContainer.appendChild(this.webview.element);
this._register(this.list); this._register(this.list);
} }
@@ -697,7 +702,17 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor {
return this.outputRenderer; return this.outputRenderer;
} }
postMessage(message: any) {
this.webview?.webview.sendMessage(message);
}
//#endregion //#endregion
toJSON(): any {
return {
notebookHandle: this.viewModel?.handle
};
}
} }
const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground';
@@ -717,12 +732,12 @@ registerThemingParticipant((theme, collector) => {
} }
const link = theme.getColor(textLinkForeground); const link = theme.getColor(textLinkForeground);
if (link) { 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); const activeLink = theme.getColor(textLinkActiveForeground);
if (activeLink) { if (activeLink) {
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell a:hover, collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .output a:hover,
.monaco-workbench .part.editor > .content .notebook-editor .cell a:active { color: ${activeLink}; }`); .monaco-workbench .part.editor > .content .notebook-editor .cell .output a:active { color: ${activeLink}; }`);
} }
const shortcut = theme.getColor(textPreformatForeground); const shortcut = theme.getColor(textPreformatForeground);
if (shortcut) { if (shortcut) {
@@ -756,5 +771,7 @@ registerThemingParticipant((theme, collector) => {
// Cell Margin // 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 .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); }`);
}); });
@@ -29,7 +29,8 @@ export interface IMainNotebookController {
updateNotebookActiveCell(uri: URI, cellHandle: number): void; updateNotebookActiveCell(uri: URI, cellHandle: number): void;
createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise<NotebookCellTextModel | undefined>; createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise<NotebookCellTextModel | undefined>;
deleteCell(uri: URI, index: number): Promise<boolean> deleteCell(uri: URI, index: number): Promise<boolean>
executeNotebookActiveCell(uri: URI): void; executeNotebookActiveCell(uri: URI): Promise<void>;
onDidReceiveMessage(uri: URI, message: any): void;
destoryNotebookDocument(notebook: INotebookTextModel): Promise<void>; destoryNotebookDocument(notebook: INotebookTextModel): Promise<void>;
save(uri: URI): Promise<boolean>; save(uri: URI): Promise<boolean>;
} }
@@ -54,6 +55,7 @@ export interface INotebookService {
destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void;
updateActiveNotebookDocument(viewType: string, resource: URI): void; updateActiveNotebookDocument(viewType: string, resource: URI): void;
save(viewType: string, resource: URI): Promise<boolean>; save(viewType: string, resource: URI): Promise<boolean>;
onDidReceiveMessage(viewType: string, uri: URI, message: any): void;
} }
export class NotebookProviderInfoStore { export class NotebookProviderInfoStore {
@@ -325,6 +327,14 @@ export class NotebookService extends Disposable implements INotebookService {
return false; 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 { private _onWillDispose(model: INotebookTextModel): void {
let modelId = MODEL_ID(model.uri); let modelId = MODEL_ID(model.uri);
let modelData = this._models[modelId]; let modelData = this._models[modelId];
@@ -16,8 +16,10 @@ import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/br
import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader';
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants';
import { Emitter, Event } from 'vs/base/common/event';
export interface IDimentionMessage { export interface IDimentionMessage {
__vscode_notebook_message: boolean;
type: 'dimension'; type: 'dimension';
id: string; id: string;
data: DOM.Dimension; data: DOM.Dimension;
@@ -25,6 +27,7 @@ export interface IDimentionMessage {
export interface IScrollAckMessage { export interface IScrollAckMessage {
__vscode_notebook_message: boolean;
type: 'scroll-ack'; type: 'scroll-ack';
data: { top: number }; data: { top: number };
version: number; version: number;
@@ -78,6 +81,9 @@ export class BackLayerWebView extends Disposable {
preloadsCache: Map<string, boolean> = new Map(); preloadsCache: Map<string, boolean> = new Map();
localResourceRootsCache: URI[] | undefined = undefined; localResourceRootsCache: URI[] | undefined = undefined;
rendererRootsCache: URI[] = []; rendererRootsCache: URI[] = [];
private readonly _onMessage = this._register(new Emitter<any>());
public readonly onMessage: Event<any> = this._onMessage.event;
constructor(public webviewService: IWebviewService, public notebookService: INotebookService, public notebookEditor: INotebookEditor, public environmentSerice: IEnvironmentService) { constructor(public webviewService: IWebviewService, public notebookService: INotebookService, public notebookEditor: INotebookEditor, public environmentSerice: IEnvironmentService) {
super(); super();
@@ -154,6 +160,7 @@ export class BackLayerWebView extends Disposable {
for (let entry of entries) { for (let entry of entries) {
if (entry.target.id === id && entry.contentRect) { if (entry.target.id === id && entry.contentRect) {
vscode.postMessage({ vscode.postMessage({
__vscode_notebook_message: true,
type: 'dimension', type: 'dimension',
id: id, id: id,
data: { data: {
@@ -198,6 +205,7 @@ export class BackLayerWebView extends Disposable {
resizeObserve(outputNode, outputId); resizeObserve(outputNode, outputId);
vscode.postMessage({ vscode.postMessage({
__vscode_notebook_message: true,
type: 'dimension', type: 'dimension',
id: outputId, id: outputId,
data: { data: {
@@ -255,6 +263,7 @@ export class BackLayerWebView extends Disposable {
})); }));
this._register(this.webview.onMessage((data: IMessage) => { this._register(this.webview.onMessage((data: IMessage) => {
if (data.__vscode_notebook_message) {
if (data.type === 'dimension') { if (data.type === 'dimension') {
let output = this.reversedInsetMapping.get(data.id); let output = this.reversedInsetMapping.get(data.id);
@@ -276,6 +285,10 @@ export class BackLayerWebView extends Disposable {
// const top = data.data.top; // const top = data.data.top;
// console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); // 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(), { const webview = webviewService.createWebviewElement('' + UUID.generateUuid(), {
enableFindWidget: false, enableFindWidget: false,
}, { }, {
allowMultipleAPIAcquire: true,
allowScripts: true, allowScripts: true,
localResourceRoots: this.localResourceRootsCache localResourceRoots: this.localResourceRootsCache
}); });
@@ -396,6 +410,7 @@ export class BackLayerWebView extends Disposable {
const mixedResourceRoots = [...(this.localResourceRootsCache || []), ...this.rendererRootsCache]; const mixedResourceRoots = [...(this.localResourceRootsCache || []), ...this.rendererRootsCache];
this.webview.contentOptions = { this.webview.contentOptions = {
allowMultipleAPIAcquire: true,
allowScripts: true, allowScripts: true,
enableCommandUris: true, enableCommandUris: true,
localResourceRoots: mixedResourceRoots localResourceRoots: mixedResourceRoots
@@ -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 {
}
}
@@ -15,22 +15,27 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; 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 { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; 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 { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { CellRenderTemplate, INotebookEditor, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; 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 { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell';
import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellViewModel } from '../../viewModel/notebookCellViewModel'; import { CellViewModel } from '../../viewModel/notebookCellViewModel';
import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { MenuItemAction } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus';
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';
export class NotebookCellListDelegate implements IListVirtualDelegate<ICellViewModel> { const $ = DOM.$;
export class NotebookCellListDelegate implements IListVirtualDelegate<CellViewModel> {
private _lineHeight: number; private _lineHeight: number;
private _toolbarHeight = EDITOR_TOOLBAR_HEIGHT; private _toolbarHeight = EDITOR_TOOLBAR_HEIGHT;
@@ -68,6 +73,7 @@ abstract class AbstractCellRenderer {
private readonly configurationService: IConfigurationService, private readonly configurationService: IConfigurationService,
private readonly keybindingService: IKeybindingService, private readonly keybindingService: IKeybindingService,
private readonly notificationService: INotificationService, private readonly notificationService: INotificationService,
protected readonly contextKeyService: IContextKeyService,
language: string, language: string,
) { ) {
const editorOptions = deepClone(this.configurationService.getValue<IEditorOptions>('editor', { overrideIdentifier: language })); const editorOptions = deepClone(this.configurationService.getValue<IEditorOptions>('editor', { overrideIdentifier: language }));
@@ -108,6 +114,13 @@ abstract class AbstractCellRenderer {
return toolbar; 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) { showContextMenu(listIndex: number | undefined, element: CellViewModel, x: number, y: number) {
const actions: IAction[] = [ const actions: IAction[] = [
this.instantiationService.createInstance(InsertCodeCellAboveAction), this.instantiationService.createInstance(InsertCodeCellAboveAction),
@@ -150,8 +163,9 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR
@IContextMenuService contextMenuService: IContextMenuService, @IContextMenuService contextMenuService: IContextMenuService,
@IKeybindingService keybindingService: IKeybindingService, @IKeybindingService keybindingService: IKeybindingService,
@INotificationService notificationService: INotificationService, @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() { get templateId() {
@@ -165,14 +179,6 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR
const disposables = new DisposableStore(); const disposables = new DisposableStore();
const toolbar = this.createToolbar(container); 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); disposables.add(toolbar);
container.appendChild(codeInnerContent); container.appendChild(codeInnerContent);
@@ -181,16 +187,11 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR
DOM.addClasses(innerContent, 'cell', 'markdown'); DOM.addClasses(innerContent, 'cell', 'markdown');
container.appendChild(innerContent); 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')); DOM.append(container, DOM.$('.notebook-cell-focus-indicator'));
return { return {
container: container, container: container,
cellContainer: innerContent, cellContainer: innerContent,
menuContainer: action,
editingContainer: codeInnerContent, editingContainer: codeInnerContent,
disposables, disposables,
toolbar toolbar
@@ -212,32 +213,67 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR
} }
let elementDisposable = this.disposables.get(element); 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)); 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 = <INotebookCellActionContext>{ templateData.toolbar!.context = <INotebookCellActionContext>{
cell: element, 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[] { getAdditionalContextMenuActions(): IAction[] {
return [ return [
this.instantiationService.createInstance(EditCellAction), this.instantiationService.createInstance(EditCellAction),
@@ -269,8 +305,9 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
@IInstantiationService instantiationService: IInstantiationService, @IInstantiationService instantiationService: IInstantiationService,
@IKeybindingService keybindingService: IKeybindingService, @IKeybindingService keybindingService: IKeybindingService,
@INotificationService notificationService: INotificationService, @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() { get templateId() {
@@ -279,9 +316,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
renderTemplate(container: HTMLElement): CellRenderTemplate { renderTemplate(container: HTMLElement): CellRenderTemplate {
const disposables = new DisposableStore(); 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); const toolbar = this.createToolbar(container);
toolbar.setActions([ toolbar.setActions([
this.instantiationService.createInstance(MoveCellUpAction), this.instantiationService.createInstance(MoveCellUpAction),
@@ -291,19 +325,22 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
])(); ])();
disposables.add(toolbar); disposables.add(toolbar);
const cellContainer = document.createElement('div'); const cellContainer = DOM.append(container, $('.cell.code'));
DOM.addClasses(cellContainer, 'cell', 'code'); const runButtonContainer = DOM.append(cellContainer, $('.run-button-container'));
container.appendChild(cellContainer); const runToolbar = this.createToolbar(runButtonContainer);
const editor = this.instantiationService.createInstance(CodeEditorWidget, cellContainer, { 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, ...this.editorOptions,
dimension: { dimension: {
width: 0, width: 0,
height: 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')); 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'); DOM.addClasses(outputContainer, 'output');
container.appendChild(outputContainer); container.appendChild(outputContainer);
const progressBar = new ProgressBar(editorContainer);
progressBar.hide();
disposables.add(progressBar);
return { return {
container, container,
cellContainer, cellContainer,
menuContainer, editorContainer,
progressBar,
focusIndicator, focusIndicator,
toolbar, toolbar,
runToolbar,
outputContainer, outputContainer,
editor, editor,
disposables disposables
@@ -339,24 +382,6 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
const elementDisposable = this.disposables.get(element); 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)); elementDisposable?.add(this.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData));
this.renderedEditors.set(element, templateData.editor); this.renderedEditors.set(element, templateData.editor);
@@ -364,11 +389,57 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
templateData.focusIndicator!.style.height = `${element.getIndicatorHeight()}px`; templateData.focusIndicator!.style.height = `${element.getIndicatorHeight()}px`;
})); }));
templateData.toolbar!.context = <INotebookCellActionContext>{ const toolbarContext = <INotebookCellActionContext>{
cell: element, 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[] { getAdditionalContextMenuActions(): IAction[] {
return []; return [];
@@ -14,7 +14,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; 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 { interface IMimeTypeRenderer extends IQuickPickItem {
index: number; index: number;
@@ -34,7 +34,8 @@ export class CodeCell extends Disposable {
let width: number; let width: number;
const listDimension = notebookEditor.getLayoutInfo(); const listDimension = notebookEditor.getLayoutInfo();
width = listDimension.width - CELL_MARGIN * 2; width = listDimension.width - CELL_MARGIN * 2 - RUN_BUTTON_WIDTH;
const lineNum = viewCell.lineCount; const lineNum = viewCell.lineCount;
const lineHeight = notebookEditor.getLayoutInfo().fontInfo.lineHeight; const lineHeight = notebookEditor.getLayoutInfo().fontInfo.lineHeight;
const totalHeight = lineNum * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; 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 realContentHeight = templateData.editor?.getContentHeight();
let width: number; let width: number;
const listDimension = notebookEditor.getLayoutInfo(); const listDimension = notebookEditor.getLayoutInfo();
width = listDimension.width - CELL_MARGIN * 2; width = listDimension.width - CELL_MARGIN * 2 - RUN_BUTTON_WIDTH;
if (realContentHeight !== undefined && realContentHeight !== totalHeight) { if (realContentHeight !== undefined && realContentHeight !== totalHeight) {
templateData.editor?.layout( templateData.editor?.layout(
@@ -84,7 +85,7 @@ export class CodeCell extends Disposable {
} }
})); }));
let cellWidthResizeObserver = getResizesObserver(templateData.cellContainer, { let cellWidthResizeObserver = getResizesObserver(templateData.editorContainer!, {
width: width, width: width,
height: totalHeight height: totalHeight
}, () => { }, () => {
@@ -267,7 +268,7 @@ export class CodeCell extends Disposable {
let clientHeight = outputItemDiv.clientHeight; let clientHeight = outputItemDiv.clientHeight;
let listDimension = this.notebookEditor.getLayoutInfo(); let listDimension = this.notebookEditor.getLayoutInfo();
let dimension = listDimension ? { let dimension = listDimension ? {
width: listDimension.width - CELL_MARGIN * 2, width: listDimension.width - CELL_MARGIN * 2 - RUN_BUTTON_WIDTH,
height: clientHeight height: clientHeight
} : undefined; } : undefined;
const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => { const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => {
@@ -55,6 +55,10 @@ export class CellViewModel extends Disposable implements ICellViewModel {
return this.cell.outputs; return this.cell.outputs;
} }
get metadata() {
return this.cell.metadata;
}
private _state: CellState = CellState.Preview; private _state: CellState = CellState.Preview;
get state(): CellState { get state(): CellState {
@@ -519,4 +523,10 @@ export class CellViewModel extends Disposable implements ICellViewModel {
this._outputsTop = new PrefixSumComputer(values); this._outputsTop = new PrefixSumComputer(values);
} }
} }
toJSON(): any {
return {
handle: this.handle
};
}
} }
@@ -81,6 +81,10 @@ export class NotebookViewModel extends Disposable {
return this._model.notebook.uri; return this._model.notebook.uri;
} }
get metadata() {
return this._model.notebook.metadata;
}
private readonly _onDidChangeViewCells = new Emitter<INotebookViewCellsUpdateEvent>(); private readonly _onDidChangeViewCells = new Emitter<INotebookViewCellsUpdateEvent>();
get onDidChangeViewCells(): Event<INotebookViewCellsUpdateEvent> { return this._onDidChangeViewCells.event; } get onDidChangeViewCells(): Event<INotebookViewCellsUpdateEvent> { return this._onDidChangeViewCells.event; }
@@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; 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 { export class NotebookTextModel extends Disposable implements INotebookTextModel {
private readonly _onWillDispose: Emitter<void> = this._register(new Emitter<void>()); private readonly _onWillDispose: Emitter<void> = this._register(new Emitter<void>());
@@ -21,6 +21,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
cells: NotebookCellTextModel[]; cells: NotebookCellTextModel[];
activeCell: NotebookCellTextModel | undefined; activeCell: NotebookCellTextModel | undefined;
languages: string[] = []; languages: string[] = [];
metadata: NotebookDocumentMetadata | undefined = undefined;
renderers = new Set<number>(); renderers = new Set<number>();
constructor( constructor(
@@ -36,6 +37,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
this.languages = languages; this.languages = languages;
} }
updateNotebookMetadata(metadata: NotebookDocumentMetadata | undefined) {
this.metadata = metadata;
}
updateRenderers(renderers: number[]) { updateRenderers(renderers: number[]) {
renderers.forEach(render => { renderers.forEach(render => {
this.renderers.add(render); this.renderers.add(render);
@@ -36,6 +36,14 @@ export const NOTEBOOK_DISPLAY_ORDER = [
'text/plain' 'text/plain'
]; ];
export interface NotebookDocumentMetadata {
editable: boolean;
}
export interface NotebookCellMetadata {
editable: boolean;
}
export interface INotebookDisplayOrder { export interface INotebookDisplayOrder {
defaultOrder: string[]; defaultOrder: string[];
userOrder?: string[]; userOrder?: string[];
@@ -122,6 +130,7 @@ export interface ICell {
language: string; language: string;
cellKind: CellKind; cellKind: CellKind;
outputs: IOutput[]; outputs: IOutput[];
metadata?: NotebookCellMetadata;
onDidChangeOutputs?: Event<NotebookCellOutputsSplice[]>; onDidChangeOutputs?: Event<NotebookCellOutputsSplice[]>;
resolveTextBufferFactory(): PieceTreeTextBufferFactory; resolveTextBufferFactory(): PieceTreeTextBufferFactory;
// TODO@rebornix it should be later on replaced by moving textmodel resolution into CellTextModel // TODO@rebornix it should be later on replaced by moving textmodel resolution into CellTextModel
@@ -69,6 +69,12 @@ export class TestNotebookEditor implements INotebookEditor {
constructor( constructor(
) { } ) { }
isNotebookEditor = true;
postMessage(message: any): void {
throw new Error('Method not implemented.');
}
setCellSelection(cell: CellViewModel, selection: Range): void { setCellSelection(cell: CellViewModel, selection: Range): void {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
@@ -10,7 +10,7 @@ import { prepareQuery, IPreparedQuery, compareItemsByScore, scoreItem, ScorerCac
import { IFileQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IFileQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { getOutOfWorkspaceEditorResources, extractRangeFromFilter, IWorkbenchSearchConfiguration } from 'vs/workbench/contrib/search/common/search'; 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 { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { untildify } from 'vs/base/common/labels'; import { untildify } from 'vs/base/common/labels';
import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService';
@@ -54,13 +54,24 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
private readonly pickState = new class { private readonly pickState = new class {
scorerCache: ScorerCache = Object.create(null); scorerCache: ScorerCache = Object.create(null);
fileQueryCache: FileQueryCacheState | undefined; fileQueryCache: FileQueryCacheState | undefined = undefined;
lastOriginalFilter: string | undefined = undefined;
lastFilter: string | undefined = undefined;
lastRange: IRange | undefined = undefined;
constructor(private readonly provider: AnythingQuickAccessProvider) { } constructor(private readonly provider: AnythingQuickAccessProvider) { }
reset(): void { reset(): void {
// Caches
this.fileQueryCache = this.provider.createFileQueryCache(); this.fileQueryCache = this.provider.createFileQueryCache();
this.scorerCache = Object.create(null); this.scorerCache = Object.create(null);
// Other
this.lastOriginalFilter = undefined;
this.lastFilter = undefined;
this.lastRange = undefined;
} }
}(this); }(this);
@@ -91,6 +102,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, openEditorPinned: !editorConfig.enablePreviewFromQuickOpen,
openSideBySideDirection: editorConfig.openSideBySideDirection, openSideBySideDirection: editorConfig.openSideBySideDirection,
includeSymbols: searchConfig.search.quickOpen.includeSymbols, includeSymbols: searchConfig.search.quickOpen.includeSymbols,
workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter,
includeHistory: searchConfig.search.quickOpen.includeHistory, includeHistory: searchConfig.search.quickOpen.includeHistory,
shortAutoSaveDelay: this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY shortAutoSaveDelay: this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY
}; };
@@ -105,19 +117,37 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
return super.provide(picker, token); return super.provide(picker, token);
} }
protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicksType<IAnythingQuickPickItem> { protected getPicks(originalFilter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicksType<IAnythingQuickPickItem> | null {
// Find a suitable range from the pattern looking for ":", "#" or "," // Find a suitable range from the pattern looking for ":", "#" or ","
let range: IRange | undefined = undefined; const filterWithRange = extractRangeFromFilter(originalFilter);
const filterWithRange = extractRangeFromFilter(filter);
// Update filter with normalized values
let filter: string;
if (filterWithRange) { if (filterWithRange) {
filter = filterWithRange.filter; 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
// (:<line>:<column>) 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 query = prepareQuery(filter);
const historyEditorPicks = this.getEditorHistoryPicks(query, range); const historyEditorPicks = this.getEditorHistoryPicks(query);
return { return {
@@ -139,7 +169,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
} }
} }
const additionalPicks = await this.getAdditionalPicks(query, range, additionalPicksExcludes, token); const additionalPicks = await this.getAdditionalPicks(query, additionalPicksExcludes, token);
if (token.isCancellationRequested) { if (token.isCancellationRequested) {
return []; return [];
} }
@@ -152,12 +182,12 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
}; };
} }
private async getAdditionalPicks(query: IPreparedQuery, range: IRange | undefined, excludes: ResourceMap<boolean>, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> { private async getAdditionalPicks(query: IPreparedQuery, excludes: ResourceMap<boolean>, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
// Resolve file and symbol picks (if enabled) // Resolve file and symbol picks (if enabled)
const [filePicks, symbolPicks] = await Promise.all([ const [filePicks, symbolPicks] = await Promise.all([
this.getFilePicks(query, range, excludes, token), this.getFilePicks(query, excludes, token),
this.getSymbolPicks(query, range, token) this.getSymbolPicks(query, token)
]); ]);
if (token.isCancellationRequested) { if (token.isCancellationRequested) {
@@ -193,11 +223,12 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
private readonly labelOnlyEditorHistoryPickAccessor = new QuickPickItemScorerAccessor({ skipDescription: true }); private readonly labelOnlyEditorHistoryPickAccessor = new QuickPickItemScorerAccessor({ skipDescription: true });
protected getEditorHistoryPicks(query: IPreparedQuery, range: IRange | undefined): Array<IAnythingQuickPickItem> { protected getEditorHistoryPicks(query: IPreparedQuery): Array<IAnythingQuickPickItem> {
const configuration = this.configuration;
// Just return all history entries if not searching // Just return all history entries if not searching
if (!query.value) { 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) { if (!this.configuration.includeHistory) {
@@ -215,7 +246,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
continue; // exclude editors without file resource if we are searching by pattern continue; // exclude editors without file resource if we are searching by pattern
} }
const editorHistoryPick = this.createAnythingPick(editor, range); const editorHistoryPick = this.createAnythingPick(editor, configuration);
const { score, labelMatch, descriptionMatch } = scoreItem(editorHistoryPick, query, false, editorHistoryScorerAccessor, this.pickState.scorerCache); const { score, labelMatch, descriptionMatch } = scoreItem(editorHistoryPick, query, false, editorHistoryScorerAccessor, this.pickState.scorerCache);
if (!score) { if (!score) {
@@ -238,7 +269,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
//#region File Search //#region File Search
private fileQueryDelayer = this._register(new ThrottledDelayer<IFileMatch[]>(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY)); private fileQueryDelayer = this._register(new ThrottledDelayer<URI[]>(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY));
private fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder); private fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder);
@@ -251,7 +282,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
).load(); ).load();
} }
protected async getFilePicks(query: IPreparedQuery, range: IRange | undefined, excludes: ResourceMap<boolean>, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> { protected async getFilePicks(query: IPreparedQuery, excludes: ResourceMap<boolean>, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
if (!query.value) { if (!query.value) {
return []; return [];
} }
@@ -263,9 +294,9 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
} }
// Use absolute path result as only results if present // Use absolute path result as only results if present
let fileMatches: Array<IFileMatch<URI>>; let fileMatches: Array<URI>;
if (absolutePathResult) { if (absolutePathResult) {
fileMatches = [{ resource: absolutePathResult }]; fileMatches = [absolutePathResult];
} }
// Otherwise run the file search (with a delayer if cache is not ready yet) // Otherwise run the file search (with a delayer if cache is not ready yet)
@@ -288,13 +319,17 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
} }
// Filter excludes & convert to picks // Filter excludes & convert to picks
const configuration = this.configuration;
return fileMatches return fileMatches
.filter(fileMatch => !excludes.has(fileMatch.resource)) .filter(resource => !excludes.has(resource))
.map(fileMatch => this.createAnythingPick(fileMatch.resource, range)); .map(resource => this.createAnythingPick(resource, configuration));
} }
private async doFileSearch(query: IPreparedQuery, token: CancellationToken): Promise<IFileMatch[]> { private async doFileSearch(query: IPreparedQuery, token: CancellationToken): Promise<URI[]> {
const { results } = await this.searchService.fileSearch( const [fileSearchResults, relativePathFileResults] = await Promise.all([
// File search: this is a search over all files of the workspace using the provided pattern
this.searchService.fileSearch(
this.fileQueryBuilder.file( this.fileQueryBuilder.file(
this.contextService.getWorkspace().folders, this.contextService.getWorkspace().folders,
this.getFileQueryOptions({ this.getFileQueryOptions({
@@ -302,9 +337,19 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
cacheKey: this.pickState.fileQueryCache?.cacheKey, cacheKey: this.pickState.fileQueryCache?.cacheKey,
maxResults: AnythingQuickAccessProvider.MAX_RESULTS maxResults: AnythingQuickAccessProvider.MAX_RESULTS
}) })
), token); ), token),
return results; // 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 { private getFileQueryOptions(input: { filePattern?: string, cacheKey?: string, maxResults?: number }): IFileQueryBuilderOptions {
@@ -321,7 +366,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
} }
private async getAbsolutePathFileResult(query: IPreparedQuery, token: CancellationToken): Promise<URI | undefined> { private async getAbsolutePathFileResult(query: IPreparedQuery, token: CancellationToken): Promise<URI | undefined> {
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) { if (token.isCancellationRequested) {
return undefined; // {{SQL CARBON EDIT}} strict-null return undefined; // {{SQL CARBON EDIT}} strict-null
} }
@@ -342,10 +391,47 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
} }
try { try {
return (await this.fileService.resolve(resource)).isDirectory ? undefined : resource; if ((await this.fileService.resolve(resource)).isFile) {
} catch (error) { return resource;
// ignore
} }
} catch (error) {
// ignore if file does not exist
}
}
return undefined; // {{SQL CARBON EDIT}} strict-null
}
private async getRelativePathFileResults(query: IPreparedQuery, token: CancellationToken): Promise<URI[] | undefined> {
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 return undefined; // {{SQL CARBON EDIT}} strict-null
@@ -358,18 +444,23 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
private symbolsQuickAccess = this._register(this.instantiationService.createInstance(SymbolsQuickAccessProvider)); private symbolsQuickAccess = this._register(this.instantiationService.createInstance(SymbolsQuickAccessProvider));
protected async getSymbolPicks(query: IPreparedQuery, range: IRange | undefined, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> { protected async getSymbolPicks(query: IPreparedQuery, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
const configuration = this.configuration;
if ( if (
!query.value || // we need a value for search for !query.value || // we need a value for search for
!this.configuration.includeSymbols || // we need to enable symbols in search !configuration.includeSymbols || // we need to enable symbols in search
range // a range is an indicator for just searching for files this.pickState.lastRange // a range is an indicator for just searching for files
) { ) {
return []; return [];
} }
// Delegate to the existing symbols quick access // Delegate to the existing symbols quick access
// but skip local results and also do not sort // 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 //#endregion
@@ -377,7 +468,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
//#region Helpers //#region Helpers
private createAnythingPick(resourceOrEditor: URI | IEditorInput | IResourceEditorInput, range: IRange | undefined): IAnythingQuickPickItem { private createAnythingPick(resourceOrEditor: URI | IEditorInput | IResourceEditorInput, configuration: { shortAutoSaveDelay: boolean, openSideBySideDirection: 'right' | 'down' | undefined }): IAnythingQuickPickItem {
const isEditorHistoryEntry = !URI.isUri(resourceOrEditor); const isEditorHistoryEntry = !URI.isUri(resourceOrEditor);
let resource: URI | undefined; let resource: URI | undefined;
@@ -394,7 +485,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
resource = URI.isUri(resourceOrEditor) ? resourceOrEditor : (resourceOrEditor as IResourceEditorInput).resource; resource = URI.isUri(resourceOrEditor) ? resourceOrEditor : (resourceOrEditor as IResourceEditorInput).resource;
label = basenameOrAuthority(resource); label = basenameOrAuthority(resource);
description = this.labelService.getUriLabel(dirname(resource), { relative: true }); description = this.labelService.getUriLabel(dirname(resource), { relative: true });
isDirty = this.workingCopyService.isDirty(resource) && !this.configuration.shortAutoSaveDelay; isDirty = this.workingCopyService.isDirty(resource) && !configuration.shortAutoSaveDelay;
} }
return { return {
@@ -406,7 +497,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
description, description,
iconClasses: getIconClasses(this.modelService, this.modeService, resource), iconClasses: getIconClasses(this.modelService, this.modeService, resource),
buttons: (() => { buttons: (() => {
const openSideBySideDirection = this.configuration.openSideBySideDirection; const openSideBySideDirection = configuration.openSideBySideDirection;
const buttons: IQuickInputButton[] = []; const buttons: IQuickInputButton[] = [];
// Open to side / below // Open to side / below
@@ -431,7 +522,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
// Open to side / below // Open to side / below
case 0: case 0:
this.openAnything(resourceOrEditor, { keyMods, range, forceOpenSideBySide: true }); this.openAnything(resourceOrEditor, { keyMods, range: this.pickState.lastRange, forceOpenSideBySide: true });
return TriggerAction.CLOSE_PICKER; return TriggerAction.CLOSE_PICKER;
// Remove from History // Remove from History
@@ -445,7 +536,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
return TriggerAction.NO_ACTION; return TriggerAction.NO_ACTION;
}, },
accept: (keyMods, event) => this.openAnything(resourceOrEditor, { keyMods, range, preserveFocus: event.inBackground }) accept: (keyMods, event) => this.openAnything(resourceOrEditor, { keyMods, range: this.pickState.lastRange, preserveFocus: event.inBackground })
}; };
} }
@@ -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."), description: nls.localize('search.quickOpen.includeSymbols', "Whether to include results from a global symbol search in the file results for Quick Open."),
default: false 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': { 'search.quickOpen.includeHistory': {
type: 'boolean', type: 'boolean',
description: nls.localize('search.quickOpen.includeHistory', "Whether to include results from recently opened files in the file results for Quick Open."), 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', type: 'string',
enum: ['auto', 'alwaysCollapse', 'alwaysExpand'], enum: ['auto', 'alwaysCollapse', 'alwaysExpand'],
enumDescriptions: [ 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."),
'', '',
'' ''
], ],
@@ -10,8 +10,8 @@ import { stripWildcards } from 'vs/base/common/strings';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore } from 'vs/base/common/lifecycle'; import { DisposableStore } from 'vs/base/common/lifecycle';
import { ThrottledDelayer } from 'vs/base/common/async'; import { ThrottledDelayer } from 'vs/base/common/async';
import { getWorkspaceSymbols, IWorkspaceSymbol, IWorkspaceSymbolProvider } from 'vs/workbench/contrib/search/common/search'; import { getWorkspaceSymbols, IWorkspaceSymbol, IWorkspaceSymbolProvider, IWorkbenchSearchConfiguration } from 'vs/workbench/contrib/search/common/search';
import { SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; import { SymbolKinds, SymbolTag, SymbolKind } from 'vs/editor/common/modes';
import { ILabelService } from 'vs/platform/label/common/label'; import { ILabelService } from 'vs/platform/label/common/label';
import { Schemas } from 'vs/base/common/network'; import { Schemas } from 'vs/base/common/network';
import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IOpenerService } from 'vs/platform/opener/common/opener';
@@ -37,6 +37,16 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
private static readonly TYPING_SEARCH_DELAY = 200; // this delay accommodates for the user typing a word and then stops typing to start searching private static readonly TYPING_SEARCH_DELAY = 200; // this delay accommodates for the user typing a word and then stops typing to start searching
private static TREAT_AS_GLOBAL_SYMBOL_TYPES = new Set<SymbolKind>([
SymbolKind.Class,
SymbolKind.Enum,
SymbolKind.File,
SymbolKind.Interface,
SymbolKind.Namespace,
SymbolKind.Package,
SymbolKind.Module
]);
private delayer = this._register(new ThrottledDelayer<ISymbolQuickPickItem[]>(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY)); private delayer = this._register(new ThrottledDelayer<ISymbolQuickPickItem[]>(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY));
private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService)); private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService));
@@ -53,18 +63,20 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
private get configuration() { private get configuration() {
const editorConfig = this.configurationService.getValue<IWorkbenchEditorConfiguration>().workbench.editor; const editorConfig = this.configurationService.getValue<IWorkbenchEditorConfiguration>().workbench.editor;
const searchConfig = this.configurationService.getValue<IWorkbenchSearchConfiguration>();
return { return {
openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, openEditorPinned: !editorConfig.enablePreviewFromQuickOpen,
openSideBySideDirection: editorConfig.openSideBySideDirection openSideBySideDirection: editorConfig.openSideBySideDirection,
workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter
}; };
} }
protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> { protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> {
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<Array<ISymbolQuickPickItem>> { async getSymbolPicks(filter: string, options: { skipLocal?: boolean, skipSorting?: boolean, delay?: number } | undefined, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> {
return this.delayer.trigger(async () => { return this.delayer.trigger(async () => {
if (token.isCancellationRequested) { if (token.isCancellationRequested) {
return []; return [];
@@ -74,7 +86,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
}, options?.delay); }, options?.delay);
} }
private async doGetSymbolPicks(filter: string, options: { skipLocal: boolean, skipSorting: boolean } | undefined, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> { private async doGetSymbolPicks(filter: string, options: { skipLocal?: boolean, skipSorting?: boolean } | undefined, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> {
const workspaceSymbols = await getWorkspaceSymbols(filter, token); const workspaceSymbols = await getWorkspaceSymbols(filter, token);
if (token.isCancellationRequested) { if (token.isCancellationRequested) {
return []; return [];
@@ -92,8 +104,12 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
const symbolsExcludedByResource = new ResourceMap<boolean>(); const symbolsExcludedByResource = new ResourceMap<boolean>();
for (const [provider, symbols] of workspaceSymbols) { for (const [provider, symbols] of workspaceSymbols) {
for (const symbol of symbols) { 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 // Score by symbol label
@@ -77,6 +77,7 @@ export interface IWorkbenchSearchConfigurationProperties extends ISearchConfigur
quickOpen: { quickOpen: {
includeSymbols: boolean; includeSymbols: boolean;
includeHistory: boolean; includeHistory: boolean;
workspaceSymbolsFilter: 'default' | 'reduced' | 'all';
}; };
} }
@@ -102,7 +103,12 @@ export function getOutOfWorkspaceEditorResources(accessor: ServicesAccessor): UR
// Supports patterns of <path><#|:|(><line><#|:|,><col?> // Supports patterns of <path><#|:|(><line><#|:|,><col?>
const LINE_COLON_PATTERN = /\s?[#:\(](\d*)([#:,](\d*))?\)?\s*$/; 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) { if (!filter) {
return undefined; return undefined;
} }
@@ -151,7 +157,7 @@ export function extractRangeFromFilter(filter: string): { filter: string, range:
if (patternMatch && range) { if (patternMatch && range) {
return { return {
filter: filter.substr(0, patternMatch.index), // clear range suffix from search value filter: filter.substr(0, patternMatch.index), // clear range suffix from search value
range: range range
}; };
} }
@@ -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()) { for (const mode of modeService.getRegisteredModes()) {
const label = modeService.getLanguageName(mode); const label = modeService.getLanguageName(mode);
if (label && !seen.has(mode)) { if (label && !seen.has(mode)) {
@@ -219,7 +219,7 @@ CommandsRegistry.registerCommand(id, async (accessor): Promise<any> => {
const globalSnippetPicks: SnippetPick[] = [{ const globalSnippetPicks: SnippetPick[] = [{
scope: nls.localize('new.global_scope', 'global'), scope: nls.localize('new.global_scope', 'global'),
label: nls.localize('new.global', "New Global Snippets file..."), label: nls.localize('new.global', "New Global Snippets file..."),
uri: joinPath(envService.userRoamingDataHome, 'snippets') uri: envService.snippetsHome
}]; }];
const workspaceSnippetPicks: SnippetPick[] = []; const workspaceSnippetPicks: SnippetPick[] = [];
@@ -289,7 +289,7 @@ class SnippetsService implements ISnippetsService {
} }
private _initUserSnippets(): Promise<any> { private _initUserSnippets(): Promise<any> {
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)); return this._fileService.createFolder(userSnippetsFolder).then(() => this._initFolderSnippets(SnippetSource.User, userSnippetsFolder, this._disposables));
} }
@@ -368,11 +368,7 @@ export class TerminalTaskSystem implements ITaskSystem {
}); });
} }
private removeFromActiveTasks(task: Task): void { private removeInstances(task: Task) {
if (!this.activeTasks[task.getMapKey()]) {
return;
}
delete this.activeTasks[task.getMapKey()];
let commonKey = task._id.split('|')[0]; let commonKey = task._id.split('|')[0];
if (this.instances[commonKey]) { if (this.instances[commonKey]) {
this.instances[commonKey].removeInstance(); 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<TaskTerminateResponse> { public terminate(task: Task): Promise<TaskTerminateResponse> {
let activeTerminal = this.activeTasks[task.getMapKey()]; let activeTerminal = this.activeTasks[task.getMapKey()];
if (!activeTerminal) { if (!activeTerminal) {
@@ -466,6 +470,7 @@ export class TerminalTaskSystem implements ITaskSystem {
return Promise.all(promises).then((summaries): Promise<ITaskSummary> | ITaskSummary => { return Promise.all(promises).then((summaries): Promise<ITaskSummary> | ITaskSummary => {
for (let summary of summaries) { for (let summary of summaries) {
if (summary.exitCode !== 0) { if (summary.exitCode !== 0) {
this.removeInstances(task);
return { exitCode: summary.exitCode }; return { exitCode: summary.exitCode };
} }
} }
@@ -20,7 +20,7 @@ import { configurationTelemetry } from 'vs/platform/telemetry/common/telemetryUt
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { ITextFileService, ITextFileSaveEvent, ITextFileLoadEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextFileService, ITextFileSaveEvent, ITextFileLoadEvent } from 'vs/workbench/services/textfile/common/textfiles';
import { extname, basename, isEqual, isEqualOrParent, joinPath } from 'vs/base/common/resources'; import { extname, basename, isEqual, isEqualOrParent } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network'; import { Schemas } from 'vs/base/common/network';
import { guessMimeTypes } from 'vs/base/common/mime'; import { guessMimeTypes } from 'vs/base/common/mime';
@@ -175,7 +175,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr
} }
// Check for snippets // Check for snippets
if (isEqualOrParent(resource, joinPath(this.environmentService.userRoamingDataHome, 'snippets'))) { if (isEqualOrParent(resource, this.environmentService.snippetsHome)) {
return 'snippets'; return 'snippets';
} }
@@ -9,7 +9,7 @@ import { canceled, isPromiseCanceledError } from 'vs/base/common/errors';
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { isWeb } from 'vs/base/common/platform'; 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 { URI } from 'vs/base/common/uri';
import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
@@ -32,7 +32,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { import {
CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration,
SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, CONTEXT_SYNC_ENABLEMENT, 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'; } from 'vs/platform/userDataSync/common/userDataSync';
import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets';
import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity';
@@ -69,6 +69,7 @@ function getSyncAreaLabel(source: SyncResource): string {
switch (source) { switch (source) {
case SyncResource.Settings: return localize('settings', "Settings"); case SyncResource.Settings: return localize('settings', "Settings");
case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); 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.Extensions: return localize('extensions', "Extensions");
case SyncResource.GlobalState: return localize('ui state label', "UI State"); 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 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 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 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 configureSyncCommand = { id: 'workbench.userData.actions.configureSync', title: localize('configure sync', "Sync: Configure") };
const showSyncActivityCommand = { const showSyncActivityCommand = {
id: 'workbench.userData.actions.showSyncActivity', title(userDataSyncService: IUserDataSyncService): string { id: 'workbench.userData.actions.showSyncActivity', title(userDataSyncService: IUserDataSyncService): string {
@@ -291,6 +293,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
if (conflicts.length) { if (conflicts.length) {
const conflictsSources: SyncResource[] = conflicts.map(conflict => conflict.syncResource); const conflictsSources: SyncResource[] = conflicts.map(conflict => conflict.syncResource);
this.conflictsSources.set(conflictsSources.join(',')); this.conflictsSources.set(conflictsSources.join(','));
if (conflictsSources.indexOf(SyncResource.Snippets) !== -1) {
this.registerShowSnippetsConflictsAction();
}
// Clear and dispose conflicts those were cleared // Clear and dispose conflicts those were cleared
this.conflictsDisposables.forEach((disposable, conflictsSource) => { this.conflictsDisposables.forEach((disposable, conflictsSource) => {
@@ -301,8 +306,19 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}); });
for (const { syncResource, conflicts } of this.userDataSyncService.conflicts) { for (const { syncResource, conflicts } of this.userDataSyncService.conflicts) {
const conflictsEditorInput = this.getConflictsEditorInput(syncResource); const conflictsEditorInputs = this.getConflictsEditorInputs(syncResource);
if (!conflictsEditorInput && !this.conflictsDisposables.has(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 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()), 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(); handle.close();
// close opened conflicts editor previews // close opened conflicts editor previews
const conflictsEditorInput = this.getConflictsEditorInput(syncResource); const conflictsEditorInputs = this.getConflictsEditorInputs(syncResource);
if (conflictsEditorInput) { if (conflictsEditorInputs.length) {
conflictsEditorInput.dispose(); conflictsEditorInputs.forEach(input => input.dispose());
} }
this.conflictsDisposables.delete(syncResource); 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) { 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")); badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync"));
} else if (this.userDataSyncService.conflicts.length) { } 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) { if (badge) {
@@ -605,6 +621,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}, { }, {
id: SyncResource.Keybindings, id: SyncResource.Keybindings,
label: getSyncAreaLabel(SyncResource.Keybindings) label: getSyncAreaLabel(SyncResource.Keybindings)
}, {
id: SyncResource.Snippets,
label: getSyncAreaLabel(SyncResource.Snippets)
}, { }, {
id: SyncResource.Extensions, id: SyncResource.Extensions,
label: getSyncAreaLabel(SyncResource.Extensions) label: getSyncAreaLabel(SyncResource.Extensions)
@@ -712,6 +731,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
switch (source) { switch (source) {
case SyncResource.Settings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Settings, false); case SyncResource.Settings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Settings, false);
case SyncResource.Keybindings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Keybindings, 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.Extensions: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Extensions, false);
case SyncResource.GlobalState: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.GlobalState, 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 { private getConflictsEditorInputs(syncResource: SyncResource): DiffEditorInput[] {
return this.editorService.editors.filter(input => input instanceof DiffEditorInput && getSyncResourceFromLocalPreview(input.master.resource!, this.workbenchEnvironmentService) === syncResource)[0]; 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[] { private getAllConflictsEditorInputs(): IEditorInput[] {
@@ -752,6 +775,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
label = localize('settings conflicts preview', "Settings Conflicts (Remote ↔ Local)"); label = localize('settings conflicts preview', "Settings Conflicts (Remote ↔ Local)");
} else if (syncResource === SyncResource.Keybindings) { } else if (syncResource === SyncResource.Keybindings) {
label = localize('keybindings conflicts preview', "Keybindings Conflicts (Remote ↔ Local)"); 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({ await this.editorService.openEditor({
leftResource: conflict.remote, leftResource: conflict.remote,
@@ -775,6 +800,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
this.registerSignInAction(); this.registerSignInAction();
this.registerShowSettingsConflictsAction(); this.registerShowSettingsConflictsAction();
this.registerShowKeybindingsConflictsAction(); this.registerShowKeybindingsConflictsAction();
this.registerShowSnippetsConflictsAction();
this.registerSyncStatusAction(); this.registerSyncStatusAction();
this.registerTurnOffSyncAction(); this.registerTurnOffSyncAction();
@@ -894,7 +920,36 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
command: resolveKeybindingsConflictsCommand, command: resolveKeybindingsConflictsCommand,
when: resolveKeybindingsConflictsWhenContext, 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 { private registerSyncStatusAction(): void {
@@ -938,6 +993,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
case SyncResource.Keybindings: case SyncResource.Keybindings:
items.push({ id: resolveKeybindingsConflictsCommand.id, label: resolveKeybindingsConflictsCommand.title }); items.push({ id: resolveKeybindingsConflictsCommand.id, label: resolveKeybindingsConflictsCommand.title });
break; break;
case SyncResource.Snippets:
items.push({ id: resolveSnippetsConflictsCommand.id, label: resolveSnippetsConflictsCommand.title });
break;
} }
} }
items.push({ type: 'separator' }); items.push({ type: 'separator' });
@@ -1074,7 +1132,6 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio
constructor( constructor(
private editor: ICodeEditor, private editor: ICodeEditor,
@IInstantiationService private readonly instantiationService: IInstantiationService, @IInstantiationService private readonly instantiationService: IInstantiationService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@INotificationService private readonly notificationService: INotificationService, @INotificationService private readonly notificationService: INotificationService,
@IDialogService private readonly dialogService: IDialogService, @IDialogService private readonly dialogService: IDialogService,
@@ -1088,7 +1145,8 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio
} }
private registerListeners(): void { 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())); 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 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; return true;
} }
if (getSyncResourceFromRemotePreview(model.uri, this.environmentService) !== undefined) { if (syncResourceConflicts.conflicts.some(({ remote }) => isEqual(remote, model.uri))) {
return this.configurationService.getValue<boolean>('diffEditor.renderSideBySide'); return this.configurationService.getValue<boolean>('diffEditor.renderSideBySide');
} }
@@ -1121,16 +1184,17 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio
private createAcceptChangesWidgetRenderer(): void { private createAcceptChangesWidgetRenderer(): void {
if (!this.acceptChangesButton) { 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 acceptRemoteLabel = localize('accept remote', "Accept Remote");
const acceptLocalLabel = localize('accept local', "Accept Local"); const acceptLocalLabel = localize('accept local', "Accept Local");
this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptLocalLabel, null); this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptLocalLabel, null);
this._register(this.acceptChangesButton.onClick(async () => { this._register(this.acceptChangesButton.onClick(async () => {
const model = this.editor.getModel(); const model = this.editor.getModel();
if (model) { 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: syncResourceConflicts.syncResource, action: isRemote ? 'acceptRemote' : 'acceptLocal' });
this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: isRemote ? 'acceptRemote' : 'acceptLocal' }); const syncAreaLabel = getSyncAreaLabel(syncResourceConflicts.syncResource);
const syncAreaLabel = getSyncAreaLabel(conflictsSource);
const result = await this.dialogService.confirm({ const result = await this.dialogService.confirm({
type: 'info', type: 'info',
title: isRemote title: isRemote
@@ -1146,7 +1210,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio
await this.userDataSyncService.acceptConflict(model.uri, model.getValue()); await this.userDataSyncService.acceptConflict(model.uri, model.getValue());
} catch (e) { } catch (e) {
if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.LocalPreconditionFailed) { 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))) { 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.")); 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 { private disposeAcceptChangesWidgetRenderer(): void {
dispose(this.acceptChangesButton); dispose(this.acceptChangesButton);
this.acceptChangesButton = undefined; this.acceptChangesButton = undefined;
@@ -125,10 +125,11 @@
}`; }`;
/** /**
* @param {boolean} allowMultipleAPIAcquire
* @param {*} [state] * @param {*} [state]
* @return {string} * @return {string}
*/ */
function getVsCodeApiScript(state) { function getVsCodeApiScript(allowMultipleAPIAcquire, state) {
return ` return `
const acquireVsCodeApi = (function() { const acquireVsCodeApi = (function() {
const originalPostMessage = window.parent.postMessage.bind(window.parent); const originalPostMessage = window.parent.postMessage.bind(window.parent);
@@ -138,7 +139,7 @@
let state = ${state ? `JSON.parse(${JSON.stringify(state)})` : undefined}; let state = ${state ? `JSON.parse(${JSON.stringify(state)})` : undefined};
return () => { return () => {
if (acquired) { if (acquired && !${allowMultipleAPIAcquire}) {
throw new Error('An instance of the VS Code API has already been acquired'); throw new Error('An instance of the VS Code API has already been acquired');
} }
acquired = true; acquired = true;
@@ -325,7 +326,7 @@
if (options.allowScripts) { if (options.allowScripts) {
const defaultScript = newDocument.createElement('script'); const defaultScript = newDocument.createElement('script');
defaultScript.id = '_vscodeApiScript'; defaultScript.id = '_vscodeApiScript';
defaultScript.textContent = getVsCodeApiScript(data.state); defaultScript.textContent = getVsCodeApiScript(options.allowMultipleAPIAcquire, data.state);
newDocument.head.prepend(defaultScript); newDocument.head.prepend(defaultScript);
} }
@@ -59,6 +59,7 @@ export interface WebviewOptions {
} }
export interface WebviewContentOptions { export interface WebviewContentOptions {
readonly allowMultipleAPIAcquire?: boolean;
readonly allowScripts?: boolean; readonly allowScripts?: boolean;
readonly localResourceRoots?: ReadonlyArray<URI>; readonly localResourceRoots?: ReadonlyArray<URI>;
readonly portMapping?: ReadonlyArray<modes.IWebviewPortMapping>; readonly portMapping?: ReadonlyArray<modes.IWebviewPortMapping>;
@@ -36,6 +36,7 @@ export function areWebviewInputOptionsEqual(a: WebviewInputOptions, b: WebviewIn
return a.enableCommandUris === b.enableCommandUris return a.enableCommandUris === b.enableCommandUris
&& a.enableFindWidget === b.enableFindWidget && a.enableFindWidget === b.enableFindWidget
&& a.allowScripts === b.allowScripts && a.allowScripts === b.allowScripts
&& a.allowMultipleAPIAcquire === b.allowMultipleAPIAcquire
&& a.retainContextWhenHidden === b.retainContextWhenHidden && a.retainContextWhenHidden === b.retainContextWhenHidden
&& a.tryRestoreScrollPosition === b.tryRestoreScrollPosition && a.tryRestoreScrollPosition === b.tryRestoreScrollPosition
&& equals(a.localResourceRoots, b.localResourceRoots, isEqual) && equals(a.localResourceRoots, b.localResourceRoots, isEqual)
@@ -102,6 +102,9 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
@memoize @memoize
get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); } get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); }
@memoize
get snippetsHome(): URI { return joinPath(this.userRoamingDataHome, 'snippets'); }
@memoize @memoize
get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); } get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); }