Merge from vscode 7eaf220cafb9d9e901370ffce02229171cbf3ea6

This commit is contained in:
ADS Merger
2020-09-03 02:34:56 +00:00
committed by Anthony Dresser
parent 39d9eed585
commit a63578e6f7
519 changed files with 14338 additions and 6670 deletions

View File

@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { groupBy } from 'vs/base/common/arrays';
import { compare } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService';
import { WorkspaceEditMetadata } from 'vs/editor/common/modes';
import { IProgress } from 'vs/platform/progress/common/progress';
import { ICellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
export class ResourceNotebookCellEdit extends ResourceEdit {
constructor(
readonly resource: URI,
readonly cellEdit: ICellEditOperation,
readonly versionId?: number,
readonly metadata?: WorkspaceEditMetadata
) {
super(metadata);
}
}
export class BulkCellEdits {
constructor(
private readonly _progress: IProgress<void>,
private readonly _edits: ResourceNotebookCellEdit[],
@INotebookService private readonly _notebookService: INotebookService,
@INotebookEditorModelResolverService private readonly _notebookModelService: INotebookEditorModelResolverService,
) { }
async apply(): Promise<void> {
const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString()));
for (let group of editsByNotebook) {
const [first] = group;
const ref = await this._notebookModelService.resolve(first.resource);
// check state
// if (typeof first.versionId === 'number' && ref.object.notebook.versionId !== first.versionId) {
// ref.dispose();
// throw new Error(`Notebook '${first.resource}' has changed in the meantime`);
// }
// apply edits
const cellEdits = group.map(edit => edit.cellEdit);
this._notebookService.transformEditsOutputs(ref.object.notebook, cellEdits);
ref.object.notebook.applyEdit(ref.object.notebook.versionId, cellEdits, true);
ref.dispose();
this._progress.report(undefined);
}
}
}

View File

@@ -0,0 +1,167 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { BulkTextEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkTextEdits';
import { BulkFileEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkFileEdits';
import { BulkCellEdits, ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
class BulkEdit {
constructor(
private readonly _label: string | undefined,
private readonly _editor: ICodeEditor | undefined,
private readonly _progress: IProgress<IProgressStep>,
private readonly _edits: ResourceEdit[],
@IInstantiationService private readonly _instaService: IInstantiationService,
@ILogService private readonly _logService: ILogService,
) {
}
ariaMessage(): string {
const editCount = this._edits.length;
const resourceCount = this._edits.length;
if (editCount === 0) {
return localize('summary.0', "Made no edits");
} else if (editCount > 1 && resourceCount > 1) {
return localize('summary.nm', "Made {0} text edits in {1} files", editCount, resourceCount);
} else {
return localize('summary.n0', "Made {0} text edits in one file", editCount, resourceCount);
}
}
async perform(): Promise<void> {
if (this._edits.length === 0) {
return;
}
const ranges: number[] = [1];
for (let i = 1; i < this._edits.length; i++) {
if (Object.getPrototypeOf(this._edits[i - 1]) === Object.getPrototypeOf(this._edits[i])) {
ranges[ranges.length - 1]++;
} else {
ranges.push(1);
}
}
this._progress.report({ total: this._edits.length });
const progress: IProgress<void> = { report: _ => this._progress.report({ increment: 1 }) };
let index = 0;
for (let range of ranges) {
const group = this._edits.slice(index, index + range);
if (group[0] instanceof ResourceFileEdit) {
await this._performFileEdits(<ResourceFileEdit[]>group, progress);
} else if (group[0] instanceof ResourceTextEdit) {
await this._performTextEdits(<ResourceTextEdit[]>group, progress);
} else if (group[0] instanceof ResourceNotebookCellEdit) {
await this._performCellEdits(<ResourceNotebookCellEdit[]>group, progress);
} else {
console.log('UNKNOWN EDIT');
}
index = index + range;
}
}
private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress<void>) {
this._logService.debug('_performFileEdits', JSON.stringify(edits));
const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), progress, edits);
await model.apply();
}
private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress<void>): Promise<void> {
this._logService.debug('_performTextEdits', JSON.stringify(edits));
const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._editor, progress, edits);
await model.apply();
}
private async _performCellEdits(edits: ResourceNotebookCellEdit[], progress: IProgress<void>): Promise<void> {
this._logService.debug('_performCellEdits', JSON.stringify(edits));
const model = this._instaService.createInstance(BulkCellEdits, progress, edits);
await model.apply();
}
}
export class BulkEditService implements IBulkEditService {
declare readonly _serviceBrand: undefined;
private _previewHandler?: IBulkEditPreviewHandler;
constructor(
@IInstantiationService private readonly _instaService: IInstantiationService,
@ILogService private readonly _logService: ILogService,
@IEditorService private readonly _editorService: IEditorService,
) { }
setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable {
this._previewHandler = handler;
return toDisposable(() => {
if (this._previewHandler === handler) {
this._previewHandler = undefined;
}
});
}
hasPreviewHandler(): boolean {
return Boolean(this._previewHandler);
}
async apply(edits: ResourceEdit[], options?: IBulkEditOptions): Promise<IBulkEditResult> {
if (edits.length === 0) {
return { ariaSummary: localize('nothing', "Made no edits") };
}
if (this._previewHandler && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) {
edits = await this._previewHandler(edits, options);
}
let codeEditor = options?.editor;
// try to find code editor
if (!codeEditor) {
let candidate = this._editorService.activeTextEditorControl;
if (isCodeEditor(candidate)) {
codeEditor = candidate;
}
}
if (codeEditor && codeEditor.getOption(EditorOption.readOnly)) {
// If the code editor is readonly still allow bulk edits to be applied #68549
codeEditor = undefined;
}
const bulkEdit = this._instaService.createInstance(
BulkEdit,
options?.quotableLabel || options?.label,
codeEditor, options?.progress ?? Progress.None,
edits
);
try {
await bulkEdit.perform();
return { ariaSummary: bulkEdit.ariaMessage() };
} catch (err) {
// console.log('apply FAILED');
// console.log(err);
this._logService.error(err);
throw err;
}
}
}
registerSingleton(IBulkEditService, BulkEditService, true);

View File

@@ -0,0 +1,181 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { WorkspaceFileEditOptions } from 'vs/editor/common/modes';
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { IProgress } from 'vs/platform/progress/common/progress';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { URI } from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { VSBuffer } from 'vs/base/common/buffer';
import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService';
interface IFileOperation {
uris: URI[];
perform(): Promise<IFileOperation>;
}
class Noop implements IFileOperation {
readonly uris = [];
async perform() { return this; }
}
class RenameOperation implements IFileOperation {
constructor(
readonly newUri: URI,
readonly oldUri: URI,
readonly options: WorkspaceFileEditOptions,
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
@IFileService private readonly _fileService: IFileService,
) { }
get uris() {
return [this.newUri, this.oldUri];
}
async perform(): Promise<IFileOperation> {
// rename
if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) {
return new Noop(); // not overwriting, but ignoring, and the target file exists
}
await this._workingCopyFileService.move([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite });
return new RenameOperation(this.oldUri, this.newUri, this.options, this._workingCopyFileService, this._fileService);
}
}
class CreateOperation implements IFileOperation {
constructor(
readonly newUri: URI,
readonly options: WorkspaceFileEditOptions,
readonly contents: VSBuffer | undefined,
@IFileService private readonly _fileService: IFileService,
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
@IInstantiationService private readonly _instaService: IInstantiationService,
) { }
get uris() {
return [this.newUri];
}
async perform(): Promise<IFileOperation> {
// create file
if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) {
return new Noop(); // not overwriting, but ignoring, and the target file exists
}
await this._workingCopyFileService.create(this.newUri, this.contents, { overwrite: this.options.overwrite });
return this._instaService.createInstance(DeleteOperation, this.newUri, this.options);
}
}
class DeleteOperation implements IFileOperation {
constructor(
readonly oldUri: URI,
readonly options: WorkspaceFileEditOptions,
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
@IFileService private readonly _fileService: IFileService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IInstantiationService private readonly _instaService: IInstantiationService,
@ILogService private readonly _logService: ILogService
) { }
get uris() {
return [this.oldUri];
}
async perform(): Promise<IFileOperation> {
// delete file
if (!await this._fileService.exists(this.oldUri)) {
if (!this.options.ignoreIfNotExists) {
throw new Error(`${this.oldUri} does not exist and can not be deleted`);
}
return new Noop();
}
let contents: VSBuffer | undefined;
try {
contents = (await this._fileService.readFile(this.oldUri)).value;
} catch (err) {
this._logService.critical(err);
}
const useTrash = this._fileService.hasCapability(this.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue<boolean>('files.enableTrash');
await this._workingCopyFileService.delete([this.oldUri], { useTrash, recursive: this.options.recursive });
return this._instaService.createInstance(CreateOperation, this.oldUri, this.options, contents);
}
}
class FileUndoRedoElement implements IWorkspaceUndoRedoElement {
readonly type = UndoRedoElementType.Workspace;
readonly resources: readonly URI[];
constructor(
readonly label: string,
readonly operations: IFileOperation[]
) {
this.resources = (<URI[]>[]).concat(...operations.map(op => op.uris));
}
async undo(): Promise<void> {
await this._reverse();
}
async redo(): Promise<void> {
await this._reverse();
}
private async _reverse() {
for (let i = 0; i < this.operations.length; i++) {
const op = this.operations[i];
const undo = await op.perform();
this.operations[i] = undo;
}
}
}
export class BulkFileEdits {
constructor(
private readonly _label: string,
private readonly _progress: IProgress<void>,
private readonly _edits: ResourceFileEdit[],
@IInstantiationService private readonly _instaService: IInstantiationService,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
) { }
async apply(): Promise<void> {
const undoOperations: IFileOperation[] = [];
for (const edit of this._edits) {
this._progress.report(undefined);
const options = edit.options || {};
let op: IFileOperation | undefined;
if (edit.newResource && edit.oldResource) {
// rename
op = this._instaService.createInstance(RenameOperation, edit.newResource, edit.oldResource, options);
} else if (!edit.newResource && edit.oldResource) {
// delete file
op = this._instaService.createInstance(DeleteOperation, edit.oldResource, options);
} else if (edit.newResource && !edit.oldResource) {
// create file
op = this._instaService.createInstance(CreateOperation, edit.newResource, options, undefined);
}
if (op) {
const undoOp = await op.perform();
undoOperations.push(undoOp);
}
}
this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations));
}
}

View File

@@ -0,0 +1,244 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { mergeSort } from 'vs/base/common/arrays';
import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
import { IProgress } from 'vs/platform/progress/common/progress';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack';
import { ResourceMap } from 'vs/base/common/map';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
type ValidationResult = { canApply: true } | { canApply: false, reason: URI };
class ModelEditTask implements IDisposable {
readonly model: ITextModel;
private _expectedModelVersionId: number | undefined;
protected _edits: IIdentifiedSingleEditOperation[];
protected _newEol: EndOfLineSequence | undefined;
constructor(private readonly _modelReference: IReference<IResolvedTextEditorModel>) {
this.model = this._modelReference.object.textEditorModel;
this._edits = [];
}
dispose() {
this._modelReference.dispose();
}
addEdit(resourceEdit: ResourceTextEdit): void {
this._expectedModelVersionId = resourceEdit.versionId;
const { textEdit } = resourceEdit;
if (typeof textEdit.eol === 'number') {
// honor eol-change
this._newEol = textEdit.eol;
}
if (!textEdit.range && !textEdit.text) {
// lacks both a range and the text
return;
}
if (Range.isEmpty(textEdit.range) && !textEdit.text) {
// no-op edit (replace empty range with empty text)
return;
}
// create edit operation
let range: Range;
if (!textEdit.range) {
range = this.model.getFullModelRange();
} else {
range = Range.lift(textEdit.range);
}
this._edits.push(EditOperation.replaceMove(range, textEdit.text));
}
validate(): ValidationResult {
if (typeof this._expectedModelVersionId === 'undefined' || this.model.getVersionId() === this._expectedModelVersionId) {
return { canApply: true };
}
return { canApply: false, reason: this.model.uri };
}
getBeforeCursorState(): Selection[] | null {
return null;
}
apply(): void {
if (this._edits.length > 0) {
this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
this.model.pushEditOperations(null, this._edits, () => null);
}
if (this._newEol !== undefined) {
this.model.pushEOL(this._newEol);
}
}
}
class EditorEditTask extends ModelEditTask {
private _editor: ICodeEditor;
constructor(modelReference: IReference<IResolvedTextEditorModel>, editor: ICodeEditor) {
super(modelReference);
this._editor = editor;
}
getBeforeCursorState(): Selection[] | null {
return this._editor.getSelections();
}
apply(): void {
if (this._edits.length > 0) {
this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
this._editor.executeEdits('', this._edits);
}
if (this._newEol !== undefined) {
if (this._editor.hasModel()) {
this._editor.getModel().pushEOL(this._newEol);
}
}
}
}
export class BulkTextEdits {
private readonly _edits = new ResourceMap<ResourceTextEdit[]>();
constructor(
private readonly _label: string,
private readonly _editor: ICodeEditor | undefined,
private readonly _progress: IProgress<void>,
edits: ResourceTextEdit[],
@IEditorWorkerService private readonly _editorWorker: IEditorWorkerService,
@IModelService private readonly _modelService: IModelService,
@ITextModelService private readonly _textModelResolverService: ITextModelService,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService
) {
for (const edit of edits) {
let array = this._edits.get(edit.resource);
if (!array) {
array = [];
this._edits.set(edit.resource, array);
}
array.push(edit);
}
}
private _validateBeforePrepare(): void {
// First check if loaded models were not changed in the meantime
for (const array of this._edits.values()) {
for (let edit of array) {
if (typeof edit.versionId === 'number') {
let model = this._modelService.getModel(edit.resource);
if (model && model.getVersionId() !== edit.versionId) {
// model changed in the meantime
throw new Error(`${model.uri.toString()} has changed in the meantime`);
}
}
}
}
}
private async _createEditsTasks(): Promise<ModelEditTask[]> {
const tasks: ModelEditTask[] = [];
const promises: Promise<any>[] = [];
for (let [key, value] of this._edits) {
const promise = this._textModelResolverService.createModelReference(key).then(async ref => {
let task: ModelEditTask;
let makeMinimal = false;
if (this._editor?.getModel()?.uri.toString() === ref.object.textEditorModel.uri.toString()) {
task = new EditorEditTask(ref, this._editor);
makeMinimal = true;
} else {
task = new ModelEditTask(ref);
}
for (const edit of value) {
if (makeMinimal) {
const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.textEdit]);
if (!newEdits) {
task.addEdit(edit);
} else {
for (let moreMinialEdit of newEdits) {
task.addEdit(new ResourceTextEdit(edit.resource, moreMinialEdit, edit.versionId, edit.metadata));
}
}
} else {
task.addEdit(edit);
}
}
tasks.push(task);
});
promises.push(promise);
}
await Promise.all(promises);
return tasks;
}
private _validateTasks(tasks: ModelEditTask[]): ValidationResult {
for (const task of tasks) {
const result = task.validate();
if (!result.canApply) {
return result;
}
}
return { canApply: true };
}
async apply(): Promise<void> {
this._validateBeforePrepare();
const tasks = await this._createEditsTasks();
try {
const validation = this._validateTasks(tasks);
if (!validation.canApply) {
throw new Error(`${(validation as { canApply: false, reason: URI }).reason.toString()} has changed in the meantime`); // {{SQL CARBON EDIT}} strict-null-checks
}
if (tasks.length === 1) {
// This edit touches a single model => keep things simple
for (const task of tasks) {
task.model.pushStackElement();
task.apply();
task.model.pushStackElement();
this._progress.report(undefined);
}
} else {
// prepare multi model undo element
const multiModelEditStackElement = new MultiModelEditStackElement(
this._label,
tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState()))
);
this._undoRedoService.pushElement(multiModelEditStackElement);
for (const task of tasks) {
task.apply();
this._progress.report(undefined);
}
multiModelEditStackElement.close();
}
} finally {
dispose(tasks);
}
}
}

View File

@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ResourceMap } from 'vs/base/common/map';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { ITextModel } from 'vs/editor/common/model';
import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
export class ConflictDetector {
private readonly _conflicts = new ResourceMap<boolean>();
private readonly _disposables = new DisposableStore();
private readonly _onDidConflict = new Emitter<this>();
readonly onDidConflict: Event<this> = this._onDidConflict.event;
constructor(
edits: ResourceEdit[],
@IFileService fileService: IFileService,
@IModelService modelService: IModelService,
) {
const _workspaceEditResources = new ResourceMap<boolean>();
for (let edit of edits) {
if (edit instanceof ResourceTextEdit) {
_workspaceEditResources.set(edit.resource, true);
if (typeof edit.versionId === 'number') {
const model = modelService.getModel(edit.resource);
if (model && model.getVersionId() !== edit.versionId) {
this._conflicts.set(edit.resource, true);
this._onDidConflict.fire(this);
}
}
} else if (edit instanceof ResourceFileEdit) {
if (edit.newResource) {
_workspaceEditResources.set(edit.newResource, true);
} else if (edit.oldResource) {
_workspaceEditResources.set(edit.oldResource, true);
}
} else {
//todo@jrieken
console.log('UNKNOWN EDIT TYPE');
}
}
// listen to file changes
this._disposables.add(fileService.onDidFilesChange(e => {
for (let change of e.changes) {
if (modelService.getModel(change.resource)) {
// ignore changes for which a model exists
// because we have a better check for models
continue;
}
// conflict
if (_workspaceEditResources.has(change.resource)) {
this._conflicts.set(change.resource, true);
this._onDidConflict.fire(this);
}
}
}));
// listen to model changes...?
const onDidChangeModel = (model: ITextModel) => {
// conflict
if (_workspaceEditResources.has(model.uri)) {
this._conflicts.set(model.uri, true);
this._onDidConflict.fire(this);
}
};
for (let model of modelService.getModels()) {
this._disposables.add(model.onDidChangeContent(() => onDidChangeModel(model)));
}
}
dispose(): void {
this._disposables.dispose();
this._onDidConflict.dispose();
}
list(): URI[] {
return [...this._conflicts.keys()];
}
hasConflicts(): boolean {
return this._conflicts.size > 0;
}
}

View File

@@ -7,16 +7,15 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { WorkspaceEdit } from 'vs/editor/common/modes';
import { BulkEditPane } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPane';
import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService';
import { BulkEditPane } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane';
import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, FocusedViewContext, IViewsService } from 'vs/workbench/common/views';
import { localize } from 'vs/nls';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { BulkEditPreviewProvider } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview';
import { BulkEditPreviewProvider } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService';
@@ -105,18 +104,18 @@ class BulkEditPreviewContribution {
@IBulkEditService bulkEditService: IBulkEditService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
bulkEditService.setPreviewHandler((edit) => this._previewEdit(edit));
bulkEditService.setPreviewHandler(edits => this._previewEdit(edits));
this._ctxEnabled = BulkEditPreviewContribution.ctxEnabled.bindTo(contextKeyService);
}
private async _previewEdit(edit: WorkspaceEdit) {
private async _previewEdit(edits: ResourceEdit[]): Promise<ResourceEdit[]> {
this._ctxEnabled.set(true);
const uxState = this._activeSession?.uxState ?? new UXState(this._panelService, this._editorGroupsService);
const view = await getBulkEditPane(this._viewsService);
if (!view) {
this._ctxEnabled.set(false);
return edit;
return edits;
}
// check for active preview session and let the user decide
@@ -130,7 +129,7 @@ class BulkEditPreviewContribution {
if (choice.choice === 0) {
// this refactoring is being cancelled
return { edits: [] };
return [];
}
}
@@ -147,12 +146,7 @@ class BulkEditPreviewContribution {
// the actual work...
try {
const newEditOrUndefined = await view.setInput(edit, session.cts.token);
if (!newEditOrUndefined) {
return { edits: [] };
}
return newEditOrUndefined;
return await view.setInput(edits, session.cts.token);
} finally {
// restore UX state
@@ -366,4 +360,3 @@ Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry).registerViews
ctorDescriptor: new SyncDescriptor(BulkEditPane),
containerIcon: Codicon.lightbulb.classNames,
}], container);

View File

@@ -5,8 +5,7 @@
import 'vs/css!./bulkEdit';
import { WorkbenchAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService';
import { WorkspaceEdit } from 'vs/editor/common/modes';
import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditTree';
import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree';
import { FuzzyScore } from 'vs/base/common/filters';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { registerThemingParticipant, IColorTheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService';
@@ -14,7 +13,7 @@ import { diffInserted, diffRemoved } from 'vs/platform/theme/common/colorRegistr
import { localize } from 'vs/nls';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview';
import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview';
import { ILabelService } from 'vs/platform/label/common/label';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { URI } from 'vs/base/common/uri';
@@ -39,6 +38,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService';
const enum State {
Data = 'data',
@@ -66,7 +66,7 @@ export class BulkEditPane extends ViewPane {
private readonly _disposables = new DisposableStore();
private readonly _sessionDisposables = new DisposableStore();
private _currentResolve?: (edit?: WorkspaceEdit) => void;
private _currentResolve?: (edit?: ResourceEdit[]) => void;
private _currentInput?: BulkFileOperations;
@@ -163,7 +163,7 @@ export class BulkEditPane extends ViewPane {
this.element.dataset['state'] = state;
}
async setInput(edit: WorkspaceEdit, token: CancellationToken): Promise<WorkspaceEdit | undefined> {
async setInput(edit: ResourceEdit[], token: CancellationToken): Promise<ResourceEdit[]> {
this._setState(State.Data);
this._sessionDisposables.clear();
this._treeViewStates.clear();
@@ -307,11 +307,11 @@ export class BulkEditPane extends ViewPane {
let fileElement: FileElement;
if (e.element instanceof TextEditElement) {
fileElement = e.element.parent;
options.selection = e.element.edit.textEdit.edit.range;
options.selection = e.element.edit.textEdit.textEdit.range;
} else if (e.element instanceof FileElement) {
fileElement = e.element;
options.selection = e.element.edit.textEdits[0]?.textEdit.edit.range;
options.selection = e.element.edit.textEdits[0]?.textEdit.textEdit.range;
} else {
// invalid event

View File

@@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { WorkspaceEdit, WorkspaceTextEdit, WorkspaceFileEdit, WorkspaceEditMetadata } from 'vs/editor/common/modes';
import { WorkspaceEditMetadata } from 'vs/editor/common/modes';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { mergeSort, coalesceInPlace } from 'vs/base/common/arrays';
import { Range } from 'vs/editor/common/core/range';
@@ -17,10 +17,11 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati
import { IFileService } from 'vs/platform/files/common/files';
import { Emitter, Event } from 'vs/base/common/event';
import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
import { ConflictDetector } from 'vs/workbench/services/bulkEdit/browser/conflicts';
import { ConflictDetector } from 'vs/workbench/contrib/bulkEdit/browser/conflicts';
import { ResourceMap } from 'vs/base/common/map';
import { localize } from 'vs/nls';
import { extUri } from 'vs/base/common/resources';
import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
export class CheckedStates<T extends object> {
@@ -67,7 +68,7 @@ export class BulkTextEdit {
constructor(
readonly parent: BulkFileOperation,
readonly textEdit: WorkspaceTextEdit
readonly textEdit: ResourceTextEdit
) { }
}
@@ -82,7 +83,7 @@ export class BulkFileOperation {
type: BulkFileOperationType = 0;
textEdits: BulkTextEdit[] = [];
originalEdits = new Map<number, WorkspaceTextEdit | WorkspaceFileEdit>();
originalEdits = new Map<number, ResourceTextEdit | ResourceFileEdit>();
newUri?: URI;
constructor(
@@ -90,14 +91,14 @@ export class BulkFileOperation {
readonly parent: BulkFileOperations
) { }
addEdit(index: number, type: BulkFileOperationType, edit: WorkspaceTextEdit | WorkspaceFileEdit) {
addEdit(index: number, type: BulkFileOperationType, edit: ResourceTextEdit | ResourceFileEdit) {
this.type |= type;
this.originalEdits.set(index, edit);
if (WorkspaceTextEdit.is(edit)) {
if (edit instanceof ResourceTextEdit) {
this.textEdits.push(new BulkTextEdit(this, edit));
} else if (type === BulkFileOperationType.Rename) {
this.newUri = edit.newUri;
this.newUri = edit.newResource;
}
}
@@ -134,19 +135,19 @@ export class BulkCategory {
export class BulkFileOperations {
static async create(accessor: ServicesAccessor, bulkEdit: WorkspaceEdit): Promise<BulkFileOperations> {
static async create(accessor: ServicesAccessor, bulkEdit: ResourceEdit[]): Promise<BulkFileOperations> {
const result = accessor.get(IInstantiationService).createInstance(BulkFileOperations, bulkEdit);
return await result._init();
}
readonly checked = new CheckedStates<WorkspaceFileEdit | WorkspaceTextEdit>();
readonly checked = new CheckedStates<ResourceEdit>();
readonly fileOperations: BulkFileOperation[] = [];
readonly categories: BulkCategory[] = [];
readonly conflicts: ConflictDetector;
constructor(
private readonly _bulkEdit: WorkspaceEdit,
private readonly _bulkEdit: ResourceEdit[],
@IFileService private readonly _fileService: IFileService,
@IInstantiationService instaService: IInstantiationService,
) {
@@ -164,8 +165,8 @@ export class BulkFileOperations {
const newToOldUri = new ResourceMap<URI>();
for (let idx = 0; idx < this._bulkEdit.edits.length; idx++) {
const edit = this._bulkEdit.edits[idx];
for (let idx = 0; idx < this._bulkEdit.length; idx++) {
const edit = this._bulkEdit[idx];
let uri: URI;
let type: BulkFileOperationType;
@@ -173,39 +174,45 @@ export class BulkFileOperations {
// store inital checked state
this.checked.updateChecked(edit, !edit.metadata?.needsConfirmation);
if (WorkspaceTextEdit.is(edit)) {
if (edit instanceof ResourceTextEdit) {
type = BulkFileOperationType.TextEdit;
uri = edit.resource;
} else if (edit.newUri && edit.oldUri) {
type = BulkFileOperationType.Rename;
uri = edit.oldUri;
if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) {
// noop -> "soft" rename to something that already exists
continue;
}
// map newUri onto oldUri so that text-edit appear for
// the same file element
newToOldUri.set(edit.newUri, uri);
} else if (edit instanceof ResourceFileEdit) {
if (edit.newResource && edit.oldResource) {
type = BulkFileOperationType.Rename;
uri = edit.oldResource;
if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) {
// noop -> "soft" rename to something that already exists
continue;
}
// map newResource onto oldResource so that text-edit appear for
// the same file element
newToOldUri.set(edit.newResource, uri);
} else if (edit.oldUri) {
type = BulkFileOperationType.Delete;
uri = edit.oldUri;
if (edit.options?.ignoreIfNotExists && !await this._fileService.exists(uri)) {
// noop -> "soft" delete something that doesn't exist
continue;
}
} else if (edit.oldResource) {
type = BulkFileOperationType.Delete;
uri = edit.oldResource;
if (edit.options?.ignoreIfNotExists && !await this._fileService.exists(uri)) {
// noop -> "soft" delete something that doesn't exist
continue;
}
} else if (edit.newUri) {
type = BulkFileOperationType.Create;
uri = edit.newUri;
if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) {
// noop -> "soft" create something that already exists
} else if (edit.newResource) {
type = BulkFileOperationType.Create;
uri = edit.newResource;
if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) {
// noop -> "soft" create something that already exists
continue;
}
} else {
// invalid edit -> skip
continue;
}
} else {
// invalid edit -> skip
// unsupported edit
continue;
}
@@ -249,7 +256,7 @@ export class BulkFileOperations {
if (file.type !== BulkFileOperationType.TextEdit) {
let checked = true;
for (const edit of file.originalEdits.values()) {
if (WorkspaceFileEdit.is(edit)) {
if (edit instanceof ResourceFileEdit) {
checked = checked && this.checked.isChecked(edit);
}
}
@@ -275,14 +282,14 @@ export class BulkFileOperations {
return this;
}
getWorkspaceEdit(): WorkspaceEdit {
const result: WorkspaceEdit = { edits: [] };
getWorkspaceEdit(): ResourceEdit[] {
const result: ResourceEdit[] = [];
let allAccepted = true;
for (let i = 0; i < this._bulkEdit.edits.length; i++) {
const edit = this._bulkEdit.edits[i];
for (let i = 0; i < this._bulkEdit.length; i++) {
const edit = this._bulkEdit[i];
if (this.checked.isChecked(edit)) {
result.edits[i] = edit;
result[i] = edit;
continue;
}
allAccepted = false;
@@ -293,7 +300,7 @@ export class BulkFileOperations {
}
// not all edits have been accepted
coalesceInPlace(result.edits);
coalesceInPlace(result);
return result;
}
@@ -306,9 +313,9 @@ export class BulkFileOperations {
let ignoreAll = false;
for (const edit of file.originalEdits.values()) {
if (WorkspaceTextEdit.is(edit)) {
if (edit instanceof ResourceTextEdit) {
if (this.checked.isChecked(edit)) {
result.push(EditOperation.replaceMove(Range.lift(edit.edit.range), edit.edit.text));
result.push(EditOperation.replaceMove(Range.lift(edit.textEdit.range), edit.textEdit.text));
}
} else if (!this.checked.isChecked(edit)) {
@@ -330,7 +337,7 @@ export class BulkFileOperations {
return [];
}
getUriOfEdit(edit: WorkspaceFileEdit | WorkspaceTextEdit): URI {
getUriOfEdit(edit: ResourceEdit): URI {
for (let file of this.fileOperations) {
for (const value of file.originalEdits.values()) {
if (value === edit) {

View File

@@ -14,7 +14,7 @@ import * as dom from 'vs/base/browser/dom';
import { ITextModel } from 'vs/editor/common/model';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { TextModel } from 'vs/editor/common/model/textModel';
import { BulkFileOperations, BulkFileOperation, BulkFileOperationType, BulkTextEdit, BulkCategory } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview';
import { BulkFileOperations, BulkFileOperation, BulkFileOperationType, BulkTextEdit, BulkCategory } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview';
import { FileKind } from 'vs/platform/files/common/files';
import { localize } from 'vs/nls';
import { ILabelService } from 'vs/platform/label/common/label';
@@ -22,11 +22,11 @@ import type { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWid
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { basename } from 'vs/base/common/resources';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { WorkspaceFileEdit } from 'vs/editor/common/modes';
import { compare } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { Iterable } from 'vs/base/common/iterator';
import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService';
// --- VIEW MODEL
@@ -62,7 +62,7 @@ export class FileElement implements ICheckable {
// multiple file edits -> reflect single state
for (let edit of this.edit.originalEdits.values()) {
if (WorkspaceFileEdit.is(edit)) {
if (edit instanceof ResourceFileEdit) {
checked = checked && model.checked.isChecked(edit);
}
}
@@ -73,7 +73,7 @@ export class FileElement implements ICheckable {
for (let file of category.fileOperations) {
if (file.uri.toString() === this.edit.uri.toString()) {
for (const edit of file.originalEdits.values()) {
if (WorkspaceFileEdit.is(edit)) {
if (edit instanceof ResourceFileEdit) {
checked = checked && model.checked.isChecked(edit);
}
}
@@ -113,7 +113,7 @@ export class FileElement implements ICheckable {
for (let file of category.fileOperations) {
if (file.uri.toString() === this.edit.uri.toString()) {
for (const edit of file.originalEdits.values()) {
if (WorkspaceFileEdit.is(edit)) {
if (edit instanceof ResourceFileEdit) {
checked = checked && model.checked.isChecked(edit);
}
}
@@ -155,7 +155,7 @@ export class TextEditElement implements ICheckable {
// make sure parent is checked when this element is checked...
if (value) {
for (const edit of this.parent.edit.originalEdits.values()) {
if (WorkspaceFileEdit.is(edit)) {
if (edit instanceof ResourceFileEdit) {
(<BulkFileOperations>model).checked.updateChecked(edit, value);
}
}
@@ -219,7 +219,7 @@ export class BulkEditDataSource implements IAsyncDataSource<BulkFileOperations,
}
const result = element.edit.textEdits.map((edit, idx) => {
const range = Range.lift(edit.textEdit.edit.range);
const range = Range.lift(edit.textEdit.textEdit.range);
//prefix-math
let startTokens = textModel.getLineTokens(range.startLineNumber);
@@ -241,7 +241,7 @@ export class BulkEditDataSource implements IAsyncDataSource<BulkFileOperations,
edit,
textModel.getValueInRange(new Range(range.startLineNumber, range.startColumn - prefixLen, range.startLineNumber, range.startColumn)),
textModel.getValueInRange(range),
edit.textEdit.edit.text,
edit.textEdit.textEdit.text,
textModel.getValueInRange(new Range(range.endLineNumber, range.endColumn, range.endLineNumber, range.endColumn + suffixLen))
);
});
@@ -263,7 +263,7 @@ export class BulkEditSorter implements ITreeSorter<BulkEditElement> {
}
if (a instanceof TextEditElement && b instanceof TextEditElement) {
return Range.compareRangesUsingStarts(a.edit.textEdit.edit.range, b.edit.textEdit.edit.range);
return Range.compareRangesUsingStarts(a.edit.textEdit.textEdit.range, b.edit.textEdit.textEdit.range);
}
return 0;
@@ -336,13 +336,13 @@ export class BulkEditAccessibilityProvider implements IListAccessibilityProvider
if (element instanceof TextEditElement) {
if (element.selecting.length > 0 && element.inserting.length > 0) {
// edit: replace
return localize('aria.replace', "line {0}, replacing {1} with {2}", element.edit.textEdit.edit.range.startLineNumber, element.selecting, element.inserting);
return localize('aria.replace', "line {0}, replacing {1} with {2}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting, element.inserting);
} else if (element.selecting.length > 0 && element.inserting.length === 0) {
// edit: delete
return localize('aria.del', "line {0}, removing {1}", element.edit.textEdit.edit.range.startLineNumber, element.selecting);
return localize('aria.del', "line {0}, removing {1}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting);
} else if (element.selecting.length === 0 && element.inserting.length > 0) {
// edit: insert
return localize('aria.insert', "line {0}, inserting {1}", element.edit.textEdit.edit.range.startLineNumber, element.selecting);
return localize('aria.insert', "line {0}, inserting {1}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting);
}
}

View File

@@ -11,10 +11,10 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModelService } from 'vs/editor/common/services/modelService';
import type { WorkspaceEdit } from 'vs/editor/common/modes';
import { URI } from 'vs/base/common/uri';
import { BulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview';
import { BulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview';
import { Range } from 'vs/editor/common/core/range';
import { ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
suite('BulkEditPreview', function () {
@@ -47,28 +47,25 @@ suite('BulkEditPreview', function () {
test('one needsConfirmation unchecks all of file', async function () {
const edit: WorkspaceEdit = {
edits: [
{ newUri: URI.parse('some:///uri1'), metadata: { label: 'cat1', needsConfirmation: true } },
{ oldUri: URI.parse('some:///uri1'), newUri: URI.parse('some:///uri2'), metadata: { label: 'cat2', needsConfirmation: false } },
]
};
const edits = [
new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'cat1', needsConfirmation: true }),
new ResourceFileEdit(URI.parse('some:///uri1'), URI.parse('some:///uri2'), undefined, { label: 'cat2', needsConfirmation: false }),
];
const ops = await instaService.invokeFunction(BulkFileOperations.create, edit);
const ops = await instaService.invokeFunction(BulkFileOperations.create, edits);
assert.equal(ops.fileOperations.length, 1);
assert.equal(ops.checked.isChecked(edit.edits[0]), false);
assert.equal(ops.checked.isChecked(edits[0]), false);
});
test('has categories', async function () {
const edit: WorkspaceEdit = {
edits: [
{ newUri: URI.parse('some:///uri1'), metadata: { label: 'uri1', needsConfirmation: true } },
{ newUri: URI.parse('some:///uri2'), metadata: { label: 'uri2', needsConfirmation: false } }
]
};
const edits = [
new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'uri1', needsConfirmation: true }),
new ResourceFileEdit(undefined, URI.parse('some:///uri2'), undefined, { label: 'uri2', needsConfirmation: false }),
];
const ops = await instaService.invokeFunction(BulkFileOperations.create, edit);
const ops = await instaService.invokeFunction(BulkFileOperations.create, edits);
assert.equal(ops.categories.length, 2);
assert.equal(ops.categories[0].metadata.label, 'uri1'); // unconfirmed!
assert.equal(ops.categories[1].metadata.label, 'uri2');
@@ -76,14 +73,12 @@ suite('BulkEditPreview', function () {
test('has not categories', async function () {
const edit: WorkspaceEdit = {
edits: [
{ newUri: URI.parse('some:///uri1'), metadata: { label: 'uri1', needsConfirmation: true } },
{ newUri: URI.parse('some:///uri2'), metadata: { label: 'uri1', needsConfirmation: false } }
]
};
const edits = [
new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'uri1', needsConfirmation: true }),
new ResourceFileEdit(undefined, URI.parse('some:///uri2'), undefined, { label: 'uri1', needsConfirmation: false }),
];
const ops = await instaService.invokeFunction(BulkFileOperations.create, edit);
const ops = await instaService.invokeFunction(BulkFileOperations.create, edits);
assert.equal(ops.categories.length, 1);
assert.equal(ops.categories[0].metadata.label, 'uri1'); // unconfirmed!
assert.equal(ops.categories[0].metadata.label, 'uri1');
@@ -91,43 +86,41 @@ suite('BulkEditPreview', function () {
test('category selection', async function () {
const edit: WorkspaceEdit = {
edits: [
{ newUri: URI.parse('some:///uri1'), metadata: { label: 'C1', needsConfirmation: false } },
{ resource: URI.parse('some:///uri2'), edit: { text: 'foo', range: new Range(1, 1, 1, 1) }, metadata: { label: 'C2', needsConfirmation: false } }
]
};
const edits = [
new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'C1', needsConfirmation: false }),
new ResourceTextEdit(URI.parse('some:///uri2'), { text: 'foo', range: new Range(1, 1, 1, 1) }, undefined, { label: 'C2', needsConfirmation: false }),
];
const ops = await instaService.invokeFunction(BulkFileOperations.create, edit);
assert.equal(ops.checked.isChecked(edit.edits[0]), true);
assert.equal(ops.checked.isChecked(edit.edits[1]), true);
const ops = await instaService.invokeFunction(BulkFileOperations.create, edits);
assert.ok(edit === ops.getWorkspaceEdit());
assert.equal(ops.checked.isChecked(edits[0]), true);
assert.equal(ops.checked.isChecked(edits[1]), true);
assert.ok(edits === ops.getWorkspaceEdit());
// NOT taking to create, but the invalid text edit will
// go through
ops.checked.updateChecked(edit.edits[0], false);
const newEdit = ops.getWorkspaceEdit();
assert.ok(edit !== newEdit);
ops.checked.updateChecked(edits[0], false);
const newEdits = ops.getWorkspaceEdit();
assert.ok(edits !== newEdits);
assert.equal(edit.edits.length, 2);
assert.equal(newEdit.edits.length, 1);
assert.equal(edits.length, 2);
assert.equal(newEdits.length, 1);
});
test('fix bad metadata', async function () {
// bogous edit that wants creation to be confirmed, but not it's textedit-child...
const edit: WorkspaceEdit = {
edits: [
{ newUri: URI.parse('some:///uri1'), metadata: { label: 'C1', needsConfirmation: true } },
{ resource: URI.parse('some:///uri1'), edit: { text: 'foo', range: new Range(1, 1, 1, 1) }, metadata: { label: 'C2', needsConfirmation: false } }
]
};
const ops = await instaService.invokeFunction(BulkFileOperations.create, edit);
const edits = [
new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'C1', needsConfirmation: true }),
new ResourceTextEdit(URI.parse('some:///uri1'), { text: 'foo', range: new Range(1, 1, 1, 1) }, undefined, { label: 'C2', needsConfirmation: false })
];
assert.equal(ops.checked.isChecked(edit.edits[0]), false);
assert.equal(ops.checked.isChecked(edit.edits[1]), false);
const ops = await instaService.invokeFunction(BulkFileOperations.create, edits);
assert.equal(ops.checked.isChecked(edits[0]), false);
assert.equal(ops.checked.isChecked(edits[1]), false);
});
});

View File

@@ -10,7 +10,6 @@ import { CharCode } from 'vs/base/common/charCode';
import { Color } from 'vs/base/common/color';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable } from 'vs/base/common/lifecycle';
import { escape } from 'vs/base/common/strings';
import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { Position } from 'vs/editor/common/core/position';
@@ -31,6 +30,8 @@ import { SemanticTokenRule, TokenStyleData, TokenStyle } from 'vs/platform/theme
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { SEMANTIC_HIGHLIGHTING_SETTING_ID, IEditorSemanticHighlightingOptions } from 'vs/editor/common/services/modelServiceImpl';
const $ = dom.$;
class InspectEditorTokensController extends Disposable implements IEditorContribution {
public static readonly ID = 'editor.contrib.inspectEditorTokens';
@@ -151,23 +152,11 @@ function renderTokenText(tokenText: string): string {
let charCode = tokenText.charCodeAt(charIndex);
switch (charCode) {
case CharCode.Tab:
result += '&rarr;';
result += '\u2192'; // &rarr;
break;
case CharCode.Space:
result += '&middot;';
break;
case CharCode.LessThan:
result += '&lt;';
break;
case CharCode.GreaterThan:
result += '&gt;';
break;
case CharCode.Ampersand:
result += '&amp;';
result += '\u00B7'; // &middot;
break;
default:
@@ -246,8 +235,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget {
if (this._isDisposed) {
return;
}
let text = this._compute(grammar, semanticTokens, position);
this._domNode.innerHTML = text;
this._compute(grammar, semanticTokens, position);
this._domNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`;
this._editor.layoutContentWidget(this);
}, (err) => {
@@ -268,11 +256,12 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget {
return this._themeService.getColorTheme().semanticHighlighting;
}
private _compute(grammar: IGrammar | null, semanticTokens: SemanticTokensResult | null, position: Position): string {
private _compute(grammar: IGrammar | null, semanticTokens: SemanticTokensResult | null, position: Position) {
const textMateTokenInfo = grammar && this._getTokensAtPosition(grammar, position);
const semanticTokenInfo = semanticTokens && this._getSemanticTokenAtPosition(semanticTokens, position);
if (!textMateTokenInfo && !semanticTokenInfo) {
return 'No grammar or semantic tokens available.';
dom.reset(this._domNode, 'No grammar or semantic tokens available.');
return;
}
let tmMetadata = textMateTokenInfo?.metadata;
@@ -283,91 +272,125 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget {
const tokenText = semTokenText || tmTokenText || '';
let result = '';
result += `<h2 class="tiw-token">${tokenText}<span class="tiw-token-length">(${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'})</span></h2>`;
result += `<hr class="tiw-metadata-separator" style="clear:both"/>`;
result += `<table class="tiw-metadata-table"><tbody>`;
result += `<tr><td class="tiw-metadata-key">language</td><td class="tiw-metadata-value">${escape(tmMetadata?.languageIdentifier.language || '')}</td></tr>`;
result += `<tr><td class="tiw-metadata-key">standard token type</td><td class="tiw-metadata-value">${this._tokenTypeToString(tmMetadata?.tokenType || StandardTokenType.Other)}</td></tr>`;
result += this._formatMetadata(semMetadata, tmMetadata);
result += `</tbody></table>`;
dom.reset(this._domNode,
$('h2.tiw-token', undefined,
tokenText,
$('span.tiw-token-length', undefined, `${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'}`)));
dom.append(this._domNode, $('hr.tiw-metadata-separator', { 'style': 'clear:both' }));
dom.append(this._domNode, $('table.tiw-metadata-table', undefined,
$('tbody', undefined,
$('tr', undefined,
$('td.tiw-metadata-key', undefined, 'language'),
$('td.tiw-metadata-value', undefined, tmMetadata?.languageIdentifier.language || '')
),
$('tr', undefined,
$('td.tiw-metadata-key', undefined, 'standard token type' as string),
$('td.tiw-metadata-value', undefined, this._tokenTypeToString(tmMetadata?.tokenType || StandardTokenType.Other))
),
...this._formatMetadata(semMetadata, tmMetadata)
)
));
if (semanticTokenInfo) {
result += `<hr class="tiw-metadata-separator"/>`;
result += `<table class="tiw-metadata-table"><tbody>`;
result += `<tr><td class="tiw-metadata-key">semantic token type</td><td class="tiw-metadata-value">${semanticTokenInfo.type}</td></tr>`;
dom.append(this._domNode, $('hr.tiw-metadata-separator'));
const table = dom.append(this._domNode, $('table.tiw-metadata-table', undefined));
const tbody = dom.append(table, $('tbody', undefined,
$('tr', undefined,
$('td.tiw-metadata-key', undefined, 'semantic token type' as string),
$('td.tiw-metadata-value', undefined, semanticTokenInfo.type)
)
));
if (semanticTokenInfo.modifiers.length) {
result += `<tr><td class="tiw-metadata-key">modifiers</td><td class="tiw-metadata-value">${semanticTokenInfo.modifiers.join(' ')}</td></tr>`;
dom.append(tbody, $('tr', undefined,
$('td.tiw-metadata-key', undefined, 'modifiers'),
$('td.tiw-metadata-value', undefined, semanticTokenInfo.modifiers.join(' ')),
));
}
if (semanticTokenInfo.metadata) {
const properties: (keyof TokenStyleData)[] = ['foreground', 'bold', 'italic', 'underline'];
const propertiesByDefValue: { [rule: string]: string[] } = {};
const allDefValues = []; // remember the order
const allDefValues = new Array<[Array<HTMLElement | string>, string]>(); // remember the order
// first collect to detect when the same rule is used for multiple properties
for (let property of properties) {
if (semanticTokenInfo.metadata[property] !== undefined) {
const definition = semanticTokenInfo.definitions[property];
const defValue = this._renderTokenStyleDefinition(definition, property);
let properties = propertiesByDefValue[defValue];
const defValueStr = defValue.map(el => el instanceof HTMLElement ? el.outerHTML : el).join();
let properties = propertiesByDefValue[defValueStr];
if (!properties) {
propertiesByDefValue[defValue] = properties = [];
allDefValues.push(defValue);
propertiesByDefValue[defValueStr] = properties = [];
allDefValues.push([defValue, defValueStr]);
}
properties.push(property);
}
}
for (let defValue of allDefValues) {
result += `<tr><td class="tiw-metadata-key">${propertiesByDefValue[defValue].join(', ')}</td><td class="tiw-metadata-value">${defValue}</td></tr>`;
for (const [defValue, defValueStr] of allDefValues) {
dom.append(tbody, $('tr', undefined,
$('td.tiw-metadata-key', undefined, propertiesByDefValue[defValueStr].join(', ')),
$('td.tiw-metadata-value', undefined, ...defValue)
));
}
}
result += `</tbody></table>`;
}
if (textMateTokenInfo) {
let theme = this._themeService.getColorTheme();
result += `<hr class="tiw-metadata-separator"/>`;
result += `<table class="tiw-metadata-table"><tbody>`;
dom.append(this._domNode, $('hr.tiw-metadata-separator'));
const table = dom.append(this._domNode, $('table.tiw-metadata-table'));
const tbody = dom.append(table, $('tbody'));
if (tmTokenText && tmTokenText !== tokenText) {
result += `<tr><td class="tiw-metadata-key">textmate token</td><td class="tiw-metadata-value">${tmTokenText} (${tmTokenText.length})</td></tr>`;
dom.append(tbody, $('tr', undefined,
$('td.tiw-metadata-key', undefined, 'textmate token' as string),
$('td.tiw-metadata-value', undefined, `${tmTokenText} (${tmTokenText.length})`)
));
}
let scopes = '';
const scopes = new Array<HTMLElement | string>();
for (let i = textMateTokenInfo.token.scopes.length - 1; i >= 0; i--) {
scopes += escape(textMateTokenInfo.token.scopes[i]);
scopes.push(textMateTokenInfo.token.scopes[i]);
if (i > 0) {
scopes += '<br>';
scopes.push($('br'));
}
}
result += `<tr><td class="tiw-metadata-key">textmate scopes</td><td class="tiw-metadata-value tiw-metadata-scopes">${scopes}</td></tr>`;
dom.append(tbody, $('tr', undefined,
$('td.tiw-metadata-key', undefined, 'textmate scopes' as string),
$('td.tiw-metadata-value.tiw-metadata-scopes', undefined, ...scopes),
));
let matchingRule = findMatchingThemeRule(theme, textMateTokenInfo.token.scopes, false);
const semForeground = semanticTokenInfo?.metadata?.foreground;
if (matchingRule) {
let defValue = `<code class="tiw-theme-selector">${matchingRule.rawSelector}\n${JSON.stringify(matchingRule.settings, null, '\t')}</code>`;
if (semForeground !== textMateTokenInfo.metadata.foreground) {
let defValue = $('code.tiw-theme-selector', undefined,
matchingRule.rawSelector, $('br'), JSON.stringify(matchingRule.settings, null, '\t'));
if (semForeground) {
defValue = `<s>${defValue}</s>`;
defValue = $('s', undefined, defValue);
}
result += `<tr><td class="tiw-metadata-key">foreground</td><td class="tiw-metadata-value">${defValue}</td></tr>`;
dom.append(tbody, $('tr', undefined,
$('td.tiw-metadata-key', undefined, 'foreground'),
$('td.tiw-metadata-value', undefined, defValue),
));
}
} else if (!semForeground) {
result += `<tr><td class="tiw-metadata-key">foreground</td><td class="tiw-metadata-value">No theme selector</td></tr>`;
dom.append(tbody, $('tr', undefined,
$('td.tiw-metadata-key', undefined, 'foreground'),
$('td.tiw-metadata-value', undefined, 'No theme selector' as string),
));
}
result += `</tbody></table>`;
}
return result;
}
private _formatMetadata(semantic?: IDecodedMetadata, tm?: IDecodedMetadata) {
let result = '';
private _formatMetadata(semantic?: IDecodedMetadata, tm?: IDecodedMetadata): Array<HTMLElement | string> {
const elements = new Array<HTMLElement | string>();
function render(property: 'foreground' | 'background') {
let value = semantic?.[property] || tm?.[property];
if (value !== undefined) {
const semanticStyle = semantic?.[property] ? 'tiw-metadata-semantic' : '';
result += `<tr><td class="tiw-metadata-key">${property}</td><td class="tiw-metadata-value ${semanticStyle}">${value}</td></tr>`;
elements.push($('tr', undefined,
$('td.tiw-metadata-key', undefined, property),
$(`td.tiw-metadata-value.${semanticStyle}`, undefined, value)
));
}
return value;
}
@@ -377,17 +400,23 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget {
if (foreground && background) {
const backgroundColor = Color.fromHex(background), foregroundColor = Color.fromHex(foreground);
if (backgroundColor.isOpaque()) {
result += `<tr><td class="tiw-metadata-key">contrast ratio</td><td class="tiw-metadata-value">${backgroundColor.getContrastRatio(foregroundColor.makeOpaque(backgroundColor)).toFixed(2)}</td></tr>`;
elements.push($('tr', undefined,
$('td.tiw-metadata-key', undefined, 'contrast ratio' as string),
$('td.tiw-metadata-value', undefined, backgroundColor.getContrastRatio(foregroundColor.makeOpaque(backgroundColor)).toFixed(2))
));
} else {
result += '<tr><td class="tiw-metadata-key">Contrast ratio cannot be precise for background colors that use transparency</td><td class="tiw-metadata-value"></td></tr>';
elements.push($('tr', undefined,
$('td.tiw-metadata-key', undefined, 'Contrast ratio cannot be precise for background colors that use transparency' as string),
$('td.tiw-metadata-value')
));
}
}
let fontStyleLabels: string[] = [];
const fontStyleLabels = new Array<HTMLElement | string>();
function addStyle(key: 'bold' | 'italic' | 'underline') {
if (semantic && semantic[key]) {
fontStyleLabels.push(`<span class='tiw-metadata-semantic'>${key}</span>`);
fontStyleLabels.push($('span.tiw-metadata-semantic', undefined, key));
} else if (tm && tm[key]) {
fontStyleLabels.push(key);
}
@@ -396,9 +425,12 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget {
addStyle('italic');
addStyle('underline');
if (fontStyleLabels.length) {
result += `<tr><td class="tiw-metadata-key">font style</td><td class="tiw-metadata-value">${fontStyleLabels.join(' ')}</td></tr>`;
elements.push($('tr', undefined,
$('td.tiw-metadata-key', undefined, 'font style' as string),
$('td.tiw-metadata-value', undefined, fontStyleLabels.join(' '))
));
}
return result;
return elements;
}
private _decodeMetadata(metadata: number): IDecodedMetadata {
@@ -549,9 +581,10 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget {
return null;
}
private _renderTokenStyleDefinition(definition: TokenStyleDefinition | undefined, property: keyof TokenStyleData): string {
private _renderTokenStyleDefinition(definition: TokenStyleDefinition | undefined, property: keyof TokenStyleData): Array<HTMLElement | string> {
const elements = new Array<HTMLElement | string>();
if (definition === undefined) {
return '';
return elements;
}
const theme = this._themeService.getColorTheme() as ColorThemeData;
@@ -561,20 +594,27 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget {
const matchingRule = scopesDefinition[property];
if (matchingRule && scopesDefinition.scope) {
const strScopes = Array.isArray(matchingRule.scope) ? matchingRule.scope.join(', ') : String(matchingRule.scope);
return `${escape(scopesDefinition.scope.join(' '))}<br><code class="tiw-theme-selector">${strScopes}\n${JSON.stringify(matchingRule.settings, null, '\t')}</code>`;
elements.push(
scopesDefinition.scope.join(' '),
$('br'),
$('code.tiw-theme-selector', undefined, strScopes, $('br'), JSON.stringify(matchingRule.settings, null, '\t')));
return elements;
}
return '';
return elements;
} else if (SemanticTokenRule.is(definition)) {
const scope = theme.getTokenStylingRuleScope(definition);
if (scope === 'setting') {
return `User settings: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`;
elements.push(`User settings: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`);
return elements;
} else if (scope === 'theme') {
return `Color theme: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`;
elements.push(`Color theme: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`);
return elements;
}
return '';
return elements;
} else {
const style = theme.resolveTokenStyleValue(definition);
return `Default: ${style ? this._renderStyleProperty(style, property) : ''}`;
elements.push(`Default: ${style ? this._renderStyleProperty(style, property) : ''}`);
return elements;
}
}

View File

@@ -535,11 +535,11 @@ function fillInActions(groups: [string, Array<MenuItemAction | SubmenuItemAction
}
if (isPrimaryGroup(group)) {
const to = Array.isArray<IAction>(target) ? target : target.primary;
const to = Array.isArray(target) ? target : target.primary;
to.unshift(...actions);
} else {
const to = Array.isArray<IAction>(target) ? target : target.secondary;
const to = Array.isArray(target) ? target : target.secondary;
if (to.length > 0) {
to.push(new Separator());

View File

@@ -35,7 +35,6 @@ import { isSafari } from 'vs/base/browser/browser';
import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService';
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
import { ILabelService } from 'vs/platform/label/common/label';
import { debugAdapterRegisteredEmitter } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager';
const $ = dom.$;
@@ -169,20 +168,16 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi
) {
this.breakpointWidgetVisible = CONTEXT_BREAKPOINT_WIDGET_VISIBLE.bindTo(contextKeyService);
this.setDecorationsScheduler = new RunOnceScheduler(() => this.setDecorations(), 30);
const manager = this.debugService.getConfigurationManager();
if (manager.hasDebuggers()) {
this.registerListeners();
this.setDecorationsScheduler.schedule();
} else {
this.toDispose.push(debugAdapterRegisteredEmitter.event(() => {
this.registerListeners();
this.setDecorationsScheduler.schedule();
}));
}
this.registerListeners();
this.setDecorationsScheduler.schedule();
}
private registerListeners(): void {
this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => {
if (!this.debugService.getConfigurationManager().hasDebuggers()) {
return;
}
const data = e.target.detail as IMarginData;
const model = this.editor.getModel();
if (!e.target.position || !model || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN || data.isAfterLines || !this.marginFreeFromNonDebugDecorations(e.target.position.lineNumber)) {
@@ -257,6 +252,10 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi
* 2. When users click on line numbers, the breakpoint hint displays immediately, however it doesn't create the breakpoint unless users click on the left gutter. On a touch screen, it's hard to click on that small area.
*/
this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => {
if (!this.debugService.getConfigurationManager().hasDebuggers()) {
return;
}
let showBreakpointHintAtLineNumber = -1;
const model = this.editor.getModel();
if (model && e.target.position && (e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN || e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS) && this.debugService.getConfigurationManager().canSetBreakpointsIn(model) &&

View File

@@ -17,7 +17,7 @@ import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView'
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import {
IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA,
CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID,
CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT,
} from 'vs/workbench/contrib/debug/common/debug';
import { StartAction, AddFunctionBreakpointAction, ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions';
import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar';
@@ -34,7 +34,7 @@ import { launchSchemaId } from 'vs/workbench/services/configuration/common/confi
import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView';
import { ADD_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID, RunToCursorAction, registerEditorActions } from 'vs/workbench/contrib/debug/browser/debugEditorActions';
import { WatchExpressionsView } from 'vs/workbench/contrib/debug/browser/watchExpressionsView';
import { VariablesView } from 'vs/workbench/contrib/debug/browser/variablesView';
import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID } from 'vs/workbench/contrib/debug/browser/variablesView';
import { ClearReplAction, Repl } from 'vs/workbench/contrib/debug/browser/repl';
import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider';
import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView';
@@ -51,25 +51,20 @@ import { DebugProgressContribution } from 'vs/workbench/contrib/debug/browser/de
import { DebugTitleContribution } from 'vs/workbench/contrib/debug/browser/debugTitle';
import { Codicon } from 'vs/base/common/codicons';
import { registerColors } from 'vs/workbench/contrib/debug/browser/debugColors';
import { debugAdapterRegisteredEmitter } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager';
import { DebugEditorContribution } from 'vs/workbench/contrib/debug/browser/debugEditorContribution';
const registry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionRegistryExtensions.WorkbenchActions);
// register service
debugAdapterRegisteredEmitter.event(() => {
// Register these contributions lazily only once a debug adapter extension has been registered
registerWorkbenchContributions();
registerColors();
registerCommandsAndActions();
registerDebugMenu();
});
const debugCategory = nls.localize('debugCategory', "Debug");
const runCategroy = nls.localize('runCategory', "Run");
registerWorkbenchContributions();
registerColors();
registerCommandsAndActions();
registerDebugMenu();
registerEditorActions();
registerCommands();
registerDebugPanel();
const debugCategory = nls.localize('debugCategory', "Debug");
const runCategroy = nls.localize('runCategory', "Run");
registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy);
registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE);
registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy, CONTEXT_DEBUGGERS_AVAILABLE);
registerSingleton(IDebugService, DebugService, true);
registerDebugView();
@@ -106,18 +101,18 @@ function regsiterEditorContributions(): void {
function registerCommandsAndActions(): void {
registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE);
registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE);
registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE);
registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE);
registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE);
registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE);
registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE);
registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE);
const registerDebugCommandPaletteItem = (id: string, title: string, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => {
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
when,
when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when),
command: {
id,
title: `Debug: ${title}`,
@@ -169,8 +164,8 @@ function registerCommandsAndActions(): void {
registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 60, { id: 'codicon/debug-reverse-continue' }, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped'));
// Debug callstack context menu
const registerDebugCallstackItem = (id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => {
MenuRegistry.appendMenuItem(MenuId.DebugCallStackContext, {
const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => {
MenuRegistry.appendMenuItem(menuId, {
group,
when,
order,
@@ -181,16 +176,22 @@ function registerCommandsAndActions(): void {
}
});
};
registerDebugCallstackItem(RESTART_SESSION_ID, RESTART_LABEL, 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'));
registerDebugCallstackItem(STOP_ID, STOP_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'));
registerDebugCallstackItem(PAUSE_ID, PAUSE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('running')));
registerDebugCallstackItem(CONTINUE_ID, CONTINUE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')));
registerDebugCallstackItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'));
registerDebugCallstackItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'));
registerDebugCallstackItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'));
registerDebugCallstackItem(TERMINATE_THREAD_ID, nls.localize('terminateThread', "Terminate Thread"), 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), undefined, 'termination');
registerDebugCallstackItem(RESTART_FRAME_ID, nls.localize('restartFrame', "Restart Frame"), 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), CONTEXT_RESTART_FRAME_SUPPORTED));
registerDebugCallstackItem(COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'));
registerDebugViewMenuItem(MenuId.DebugCallStackContext, RESTART_SESSION_ID, RESTART_LABEL, 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'));
registerDebugViewMenuItem(MenuId.DebugCallStackContext, STOP_ID, STOP_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session'));
registerDebugViewMenuItem(MenuId.DebugCallStackContext, PAUSE_ID, PAUSE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('running')));
registerDebugViewMenuItem(MenuId.DebugCallStackContext, CONTINUE_ID, CONTINUE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')));
registerDebugViewMenuItem(MenuId.DebugCallStackContext, STEP_OVER_ID, STEP_OVER_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'));
registerDebugViewMenuItem(MenuId.DebugCallStackContext, STEP_INTO_ID, STEP_INTO_LABEL, 30, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'));
registerDebugViewMenuItem(MenuId.DebugCallStackContext, STEP_OUT_ID, STEP_OUT_LABEL, 40, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'));
registerDebugViewMenuItem(MenuId.DebugCallStackContext, TERMINATE_THREAD_ID, nls.localize('terminateThread', "Terminate Thread"), 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), undefined, 'termination');
registerDebugViewMenuItem(MenuId.DebugCallStackContext, RESTART_FRAME_ID, nls.localize('restartFrame', "Restart Frame"), 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), CONTEXT_RESTART_FRAME_SUPPORTED));
registerDebugViewMenuItem(MenuId.DebugCallStackContext, COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'));
registerDebugViewMenuItem(MenuId.DebugVariablesContext, SET_VARIABLE_ID, nls.localize('setValue', "Set Value"), 10, CONTEXT_SET_VARIABLE_SUPPORTED);
registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 20);
registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_EVALUATE_PATH_ID, nls.localize('copyAsExpression', "Copy as Expression"), 30, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT);
registerDebugViewMenuItem(MenuId.DebugVariablesContext, ADD_TO_WATCH_ID, nls.localize('addToWatchExpressions', "Add to Watch"), 10, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, '3_watch');
registerDebugViewMenuItem(MenuId.DebugVariablesContext, BREAK_WHEN_VALUE_CHANGES_ID, nls.localize('breakWhenValueChanges', "Break When Value Changes"), 20, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, undefined, '5_breakpoint');
// Touch Bar
if (isMacintosh) {
@@ -202,7 +203,7 @@ function registerCommandsAndActions(): void {
title,
icon: { dark: iconUri }
},
when,
when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when),
group: '9_debug',
order
});
@@ -249,7 +250,8 @@ function registerDebugMenu(): void {
id: StartAction.ID,
title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging")
},
order: 1
order: 1,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, {
@@ -258,7 +260,8 @@ function registerDebugMenu(): void {
id: RunAction.ID,
title: nls.localize({ key: 'miRun', comment: ['&& denotes a mnemonic'] }, "Run &&Without Debugging")
},
order: 2
order: 2,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, {
@@ -268,7 +271,8 @@ function registerDebugMenu(): void {
title: nls.localize({ key: 'miStopDebugging', comment: ['&& denotes a mnemonic'] }, "&&Stop Debugging"),
precondition: CONTEXT_IN_DEBUG_MODE
},
order: 3
order: 3,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, {
@@ -278,7 +282,8 @@ function registerDebugMenu(): void {
title: nls.localize({ key: 'miRestart Debugging', comment: ['&& denotes a mnemonic'] }, "&&Restart Debugging"),
precondition: CONTEXT_IN_DEBUG_MODE
},
order: 4
order: 4,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
// Configuration
@@ -288,7 +293,8 @@ function registerDebugMenu(): void {
id: ConfigureAction.ID,
title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations")
},
order: 1
order: 1,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, {
@@ -297,7 +303,8 @@ function registerDebugMenu(): void {
id: ADD_CONFIGURATION_ID,
title: nls.localize({ key: 'miAddConfiguration', comment: ['&& denotes a mnemonic'] }, "A&&dd Configuration...")
},
order: 2
order: 2,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
// Step Commands
@@ -308,7 +315,8 @@ function registerDebugMenu(): void {
title: nls.localize({ key: 'miStepOver', comment: ['&& denotes a mnemonic'] }, "Step &&Over"),
precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped')
},
order: 1
order: 1,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, {
@@ -318,7 +326,8 @@ function registerDebugMenu(): void {
title: nls.localize({ key: 'miStepInto', comment: ['&& denotes a mnemonic'] }, "Step &&Into"),
precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped')
},
order: 2
order: 2,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, {
@@ -328,7 +337,8 @@ function registerDebugMenu(): void {
title: nls.localize({ key: 'miStepOut', comment: ['&& denotes a mnemonic'] }, "Step O&&ut"),
precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped')
},
order: 3
order: 3,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, {
@@ -338,7 +348,8 @@ function registerDebugMenu(): void {
title: nls.localize({ key: 'miContinue', comment: ['&& denotes a mnemonic'] }, "&&Continue"),
precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped')
},
order: 4
order: 4,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
// New Breakpoints
@@ -348,7 +359,8 @@ function registerDebugMenu(): void {
id: TOGGLE_BREAKPOINT_ID,
title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint")
},
order: 1
order: 1,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, {
@@ -357,7 +369,8 @@ function registerDebugMenu(): void {
id: TOGGLE_CONDITIONAL_BREAKPOINT_ID,
title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...")
},
order: 1
order: 1,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, {
@@ -366,7 +379,8 @@ function registerDebugMenu(): void {
id: TOGGLE_INLINE_BREAKPOINT_ID,
title: nls.localize({ key: 'miInlineBreakpoint', comment: ['&& denotes a mnemonic'] }, "Inline Breakp&&oint")
},
order: 2
order: 2,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, {
@@ -375,7 +389,8 @@ function registerDebugMenu(): void {
id: AddFunctionBreakpointAction.ID,
title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Function Breakpoint...")
},
order: 3
order: 3,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, {
@@ -384,14 +399,16 @@ function registerDebugMenu(): void {
id: ADD_LOG_POINT_ID,
title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...")
},
order: 4
order: 4,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, {
group: '4_new_breakpoint',
title: nls.localize({ key: 'miNewBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&New Breakpoint"),
submenu: MenuId.MenubarNewBreakpointMenu,
order: 2
order: 2,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
// Modify Breakpoints
@@ -401,7 +418,8 @@ function registerDebugMenu(): void {
id: EnableAllBreakpointsAction.ID,
title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "&&Enable All Breakpoints")
},
order: 1
order: 1,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, {
@@ -410,7 +428,8 @@ function registerDebugMenu(): void {
id: DisableAllBreakpointsAction.ID,
title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints")
},
order: 2
order: 2,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, {
@@ -419,7 +438,8 @@ function registerDebugMenu(): void {
id: RemoveAllBreakpointsAction.ID,
title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints")
},
order: 3
order: 3,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
// Install Debuggers
@@ -453,10 +473,11 @@ function registerDebugPanel(): void {
containerIcon: Codicon.debugConsole.classNames,
canToggleVisibility: false,
canMoveView: true,
when: CONTEXT_DEBUGGERS_AVAILABLE,
ctorDescriptor: new SyncDescriptor(Repl),
}], VIEW_CONTAINER);
registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }), 'View: Debug Console', nls.localize('view', "View"));
registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }), 'View: Debug Console', nls.localize('view', "View"), CONTEXT_DEBUGGERS_AVAILABLE);
}
function registerDebugView(): void {

View File

@@ -21,7 +21,7 @@ import { IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation } from 'vs/workbench/contrib/debug/common/debug';
import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug';
import { Debugger } from 'vs/workbench/contrib/debug/common/debugger';
import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
@@ -49,8 +49,6 @@ const DEBUG_SELECTED_ROOT = 'debug.selectedroot';
interface IDynamicPickItem { label: string, launch: ILaunch, config: IConfig }
export const debugAdapterRegisteredEmitter = new Emitter<void>();
export class ConfigurationManager implements IConfigurationManager {
private debuggers: Debugger[];
private breakpointModeIdsSet = new Set<string>();
@@ -64,6 +62,7 @@ export class ConfigurationManager implements IConfigurationManager {
private adapterDescriptorFactories: IDebugAdapterDescriptorFactory[];
private debugAdapterFactories = new Map<string, IDebugAdapterFactory>();
private debugConfigurationTypeContext: IContextKey<string>;
private debuggersAvailable: IContextKey<boolean>;
private readonly _onDidRegisterDebugger = new Emitter<void>();
constructor(
@@ -87,6 +86,7 @@ export class ConfigurationManager implements IConfigurationManager {
const previousSelectedRoot = this.storageService.get(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE);
const previousSelectedLaunch = this.launches.find(l => l.uri.toString() === previousSelectedRoot);
this.debugConfigurationTypeContext = CONTEXT_DEBUG_CONFIGURATION_TYPE.bindTo(contextKeyService);
this.debuggersAvailable = CONTEXT_DEBUGGERS_AVAILABLE.bindTo(contextKeyService);
if (previousSelectedLaunch && previousSelectedLaunch.getConfigurationNames().length) {
this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE));
} else if (this.launches.length > 0) {
@@ -97,11 +97,9 @@ export class ConfigurationManager implements IConfigurationManager {
// debuggers
registerDebugAdapterFactory(debugTypes: string[], debugAdapterLauncher: IDebugAdapterFactory): IDisposable {
const firstTimeRegistration = debugTypes.length && this.debugAdapterFactories.size === 0;
debugTypes.forEach(debugType => this.debugAdapterFactories.set(debugType, debugAdapterLauncher));
if (firstTimeRegistration) {
debugAdapterRegisteredEmitter.fire();
}
this.debuggersAvailable.set(this.debugAdapterFactories.size > 0);
this._onDidRegisterDebugger.fire();
return {
dispose: () => {
@@ -428,7 +426,6 @@ export class ConfigurationManager implements IConfigurationManager {
});
this.setCompoundSchemaValues();
this._onDidRegisterDebugger.fire();
});
breakpointsExtPoint.setHandler((extensions, delta) => {

View File

@@ -9,7 +9,7 @@ import { Range } from 'vs/editor/common/core/range';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug';
import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView';
@@ -27,7 +27,7 @@ class ToggleBreakpointAction extends EditorAction {
id: TOGGLE_BREAKPOINT_ID,
label: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"),
alias: 'Debug: Toggle Breakpoint',
precondition: undefined,
precondition: CONTEXT_DEBUGGERS_AVAILABLE,
kbOpts: {
kbExpr: EditorContextKeys.editorTextFocus,
primary: KeyCode.F9,
@@ -66,7 +66,7 @@ class ConditionalBreakpointAction extends EditorAction {
id: TOGGLE_CONDITIONAL_BREAKPOINT_ID,
label: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."),
alias: 'Debug: Add Conditional Breakpoint...',
precondition: undefined
precondition: CONTEXT_DEBUGGERS_AVAILABLE
});
}
@@ -88,7 +88,7 @@ class LogPointAction extends EditorAction {
id: ADD_LOG_POINT_ID,
label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."),
alias: 'Debug: Add Logpoint...',
precondition: undefined
precondition: CONTEXT_DEBUGGERS_AVAILABLE
});
}
@@ -339,7 +339,7 @@ class GoToNextBreakpointAction extends GoToBreakpointAction {
id: 'editor.debug.action.goToNextBreakpoint',
label: nls.localize('goToNextBreakpoint', "Debug: Go To Next Breakpoint"),
alias: 'Debug: Go To Next Breakpoint',
precondition: undefined
precondition: CONTEXT_DEBUGGERS_AVAILABLE
});
}
}
@@ -350,7 +350,7 @@ class GoToPreviousBreakpointAction extends GoToBreakpointAction {
id: 'editor.debug.action.goToPreviousBreakpoint',
label: nls.localize('goToPreviousBreakpoint', "Debug: Go To Previous Breakpoint"),
alias: 'Debug: Go To Previous Breakpoint',
precondition: undefined
precondition: CONTEXT_DEBUGGERS_AVAILABLE
});
}
}

View File

@@ -111,7 +111,7 @@ export class DebugService implements IDebugService {
this.debugState = CONTEXT_DEBUG_STATE.bindTo(contextKeyService);
this.inDebugMode = CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService);
this.debugUx = CONTEXT_DEBUG_UX.bindTo(contextKeyService);
this.debugUx.set(!!this.configurationManager.selectedConfiguration.name ? 'default' : 'simple');
this.debugUx.set((this.configurationManager.hasDebuggers() && !!this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple');
this.breakpointsExist = CONTEXT_BREAKPOINTS_EXIST.bindTo(contextKeyService);
});
@@ -160,8 +160,8 @@ export class DebugService implements IDebugService {
this.toDispose.push(this.viewModel.onDidFocusSession(() => {
this.onStateChange();
}));
this.toDispose.push(this.configurationManager.onDidSelectConfiguration(() => {
this.debugUx.set(!!(this.state !== State.Inactive || this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple');
this.toDispose.push(Event.any(this.configurationManager.onDidRegisterDebugger, this.configurationManager.onDidSelectConfiguration)(() => {
this.debugUx.set(!!(this.state !== State.Inactive || (this.configurationManager.selectedConfiguration.name && this.configurationManager.hasDebuggers())) ? 'default' : 'simple');
}));
this.toDispose.push(this.model.onDidChangeCallStack(() => {
const numberOfSessions = this.model.getSessions().filter(s => !s.parentSession).length;
@@ -243,7 +243,7 @@ export class DebugService implements IDebugService {
this.debugState.set(getStateLabel(state));
this.inDebugMode.set(state !== State.Inactive);
// Only show the simple ux if debug is not yet started and if no launch.json exists
this.debugUx.set(((state !== State.Inactive && state !== State.Initializing) || this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple');
this.debugUx.set(((state !== State.Inactive && state !== State.Initializing) || (this.configurationManager.hasDebuggers() && this.configurationManager.selectedConfiguration.name)) ? 'default' : 'simple');
});
this.previousState = state;
this._onDidChangeState.fire(state);

View File

@@ -264,7 +264,7 @@ export class DebugSession implements IDebugSession {
*/
async launchOrAttach(config: IConfig): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'launch or attach'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'launch or attach'));
}
if (this.parentSession && this.parentSession.state === State.Inactive) {
throw canceled();
@@ -327,7 +327,7 @@ export class DebugSession implements IDebugSession {
*/
async restart(): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'restart'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'restart'));
}
this.cancelAllRequests();
@@ -336,7 +336,7 @@ export class DebugSession implements IDebugSession {
async sendBreakpoints(modelUri: URI, breakpointsToSend: IBreakpoint[], sourceModified: boolean): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'breakpoints'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'breakpoints'));
}
if (!this.raw.readyForBreakpoints) {
@@ -370,7 +370,7 @@ export class DebugSession implements IDebugSession {
async sendFunctionBreakpoints(fbpts: IFunctionBreakpoint[]): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'function breakpoints'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'function breakpoints'));
}
if (this.raw.readyForBreakpoints) {
@@ -387,7 +387,7 @@ export class DebugSession implements IDebugSession {
async sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'exception breakpoints'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'exception breakpoints'));
}
if (this.raw.readyForBreakpoints) {
@@ -397,7 +397,7 @@ export class DebugSession implements IDebugSession {
async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean } | undefined> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'data breakpoints info'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info'));
}
if (!this.raw.readyForBreakpoints) {
throw new Error(localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints"));
@@ -409,7 +409,7 @@ export class DebugSession implements IDebugSession {
async sendDataBreakpoints(dataBreakpoints: IDataBreakpoint[]): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'data breakpoints'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints'));
}
if (this.raw.readyForBreakpoints) {
@@ -426,7 +426,7 @@ export class DebugSession implements IDebugSession {
async breakpointsLocations(uri: URI, lineNumber: number): Promise<IPosition[]> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'breakpoints locations'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'breakpoints locations'));
}
const source = this.getRawSource(uri);
@@ -446,7 +446,7 @@ export class DebugSession implements IDebugSession {
customRequest(request: string, args: any): Promise<DebugProtocol.Response> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", request));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", request));
}
return this.raw.custom(request, args);
@@ -454,7 +454,7 @@ export class DebugSession implements IDebugSession {
stackTrace(threadId: number, startFrame: number, levels: number, token: CancellationToken): Promise<DebugProtocol.StackTraceResponse> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stackTrace'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stackTrace'));
}
const sessionToken = this.getNewCancellationToken(threadId, token);
@@ -463,7 +463,7 @@ export class DebugSession implements IDebugSession {
async exceptionInfo(threadId: number): Promise<IExceptionInfo | undefined> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'exceptionInfo'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'exceptionInfo'));
}
const response = await this.raw.exceptionInfo({ threadId });
@@ -481,7 +481,7 @@ export class DebugSession implements IDebugSession {
scopes(frameId: number, threadId: number): Promise<DebugProtocol.ScopesResponse> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'scopes'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'scopes'));
}
const token = this.getNewCancellationToken(threadId);
@@ -490,7 +490,7 @@ export class DebugSession implements IDebugSession {
variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise<DebugProtocol.VariablesResponse> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'variables'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'variables'));
}
const token = threadId ? this.getNewCancellationToken(threadId) : undefined;
@@ -499,7 +499,7 @@ export class DebugSession implements IDebugSession {
evaluate(expression: string, frameId: number, context?: string): Promise<DebugProtocol.EvaluateResponse> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'evaluate'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'evaluate'));
}
return this.raw.evaluate({ expression, frameId, context });
@@ -507,7 +507,7 @@ export class DebugSession implements IDebugSession {
async restartFrame(frameId: number, threadId: number): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'restartFrame'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'restartFrame'));
}
await this.raw.restartFrame({ frameId }, threadId);
@@ -515,7 +515,7 @@ export class DebugSession implements IDebugSession {
async next(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'next'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'next'));
}
await this.raw.next({ threadId });
@@ -523,7 +523,7 @@ export class DebugSession implements IDebugSession {
async stepIn(threadId: number, targetId?: number): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepIn'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepIn'));
}
await this.raw.stepIn({ threadId, targetId });
@@ -531,7 +531,7 @@ export class DebugSession implements IDebugSession {
async stepOut(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepOut'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepOut'));
}
await this.raw.stepOut({ threadId });
@@ -539,7 +539,7 @@ export class DebugSession implements IDebugSession {
async stepBack(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepBack'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepBack'));
}
await this.raw.stepBack({ threadId });
@@ -547,7 +547,7 @@ export class DebugSession implements IDebugSession {
async continue(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'continue'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'continue'));
}
await this.raw.continue({ threadId });
@@ -555,7 +555,7 @@ export class DebugSession implements IDebugSession {
async reverseContinue(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'reverse continue'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'reverse continue'));
}
await this.raw.reverseContinue({ threadId });
@@ -563,7 +563,7 @@ export class DebugSession implements IDebugSession {
async pause(threadId: number): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'pause'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'pause'));
}
await this.raw.pause({ threadId });
@@ -571,7 +571,7 @@ export class DebugSession implements IDebugSession {
async terminateThreads(threadIds?: number[]): Promise<void> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'terminateThreads'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'terminateThreads'));
}
await this.raw.terminateThreads({ threadIds });
@@ -579,7 +579,7 @@ export class DebugSession implements IDebugSession {
setVariable(variablesReference: number, name: string, value: string): Promise<DebugProtocol.SetVariableResponse> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'setVariable'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'setVariable'));
}
return this.raw.setVariable({ variablesReference, name, value });
@@ -587,7 +587,7 @@ export class DebugSession implements IDebugSession {
gotoTargets(source: DebugProtocol.Source, line: number, column?: number): Promise<DebugProtocol.GotoTargetsResponse> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'gotoTargets'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'gotoTargets'));
}
return this.raw.gotoTargets({ source, line, column });
@@ -595,7 +595,7 @@ export class DebugSession implements IDebugSession {
goto(threadId: number, targetId: number): Promise<DebugProtocol.GotoResponse> {
if (!this.raw) {
throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'goto'));
throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'goto'));
}
return this.raw.goto({ threadId, targetId });
@@ -603,7 +603,7 @@ export class DebugSession implements IDebugSession {
loadSource(resource: URI): Promise<DebugProtocol.SourceResponse> {
if (!this.raw) {
return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'loadSource')));
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'loadSource')));
}
const source = this.getSourceForUri(resource);
@@ -621,7 +621,7 @@ export class DebugSession implements IDebugSession {
async getLoadedSources(): Promise<Source[]> {
if (!this.raw) {
return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'getLoadedSources')));
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'getLoadedSources')));
}
const response = await this.raw.loadedSources({});
@@ -634,7 +634,7 @@ export class DebugSession implements IDebugSession {
async completions(frameId: number | undefined, threadId: number, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise<DebugProtocol.CompletionsResponse> {
if (!this.raw) {
return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'completions')));
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'completions')));
}
const sessionCancelationToken = this.getNewCancellationToken(threadId, token);
@@ -648,7 +648,7 @@ export class DebugSession implements IDebugSession {
async stepInTargets(frameId: number): Promise<{ id: number, label: string }[]> {
if (!this.raw) {
return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepInTargets')));
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepInTargets')));
}
const response = await this.raw.stepInTargets({ frameId });
@@ -657,7 +657,7 @@ export class DebugSession implements IDebugSession {
async cancel(progressId: string): Promise<DebugProtocol.CancelResponse> {
if (!this.raw) {
return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'cancel')));
return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'cancel')));
}
return this.raw.cancel({ progressId });
@@ -813,7 +813,7 @@ export class DebugSession implements IDebugSession {
}
if (this.configurationService.getValue<IDebugConfiguration>('debug').focusWindowOnBreak) {
this.hostService.focus();
this.hostService.focus({ force: true /* Application may not be active */ });
}
}
}

View File

@@ -21,7 +21,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerThemingParticipant, IThemeService, Themable } from 'vs/platform/theme/common/themeService';
import { registerColor, contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { localize } from 'vs/nls';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@@ -58,7 +57,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution {
@IThemeService themeService: IThemeService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IMenuService menuService: IMenuService,
@IContextMenuService contextMenuService: IContextMenuService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(themeService);

View File

@@ -88,7 +88,7 @@
.monaco-workbench .repl .repl-tree .output.expression .code-italic { font-style: italic; }
.monaco-workbench .repl .repl-tree .output.expression .code-underline { text-decoration: underline; }
.monaco-action-bar .action-item.panel-action-tree-filter-container {
.monaco-action-bar .action-item.repl-panel-filter-container {
cursor: default;
display: flex;
}
@@ -114,13 +114,7 @@
height: 25px;
}
.panel > .title .monaco-action-bar .action-item.panel-action-tree-filter-container {
max-width: 400px;
min-width: 300px;
.panel > .title .monaco-action-bar .action-item.repl-panel-filter-container {
min-width: 200px;
margin-right: 10px;
}
.monaco-action-bar .action-item.panel-action-tree-filter-container,
.panel > .title .monaco-action-bar .action-item.panel-action-tree-filter-container.grow {
flex: 1;
}

View File

@@ -626,7 +626,7 @@ export class RawDebugSession implements IDisposable {
// We are in shutdown silently complete
completeDispatch();
} else {
errorDispatch(new Error(nls.localize('noDebugAdapter', "No debug adapter found. Can not send '{0}'.", command)));
errorDispatch(new Error(nls.localize('noDebugAdapter', "No debugger available found. Can not send '{0}'.", command)));
}
return;
}

View File

@@ -59,7 +59,7 @@ import { ReplGroup } from 'vs/workbench/contrib/debug/common/replModel';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/editorOptions';
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor';
import { ReplFilter, TreeFilterState, TreeFilterPanelActionViewItem } from 'vs/workbench/contrib/debug/browser/replFilter';
import { ReplFilter, ReplFilterState, ReplFilterActionViewItem } from 'vs/workbench/contrib/debug/browser/replFilter';
const $ = dom.$;
@@ -95,7 +95,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
private completionItemProvider: IDisposable | undefined;
private modelChangeListener: IDisposable = Disposable.None;
private filter: ReplFilter;
private filterState: TreeFilterState;
private filterState: ReplFilterState;
private filterActionViewItem: ReplFilterActionViewItem | undefined;
constructor(
options: IViewPaneOptions,
@@ -120,11 +121,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50);
this.filter = new ReplFilter();
this.filterState = this._register(new TreeFilterState({
filterText: '',
filterHistory: [],
layout: new dom.Dimension(0, 0),
}));
this.filterState = new ReplFilterState();
codeEditorService.registerDecorationType(DECORATION_KEY, {});
this.registerListeners();
@@ -248,13 +245,10 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this.setMode();
}));
this._register(this.filterState.onDidChange((e) => {
if (e.filterText) {
this.filter.filterQuery = this.filterState.filterText;
if (this.tree) {
this.tree.refilter();
}
}
this._register(this.filterState.onDidChange(() => {
this.filter.filterQuery = this.filterState.filterText;
this.tree.refilter();
revealLastElement(this.tree);
}));
}
@@ -276,8 +270,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this.navigateHistory(false);
}
focusRepl(): void {
this.tree.domFocus();
focusFilter(): void {
this.filterActionViewItem?.focus();
}
private setMode(): void {
@@ -447,7 +441,6 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
this.replInputContainer.style.height = `${replInputHeight}px`;
this.replInput.layout({ width: width - 30, height: replInputHeight });
this.filterState.layout = new dom.Dimension(width, height);
}
focus(): void {
@@ -458,7 +451,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget {
if (action.id === SelectReplAction.ID) {
return this.instantiationService.createInstance(SelectReplActionViewItem, this.selectReplAction);
} else if (action.id === FILTER_ACTION_ID) {
return this.instantiationService.createInstance(TreeFilterPanelActionViewItem, action, localize('workbench.debug.filter.placeholder', "Filter. E.g.: text, !exclude"), this.filterState);
this.filterActionViewItem = this.instantiationService.createInstance(ReplFilterActionViewItem, action, localize('workbench.debug.filter.placeholder', "Filter (e.g. text, !exclude)"), this.filterState);
return this.filterActionViewItem;
}
return super.getActionViewItem(action);
@@ -764,7 +758,7 @@ class FilterReplAction extends EditorAction {
run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {
SuggestController.get(editor).acceptSelectedSuggestion(false, true);
const repl = getReplView(accessor.get(IViewsService));
repl?.focusRepl();
repl?.focusFilter();
}
}

View File

@@ -15,7 +15,7 @@ import { IAction } from 'vs/base/common/actions';
import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { toDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
@@ -51,10 +51,6 @@ export class ReplFilter implements ITreeFilter<IReplElement> {
}
filter(element: IReplElement, parentVisibility: TreeVisibility): TreeFilterResult<void> {
if (this._parsedQueries.length === 0) {
return parentVisibility;
}
let includeQueryPresent = false;
let includeQueryMatched = false;
@@ -72,68 +68,41 @@ export class ReplFilter implements ITreeFilter<IReplElement> {
}
}
return includeQueryPresent ? includeQueryMatched : parentVisibility;
return includeQueryPresent ? includeQueryMatched : (typeof parentVisibility !== 'undefined' ? parentVisibility : TreeVisibility.Visible);
}
}
export interface IReplFiltersChangeEvent {
filterText?: boolean;
layout?: boolean;
}
export class ReplFilterState {
export interface IReplFiltersOptions {
filterText: string;
filterHistory: string[];
layout: DOM.Dimension;
}
export class TreeFilterState extends Disposable {
private readonly _onDidChange: Emitter<IReplFiltersChangeEvent> = this._register(new Emitter<IReplFiltersChangeEvent>());
readonly onDidChange: Event<IReplFiltersChangeEvent> = this._onDidChange.event;
constructor(options: IReplFiltersOptions) {
super();
this._filterText = options.filterText;
this.filterHistory = options.filterHistory;
this._layout = options.layout;
private readonly _onDidChange: Emitter<void> = new Emitter<void>();
get onDidChange(): Event<void> {
return this._onDidChange.event;
}
private _filterText: string;
private _filterText = '';
get filterText(): string {
return this._filterText;
}
set filterText(filterText: string) {
if (this._filterText !== filterText) {
this._filterText = filterText;
this._onDidChange.fire({ filterText: true });
}
}
filterHistory: string[];
private _layout: DOM.Dimension = new DOM.Dimension(0, 0);
get layout(): DOM.Dimension {
return this._layout;
}
set layout(layout: DOM.Dimension) {
if (this._layout.width !== layout.width || this._layout.height !== layout.height) {
this._layout = layout;
this._onDidChange.fire(<IReplFiltersChangeEvent>{ layout: true });
this._onDidChange.fire();
}
}
}
export class TreeFilterPanelActionViewItem extends BaseActionViewItem {
export class ReplFilterActionViewItem extends BaseActionViewItem {
private delayedFilterUpdate: Delayer<void>;
private container: HTMLElement | undefined;
private filterInputBox: HistoryInputBox | undefined;
private container!: HTMLElement;
private filterInputBox!: HistoryInputBox;
constructor(
action: IAction,
private placeholder: string,
private filters: TreeFilterState,
private filters: ReplFilterState,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService private readonly themeService: IThemeService,
@IContextViewService private readonly contextViewService: IContextViewService) {
@@ -144,40 +113,33 @@ export class TreeFilterPanelActionViewItem extends BaseActionViewItem {
render(container: HTMLElement): void {
this.container = container;
DOM.addClass(this.container, 'panel-action-tree-filter-container');
DOM.addClass(this.container, 'repl-panel-filter-container');
this.element = DOM.append(this.container, DOM.$(''));
this.element.className = this.class;
this.createInput(this.element);
this.updateClass();
this.adjustInputBox();
}
focus(): void {
if (this.filterInputBox) {
this.filterInputBox.focus();
}
this.filterInputBox.focus();
}
private clearFilterText(): void {
if (this.filterInputBox) {
this.filterInputBox.value = '';
}
this.filterInputBox.value = '';
}
private createInput(container: HTMLElement): void {
this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, {
placeholder: this.placeholder,
history: this.filters.filterHistory
history: []
}));
this._register(attachInputBoxStyler(this.filterInputBox, this.themeService));
this.filterInputBox.value = this.filters.filterText;
this._register(this.filterInputBox.onDidChange(() => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!))));
this._register(this.filters.onDidChange((event: IReplFiltersChangeEvent) => {
if (event.filterText) {
this.filterInputBox!.value = this.filters.filterText;
}
this._register(this.filters.onDidChange(() => {
this.filterInputBox.value = this.filters.filterText;
}));
this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e)));
this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, this.handleKeyboardEvent));
@@ -186,19 +148,11 @@ export class TreeFilterPanelActionViewItem extends BaseActionViewItem {
e.stopPropagation();
e.preventDefault();
}));
this._register(this.filters.onDidChange(e => this.onDidFiltersChange(e)));
}
private onDidFiltersChange(e: IReplFiltersChangeEvent): void {
if (e.layout) {
this.updateClass();
}
}
private onDidInputChange(inputbox: HistoryInputBox) {
inputbox.addToHistory();
this.filters.filterText = inputbox.value;
this.filters.filterHistory = inputbox.getHistory();
}
// Action toolbar is swallowing some keys for action items which should not be for an input box
@@ -220,27 +174,7 @@ export class TreeFilterPanelActionViewItem extends BaseActionViewItem {
}
}
private adjustInputBox(): void {
if (this.element && this.filterInputBox) {
this.filterInputBox.inputElement.style.paddingRight = DOM.hasClass(this.element, 'small') ? '25px' : '150px';
}
}
protected updateClass(): void {
if (this.element && this.container) {
this.element.className = this.class;
DOM.toggleClass(this.container, 'grow', DOM.hasClass(this.element, 'grow'));
this.adjustInputBox();
}
}
protected get class(): string {
if (this.filters.layout.width > 800) {
return 'panel-action-tree-filter grow';
} else if (this.filters.layout.width < 600) {
return 'panel-action-tree-filter small';
} else {
return 'panel-action-tree-filter';
}
return 'panel-action-tree-filter';
}
}

View File

@@ -8,37 +8,47 @@ import { RunOnceScheduler } from 'vs/base/common/async';
import * as dom from 'vs/base/browser/dom';
import { CollapseAction } from 'vs/workbench/browser/viewlet';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame } from 'vs/workbench/contrib/debug/common/debug';
import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT } from 'vs/workbench/contrib/debug/common/debug';
import { Variable, Scope, ErrorScope, StackFrame } from 'vs/workbench/contrib/debug/common/debugModel';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView';
import { IAction, Action, Separator } from 'vs/base/common/actions';
import { IAction } from 'vs/base/common/actions';
import { CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ITreeRenderer, ITreeNode, ITreeMouseEvent, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Emitter } from 'vs/base/common/event';
import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree';
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { dispose } from 'vs/base/common/lifecycle';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { withUndefinedAsNull } from 'vs/base/common/types';
import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions';
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
const $ = dom.$;
let forgetScopes = true;
export const variableSetEmitter = new Emitter<void>();
let variableInternalContext: Variable | undefined;
let dataBreakpointInfoResponse: IDataBreakpointInfoResponse | undefined;
interface IVariablesContext {
container: DebugProtocol.Variable | DebugProtocol.Scope;
variable: DebugProtocol.Variable;
}
export class VariablesView extends ViewPane {
@@ -47,6 +57,10 @@ export class VariablesView extends ViewPane {
private tree!: WorkbenchAsyncDataTree<IStackFrame | null, IExpression | IScope, FuzzyScore>;
private savedViewState = new Map<string, IAsyncDataTreeViewState>();
private autoExpandedScopes = new Set<string>();
private menu: IMenu;
private debugProtocolVariableMenuContext: IContextKey<string>;
private breakWhenValueChangesSupported: IContextKey<boolean>;
private variableEvaluateName: IContextKey<boolean>;
constructor(
options: IViewletViewOptions,
@@ -56,14 +70,20 @@ export class VariablesView extends ViewPane {
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IClipboardService private readonly clipboardService: IClipboardService,
@IContextKeyService contextKeyService: IContextKeyService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@ITelemetryService telemetryService: ITelemetryService,
@IMenuService menuService: IMenuService
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this.menu = menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService);
this._register(this.menu);
this.debugProtocolVariableMenuContext = CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT.bindTo(contextKeyService);
this.breakWhenValueChangesSupported = CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.bindTo(contextKeyService);
this.variableEvaluateName = CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT.bindTo(contextKeyService);
// Use scheduler to prevent unnecessary flashing
this.onFocusStackFrameScheduler = new RunOnceScheduler(async () => {
const stackFrame = this.debugService.getViewModel().focusedStackFrame;
@@ -183,41 +203,27 @@ export class VariablesView extends ViewPane {
private async onContextMenu(e: ITreeContextMenuEvent<IExpression | IScope>): Promise<void> {
const variable = e.element;
if (variable instanceof Variable && !!variable.value) {
const actions: IAction[] = [];
this.debugProtocolVariableMenuContext.set(variable.variableMenuContext || '');
variableInternalContext = variable;
const session = this.debugService.getViewModel().focusedSession;
if (session && session.capabilities.supportsSetVariable) {
actions.push(new Action('workbench.setValue', nls.localize('setValue', "Set Value"), undefined, true, () => {
this.debugService.getViewModel().setSelectedExpression(variable);
return Promise.resolve();
}));
}
actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variable, 'variables'));
if (variable.evaluateName) {
actions.push(new Action('debug.copyEvaluatePath', nls.localize('copyAsExpression', "Copy as Expression"), undefined, true, () => {
return this.clipboardService.writeText(variable.evaluateName!);
}));
actions.push(new Separator());
actions.push(new Action('debug.addToWatchExpressions', nls.localize('addToWatchExpressions', "Add to Watch"), undefined, true, () => {
this.debugService.addWatchExpression(variable.evaluateName);
return Promise.resolve(undefined);
}));
}
this.variableEvaluateName.set(!!variable.evaluateName);
this.breakWhenValueChangesSupported.reset();
if (session && session.capabilities.supportsDataBreakpoints) {
const response = await session.dataBreakpointInfo(variable.name, variable.parent.reference);
const dataid = response?.dataId;
if (response && dataid) {
actions.push(new Separator());
actions.push(new Action('debug.breakWhenValueChanges', nls.localize('breakWhenValueChanges', "Break When Value Changes"), undefined, true, () => {
return this.debugService.addDataBreakpoint(response.description, dataid, !!response.canPersist, response.accessTypes);
}));
}
const dataBreakpointId = response?.dataId;
this.breakWhenValueChangesSupported.set(!!dataBreakpointId);
}
const context: IVariablesContext = {
container: (variable.parent as (Variable | Scope)).toDebugProtocolObject(),
variable: variable.toDebugProtocolObject()
};
const actions: IAction[] = [];
const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: context, shouldForwardArgs: false }, actions, this.contextMenuService);
this.contextMenuService.showContextMenu({
getAnchor: () => e.anchor,
getActions: () => actions,
getActionsContext: () => variable,
onHide: () => dispose(actions)
onHide: () => dispose(actionsDisposable)
});
}
}
@@ -377,3 +383,54 @@ class VariablesAccessibilityProvider implements IListAccessibilityProvider<IExpr
return null;
}
}
export const SET_VARIABLE_ID = 'debug.setVariable';
CommandsRegistry.registerCommand({
id: SET_VARIABLE_ID,
handler: (accessor: ServicesAccessor) => {
const debugService = accessor.get(IDebugService);
debugService.getViewModel().setSelectedExpression(variableInternalContext);
}
});
export const COPY_VALUE_ID = 'debug.copyValue';
CommandsRegistry.registerCommand({
id: COPY_VALUE_ID,
handler: async (accessor: ServicesAccessor) => {
const instantiationService = accessor.get(IInstantiationService);
if (variableInternalContext) {
const action = instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variableInternalContext, 'variables');
await action.run();
}
}
});
export const BREAK_WHEN_VALUE_CHANGES_ID = 'debug.breakWhenValueChanges';
CommandsRegistry.registerCommand({
id: BREAK_WHEN_VALUE_CHANGES_ID,
handler: async (accessor: ServicesAccessor) => {
const debugService = accessor.get(IDebugService);
if (dataBreakpointInfoResponse) {
await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes);
}
}
});
export const COPY_EVALUATE_PATH_ID = 'debug.copyEvaluatePath';
CommandsRegistry.registerCommand({
id: COPY_EVALUATE_PATH_ID,
handler: async (accessor: ServicesAccessor, context: IVariablesContext) => {
const clipboardService = accessor.get(IClipboardService);
await clipboardService.writeText(context.variable.evaluateName!);
}
});
export const ADD_TO_WATCH_ID = 'debug.addToWatchExpressions';
CommandsRegistry.registerCommand({
id: ADD_TO_WATCH_ID,
handler: async (accessor: ServicesAccessor, context: IVariablesContext) => {
const debugService = accessor.get(IDebugService);
debugService.addWatchExpression(context.variable.evaluateName);
}
});

View File

@@ -8,10 +8,10 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService, RawContextKey, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { localize } from 'vs/nls';
import { StartAction, ConfigureAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions';
import { IDebugService } from 'vs/workbench/contrib/debug/common/debug';
import { IDebugService, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@@ -109,30 +109,32 @@ const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, {
content: localize({ key: 'openAFileWhichCanBeDebugged', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] },
"[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID),
when: CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated()
when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated())
});
let debugKeybindingLabel = '';
viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, {
content: localize({ key: 'runAndDebugAction', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] },
"[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID),
preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR]
preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR],
when: CONTEXT_DEBUGGERS_AVAILABLE
});
viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, {
content: localize({ key: 'detectThenRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] },
"[Show](command:{0}) all automatic debug configurations.", SelectAndStartAction.ID),
priority: ViewContentPriority.Lowest
priority: ViewContentPriority.Lowest,
when: CONTEXT_DEBUGGERS_AVAILABLE
});
viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, {
content: localize({ key: 'customizeRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] },
"To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID),
when: WorkbenchStateContext.notEqualsTo('empty')
when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.notEqualsTo('empty'))
});
viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, {
content: localize({ key: 'customizeRunAndDebugOpenFolder', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] },
"To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID),
when: WorkbenchStateContext.isEqualTo('empty')
when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.isEqualTo('empty'))
});

View File

@@ -59,6 +59,11 @@ export const CONTEXT_RESTART_FRAME_SUPPORTED = new RawContextKey<boolean>('resta
export const CONTEXT_JUMP_TO_CURSOR_SUPPORTED = new RawContextKey<boolean>('jumpToCursorSupported', false);
export const CONTEXT_STEP_INTO_TARGETS_SUPPORTED = new RawContextKey<boolean>('stepIntoTargetsSupported', false);
export const CONTEXT_BREAKPOINTS_EXIST = new RawContextKey<boolean>('breakpointsExist', false);
export const CONTEXT_DEBUGGERS_AVAILABLE = new RawContextKey<boolean>('debuggersAvailable', false);
export const CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT = new RawContextKey<string>('debugProtocolVariableMenuContext', undefined);
export const CONTEXT_SET_VARIABLE_SUPPORTED = new RawContextKey<boolean>('debugSetVariableSupported', false);
export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey<boolean>('breakWhenValueChangesSupported', false);
export const CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT = new RawContextKey<boolean>('variableEvaluateNamePresent', false);
export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug';
export const BREAKPOINT_EDITOR_CONTRIBUTION_ID = 'editor.contrib.breakpoint';
@@ -160,6 +165,13 @@ export interface IDebugSessionOptions {
compact?: boolean;
}
export interface IDataBreakpointInfoResponse {
dataId: string | null;
description: string;
canPersist?: boolean,
accessTypes?: DebugProtocol.DataBreakpointAccessType[];
}
export interface IDebugSession extends ITreeElement {
readonly configuration: IConfig;
@@ -220,7 +232,7 @@ export interface IDebugSession extends ITreeElement {
sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise<void>;
sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise<void>;
dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean, accessTypes?: DebugProtocol.DataBreakpointAccessType[] } | undefined>;
dataBreakpointInfo(name: string, variablesReference?: number): Promise<IDataBreakpointInfoResponse | undefined>;
sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise<void>;
sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise<void>;
breakpointsLocations(uri: uri, lineNumber: number): Promise<IPosition[]>;

View File

@@ -24,6 +24,10 @@ import { mixin } from 'vs/base/common/objects';
import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
interface IDebugProtocolVariableWithContext extends DebugProtocol.Variable {
__vscodeVariableMenuContext?: string;
}
export class ExpressionContainer implements IExpressionContainer {
public static readonly allValues = new Map<string, string>();
@@ -86,7 +90,7 @@ export class ExpressionContainer implements IExpressionContainer {
for (let i = 0; i < numberOfChunks; i++) {
const start = (this.startOfVariables || 0) + i * chunkSize;
const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize);
children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, { kind: 'virtual' }, undefined, true, start));
children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, { kind: 'virtual' }, undefined, undefined, true, start));
}
return children;
@@ -117,14 +121,14 @@ export class ExpressionContainer implements IExpressionContainer {
try {
const response = await this.session!.variables(this.reference || 0, this.threadId, filter, start, count);
return response && response.body && response.body.variables
? distinct(response.body.variables.filter(v => !!v), v => v.name).map(v => {
? distinct(response.body.variables.filter(v => !!v), v => v.name).map((v: IDebugProtocolVariableWithContext) => {
if (isString(v.value) && isString(v.name) && typeof v.variablesReference === 'number') {
return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type);
return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type, v.__vscodeVariableMenuContext);
}
return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, { kind: 'virtual' }, undefined, false);
return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, { kind: 'virtual' }, undefined, undefined, false);
}) : [];
} catch (e) {
return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, { kind: 'virtual' }, undefined, false)];
return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, { kind: 'virtual' }, undefined, undefined, false)];
}
}
@@ -218,6 +222,7 @@ export class Variable extends ExpressionContainer implements IExpression {
indexedVariables: number | undefined,
public presentationHint: DebugProtocol.VariablePresentationHint | undefined,
public type: string | undefined = undefined,
public variableMenuContext: string | undefined = undefined,
public available = true,
startOfVariables = 0
) {
@@ -247,6 +252,15 @@ export class Variable extends ExpressionContainer implements IExpression {
toString(): string {
return `${this.name}: ${this.value}`;
}
toDebugProtocolObject(): DebugProtocol.Variable {
return {
name: this.name,
variablesReference: this.reference || 0,
value: this.value,
evaluateName: this.evaluateName
};
}
}
export class Scope extends ExpressionContainer implements IScope {
@@ -267,6 +281,14 @@ export class Scope extends ExpressionContainer implements IScope {
toString(): string {
return this.name;
}
toDebugProtocolObject(): DebugProtocol.Scope {
return {
name: this.name,
variablesReference: this.reference || 0,
expensive: this.expensive
};
}
}
export class ErrorScope extends Scope {

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug';
import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils';
@@ -29,6 +29,7 @@ export class ViewModel implements IViewModel {
private restartFrameSupportedContextKey!: IContextKey<boolean>;
private stepIntoTargetsSupported!: IContextKey<boolean>;
private jumpToCursorSupported!: IContextKey<boolean>;
private setVariableSupported!: IContextKey<boolean>;
constructor(private contextKeyService: IContextKeyService) {
this.multiSessionView = false;
@@ -41,6 +42,7 @@ export class ViewModel implements IViewModel {
this.restartFrameSupportedContextKey = CONTEXT_RESTART_FRAME_SUPPORTED.bindTo(contextKeyService);
this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService);
this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService);
this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService);
});
}
@@ -74,6 +76,7 @@ export class ViewModel implements IViewModel {
this.restartFrameSupportedContextKey.set(session ? !!session.capabilities.supportsRestartFrame : false);
this.stepIntoTargetsSupported.set(session ? !!session.capabilities.supportsStepInTargetsRequest : false);
this.jumpToCursorSupported.set(session ? !!session.capabilities.supportsGotoTargetsRequest : false);
this.setVariableSupported.set(session ? !!session.capabilities.supportsSetVariable : false);
const attach = !!session && isSessionAttach(session);
this.focusedSessionIsAttach.set(attach);
});

View File

@@ -27,7 +27,8 @@ export class SimpleReplElement implements IReplElement {
) { }
toString(): string {
return this.value;
const sourceStr = this.sourceData ? ` ${this.sourceData.source.name}:${this.sourceData.lineNumber}` : '';
return this.value + sourceStr;
}
getId(): string {
@@ -144,7 +145,8 @@ export class ReplGroup implements IReplElement {
}
toString(): string {
return this.name;
const sourceStr = this.sourceData ? ` ${this.sourceData.source.name}:${this.sourceData.lineNumber}` : '';
return this.name + sourceStr;
}
addChild(child: IReplElement): void {
@@ -174,18 +176,13 @@ export class ReplGroup implements IReplElement {
}
}
type FilterFunc = ((element: IReplElement) => void);
export class ReplModel {
private replElements: IReplElement[] = [];
private readonly _onDidChangeElements = new Emitter<void>();
readonly onDidChangeElements = this._onDidChangeElements.event;
private filterFunc: FilterFunc | undefined;
getReplElements(): IReplElement[] {
return this.replElements.filter(element =>
this.filterFunc ? this.filterFunc(element) : true
);
return this.replElements;
}
async addReplExpression(session: IDebugSession, stackFrame: IStackFrame | undefined, name: string): Promise<void> {
@@ -320,10 +317,6 @@ export class ReplModel {
}
}
setFilter(filterFunc: FilterFunc): void {
this.filterFunc = filterFunc;
}
removeReplExpressions(): void {
if (this.replElements.length > 0) {
this.replElements = [];

View File

@@ -177,7 +177,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env?
command += `cd ${quote(cwd)} ; `;
}
if (env) {
command += 'env';
command += '/usr/bin/env';
for (let key in env) {
const value = env[key];
if (value === null) {

View File

@@ -197,10 +197,13 @@ suite('Debug - REPL', () => {
const repl = new ReplModel();
const replFilter = new ReplFilter();
repl.setFilter((element) => {
const filterResult = replFilter.filter(element, TreeVisibility.Visible);
return filterResult === true || filterResult === TreeVisibility.Visible;
});
const getFilteredElements = () => {
const elements = repl.getReplElements();
return elements.filter(e => {
const filterResult = replFilter.filter(e, TreeVisibility.Visible);
return filterResult === true || filterResult === TreeVisibility.Visible;
});
};
repl.appendToRepl(session, 'first line\n', severity.Info);
repl.appendToRepl(session, 'second line\n', severity.Info);
@@ -208,19 +211,19 @@ suite('Debug - REPL', () => {
repl.appendToRepl(session, 'fourth line\n', severity.Info);
replFilter.filterQuery = 'first';
let r1 = <SimpleReplElement[]>repl.getReplElements();
let r1 = <SimpleReplElement[]>getFilteredElements();
assert.equal(r1.length, 1);
assert.equal(r1[0].value, 'first line\n');
replFilter.filterQuery = '!first';
let r2 = <SimpleReplElement[]>repl.getReplElements();
let r2 = <SimpleReplElement[]>getFilteredElements();
assert.equal(r1.length, 1);
assert.equal(r2[0].value, 'second line\n');
assert.equal(r2[1].value, 'third line\n');
assert.equal(r2[2].value, 'fourth line\n');
replFilter.filterQuery = 'first, line';
let r3 = <SimpleReplElement[]>repl.getReplElements();
let r3 = <SimpleReplElement[]>getFilteredElements();
assert.equal(r3.length, 4);
assert.equal(r3[0].value, 'first line\n');
assert.equal(r3[1].value, 'second line\n');
@@ -228,22 +231,22 @@ suite('Debug - REPL', () => {
assert.equal(r3[3].value, 'fourth line\n');
replFilter.filterQuery = 'line, !second';
let r4 = <SimpleReplElement[]>repl.getReplElements();
let r4 = <SimpleReplElement[]>getFilteredElements();
assert.equal(r4.length, 3);
assert.equal(r4[0].value, 'first line\n');
assert.equal(r4[1].value, 'third line\n');
assert.equal(r4[2].value, 'fourth line\n');
replFilter.filterQuery = '!second, line';
let r4_same = <SimpleReplElement[]>repl.getReplElements();
let r4_same = <SimpleReplElement[]>getFilteredElements();
assert.equal(r4.length, r4_same.length);
replFilter.filterQuery = '!line';
let r5 = <SimpleReplElement[]>repl.getReplElements();
let r5 = <SimpleReplElement[]>getFilteredElements();
assert.equal(r5.length, 0);
replFilter.filterQuery = 'smth';
let r6 = <SimpleReplElement[]>repl.getReplElements();
let r6 = <SimpleReplElement[]>getFilteredElements();
assert.equal(r6.length, 0);
});
});

View File

@@ -3,24 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IExtensionTipsService, IExtensionManagementService, ILocalExtension, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IExtensionTipsService, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { distinct } from 'vs/base/common/arrays';
import { Emitter } from 'vs/base/common/event';
export class ConfigBasedRecommendations extends ExtensionRecommendations {
private importantTips: IConfigBasedExtensionTip[] = [];
private otherTips: IConfigBasedExtensionTip[] = [];
private _onDidChangeRecommendations = this._register(new Emitter<void>());
readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event;
private _otherRecommendations: ExtensionRecommendation[] = [];
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._otherRecommendations; }
@@ -30,24 +27,16 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return [...this.importantRecommendations, ...this.otherRecommendations]; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
}
protected async doActivate(): Promise<void> {
await this.fetch();
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e)));
this.promptWorkspaceRecommendations();
}
private async fetch(): Promise<void> {
@@ -70,54 +59,13 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations {
this._importantRecommendations = this.importantTips.map(tip => this.toExtensionRecommendation(tip));
}
private async promptWorkspaceRecommendations(): Promise<void> {
if (this.hasToIgnoreRecommendationNotifications()) {
return;
}
if (this.importantTips.length === 0) {
return;
}
const local = await this.extensionManagementService.getInstalled();
const { uninstalled } = this.groupByInstalled(distinct(this.importantTips.map(({ extensionId }) => extensionId)), local);
if (uninstalled.length === 0) {
return;
}
const importantExtensions = this.filterIgnoredOrNotAllowed(uninstalled);
if (importantExtensions.length === 0) {
return;
}
for (const extension of importantExtensions) {
const tip = this.importantTips.filter(tip => tip.extensionId === extension)[0];
const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended for this workspace.", tip.extensionName)
: localize('extensionRecommended', "The '{0}' extension is recommended for this workspace.", tip.extensionName);
this.promptImportantExtensionsInstallNotification([extension], message);
}
}
private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } {
const installed: string[] = [], uninstalled: string[] = [];
const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set<string>());
recommendationsToSuggest.forEach(id => {
if (installedExtensionsIds.has(id.toLowerCase())) {
installed.push(id);
} else {
uninstalled.push(id);
}
});
return { installed, uninstalled };
}
private async onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): Promise<void> {
if (event.added.length) {
const oldImportantRecommended = this.importantTips;
await this.fetch();
// Suggest only if at least one of the newly added recommendations was not suggested before
if (this.importantTips.some(current => oldImportantRecommended.every(old => current.extensionId !== old.extensionId))) {
return this.promptWorkspaceRecommendations();
this._onDidChangeRecommendations.fire();
}
}
}

View File

@@ -11,13 +11,9 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags';
import { isNumber } from 'vs/base/common/types';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { localize } from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
type DynamicWorkspaceRecommendationsClassification = {
count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -34,19 +30,15 @@ export class DynamicWorkspaceRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IFileService private readonly fileService: IFileService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
}
protected async doActivate(): Promise<void> {

View File

@@ -5,17 +5,12 @@
import { IExtensionTipsService, IExecutableBasedExtensionTip, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { timeout } from 'vs/base/common/async';
import { localize } from 'vs/nls';
import { IStringDictionary } from 'vs/base/common/collections';
import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { optional } from 'vs/platform/instantiation/common/instantiation';
import { basename } from 'vs/base/common/path';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService';
type ExeExtensionRecommendationsClassification = {
@@ -25,30 +20,24 @@ type ExeExtensionRecommendationsClassification = {
export class ExeBasedRecommendations extends ExtensionRecommendations {
private _otherTips: IExecutableBasedExtensionTip[] = [];
private _importantTips: IExecutableBasedExtensionTip[] = [];
private readonly _otherRecommendations: ExtensionRecommendation[] = [];
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._otherRecommendations; }
private readonly _importantRecommendations: ExtensionRecommendation[] = [];
get importantRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._importantRecommendations; }
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._otherTips.map(tip => this.toExtensionRecommendation(tip)); }
get importantRecommendations(): ReadonlyArray<ExtensionRecommendation> { return this._importantTips.map(tip => this.toExtensionRecommendation(tip)); }
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return [...this.importantRecommendations, ...this.otherRecommendations]; }
private readonly tasExperimentService: ITASExperimentService | undefined;
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@optional(ITASExperimentService) tasExperimentService: ITASExperimentService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
this.tasExperimentService = tasExperimentService;
/*
@@ -58,27 +47,35 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
timeout(3000).then(() => this.fetchAndPromptImportantExeBasedRecommendations());
}
getRecommendations(exe: string): { important: ExtensionRecommendation[], others: ExtensionRecommendation[] } {
const important = this._importantTips
.filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase())
.map(tip => this.toExtensionRecommendation(tip));
const others = this._otherTips
.filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase())
.map(tip => this.toExtensionRecommendation(tip));
return { important, others };
}
protected async doActivate(): Promise<void> {
const otherExectuableBasedTips = await this.extensionTipsService.getOtherExecutableBasedTips();
otherExectuableBasedTips.forEach(tip => this._otherRecommendations.push(this.toExtensionRecommendation(tip)));
this._otherTips = await this.extensionTipsService.getOtherExecutableBasedTips();
await this.fetchImportantExeBasedRecommendations();
}
private _importantExeBasedRecommendations: Promise<IStringDictionary<IExecutableBasedExtensionTip>> | undefined;
private async fetchImportantExeBasedRecommendations(): Promise<IStringDictionary<IExecutableBasedExtensionTip>> {
private _importantExeBasedRecommendations: Promise<Map<string, IExecutableBasedExtensionTip>> | undefined;
private async fetchImportantExeBasedRecommendations(): Promise<Map<string, IExecutableBasedExtensionTip>> {
if (!this._importantExeBasedRecommendations) {
this._importantExeBasedRecommendations = this.doFetchImportantExeBasedRecommendations();
}
return this._importantExeBasedRecommendations;
}
private async doFetchImportantExeBasedRecommendations(): Promise<IStringDictionary<IExecutableBasedExtensionTip>> {
const importantExeBasedRecommendations: IStringDictionary<IExecutableBasedExtensionTip> = {};
const importantExectuableBasedTips = await this.extensionTipsService.getImportantExecutableBasedTips();
importantExectuableBasedTips.forEach(tip => {
this._importantRecommendations.push(this.toExtensionRecommendation(tip));
importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip;
});
private async doFetchImportantExeBasedRecommendations(): Promise<Map<string, IExecutableBasedExtensionTip>> {
const importantExeBasedRecommendations = new Map<string, IExecutableBasedExtensionTip>();
this._importantTips = await this.extensionTipsService.getImportantExecutableBasedTips();
this._importantTips.forEach(tip => importantExeBasedRecommendations.set(tip.extensionId.toLowerCase(), tip));
return importantExeBasedRecommendations;
}
@@ -86,39 +83,45 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
const importantExeBasedRecommendations = await this.fetchImportantExeBasedRecommendations();
const local = await this.extensionManagementService.getInstalled();
const { installed, uninstalled } = this.groupByInstalled(Object.keys(importantExeBasedRecommendations), local);
const { installed, uninstalled } = this.groupByInstalled([...importantExeBasedRecommendations.keys()], local);
/* Log installed and uninstalled exe based recommendations */
for (const extensionId of installed) {
const tip = importantExeBasedRecommendations[extensionId];
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) });
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) });
}
}
for (const extensionId of uninstalled) {
const tip = importantExeBasedRecommendations[extensionId];
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) });
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) });
}
}
this.promptImportantExeBasedRecommendations(uninstalled, importantExeBasedRecommendations);
}
private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: IStringDictionary<IExecutableBasedExtensionTip>): Promise<void> {
if (this.hasToIgnoreRecommendationNotifications()) {
private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: Map<string, IExecutableBasedExtensionTip>): Promise<void> {
if (this.promptedExtensionRecommendations.hasToIgnoreRecommendationNotifications()) {
return;
}
recommendations = this.filterIgnoredOrNotAllowed(recommendations);
recommendations = this.promptedExtensionRecommendations.filterIgnoredOrNotAllowed(recommendations);
if (recommendations.length === 0) {
return;
}
const recommendationsByExe = new Map<string, IExecutableBasedExtensionTip[]>();
for (const extensionId of recommendations) {
const tip = importantExeBasedRecommendations[extensionId];
let tips = recommendationsByExe.get(tip.exeFriendlyName);
if (!tips) {
tips = [];
recommendationsByExe.set(tip.exeFriendlyName, tips);
const tip = importantExeBasedRecommendations.get(extensionId);
if (tip) {
let tips = recommendationsByExe.get(tip.exeFriendlyName);
if (!tips) {
tips = [];
recommendationsByExe.set(tip.exeFriendlyName, tips);
}
tips.push(tip);
}
tips.push(tip);
}
for (const [, tips] of recommendationsByExe) {
@@ -127,22 +130,8 @@ export class ExeBasedRecommendations extends ExtensionRecommendations {
await this.tasExperimentService.getTreatment<boolean>('wslpopupaa');
}
if (tips.length === 1) {
const tip = tips[0];
const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended as you have {1} installed on your system.", tip.extensionName, tip.exeFriendlyName || basename(tip.windowsPath!))
: localize('exeRecommended', "The '{0}' extension is recommended as you have {1} installed on your system.", tip.extensionName, tip.exeFriendlyName || basename(tip.windowsPath!));
this.promptImportantExtensionsInstallNotification(extensionIds, message);
}
else if (tips.length === 2) {
const message = localize('two extensions recommended', "The '{0}' and '{1}' extensions are recommended as you have {2} installed on your system.", tips[0].extensionName, tips[1].extensionName, tips[0].exeFriendlyName || basename(tips[0].windowsPath!));
this.promptImportantExtensionsInstallNotification(extensionIds, message);
}
else if (tips.length > 2) {
const message = localize('more than two extensions recommended', "The '{0}', '{1}' and other extensions are recommended as you have {2} installed on your system.", tips[0].extensionName, tips[1].extensionName, tips[0].exeFriendlyName || basename(tips[0].windowsPath!));
this.promptImportantExtensionsInstallNotification(extensionIds, message);
}
const message = localize('exeRecommended', "You have {0} installed on your system. Do you want to install the recommended extensions for it?", tips[0].exeFriendlyName);
this.promptedExtensionRecommendations.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`);
}
}

View File

@@ -3,16 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/workbench/contrib/experiments/common/experimentService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
export class ExperimentalRecommendations extends ExtensionRecommendations {
@@ -20,16 +14,10 @@ export class ExperimentalRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExperimentService private readonly experimentService: IExperimentService,
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
}
/**

View File

@@ -15,7 +15,7 @@ import { isPromiseCanceledError } from 'vs/base/common/errors';
import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { domEvent } from 'vs/base/browser/event';
import { append, $, addClass, removeClass, finalHandler, join, toggleClass, hide, show, addDisposableListener, EventType } from 'vs/base/browser/dom';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
@@ -25,7 +25,7 @@ import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions';
import { /*RatingsWidget, InstallCountWidget, */RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets';
import { EditorOptions } from 'vs/workbench/common/editor';
import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction, SetProductIconThemeAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
@@ -165,7 +165,7 @@ interface IExtensionEditorTemplate {
header: HTMLElement;
}
export class ExtensionEditor extends BaseEditor {
export class ExtensionEditor extends EditorPane {
static readonly ID: string = 'workbench.editor.extension';
@@ -315,8 +315,8 @@ export class ExtensionEditor extends BaseEditor {
return disposables;
}
async setInput(input: ExtensionsInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
await super.setInput(input, options, token);
async setInput(input: ExtensionsInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
await super.setInput(input, options, context, token);
if (this.template) {
await this.updateTemplate(input, this.template, !!options?.preserveFocus);
}
@@ -927,6 +927,7 @@ export class ExtensionEditor extends BaseEditor {
this.renderLocalizations(content, manifest, layout),
renderDashboardContributions(content, manifest, layout), // {{SQL CARBON EDIT}}
this.renderCustomEditors(content, manifest, layout),
this.renderAuthentication(content, manifest, layout),
];
scrollableContent.scanDomNode();
@@ -1174,6 +1175,32 @@ export class ExtensionEditor extends BaseEditor {
return true;
}
private renderAuthentication(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean {
const authentication = manifest.contributes?.authentication || [];
if (!authentication.length) {
return false;
}
const details = $('details', { open: true, ontoggle: onDetailsToggle },
$('summary', { tabindex: '0' }, localize('authentication', "Authentication ({0})", authentication.length)),
$('table', undefined,
$('tr', undefined,
$('th', undefined, localize('authentication.label', "Label")),
$('th', undefined, localize('authentication.id', "Id"))
),
...authentication.map(action =>
$('tr', undefined,
$('td', undefined, action.label),
$('td', undefined, action.id)
)
)
)
);
append(container, details);
return true;
}
private renderColorThemes(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean {
const contrib = manifest.contributes?.themes || [];
if (!contrib.length) {

View File

@@ -8,19 +8,27 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { localize } from 'vs/nls';
import { InstallRecommendedExtensionAction, ShowRecommendedExtensionAction, ShowRecommendedExtensionsAction, InstallRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ExtensionRecommendationSource, IExtensionRecommendationReson } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions';
import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { EnablementState, ExtensionRecommendationSource, IExtensionRecommendationReson, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionsConfiguration, ConfigurationKey, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { IAction } from 'vs/base/common/actions';
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { CancellationToken } from 'vs/base/common/cancellation';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
type ExtensionRecommendationsNotificationClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
extensionId?: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
};
type ExtensionWorkspaceRecommendationsNotificationClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
const ignoreImportantExtensionRecommendation = 'extensionsAssistant/importantRecommendationsIgnore';
const choiceNever = localize('neverShowAgain', "Don't Show Again");
@@ -36,16 +44,9 @@ export abstract class ExtensionRecommendations extends Disposable {
protected abstract doActivate(): Promise<void>;
constructor(
protected readonly isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IConfigurationService protected readonly configurationService: IConfigurationService,
@INotificationService protected readonly notificationService: INotificationService,
@ITelemetryService protected readonly telemetryService: ITelemetryService,
@IStorageService protected readonly storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
protected readonly promptedExtensionRecommendations: PromptedExtensionRecommendations,
) {
super();
storageKeysSyncRegistryService.registerStorageKey({ key: ignoreImportantExtensionRecommendation, version: 1 });
}
private _activationPromise: Promise<void> | null = null;
@@ -57,47 +58,63 @@ export abstract class ExtensionRecommendations extends Disposable {
return this._activationPromise;
}
private runAction(action: IAction) {
try {
action.run();
} finally {
action.dispose();
}
}
export class PromptedExtensionRecommendations extends Disposable {
constructor(
private readonly isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@INotificationService private readonly notificationService: INotificationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super();
storageKeysSyncRegistryService.registerStorageKey({ key: ignoreImportantExtensionRecommendation, version: 1 });
}
protected promptImportantExtensionsInstallNotification(extensionIds: string[], message: string): void {
async promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string): Promise<void> {
if (this.hasToIgnoreRecommendationNotifications()) {
return;
}
const extensions = await this.getInstallableExtensions(extensionIds);
if (!extensions.length) {
return;
}
this.notificationService.prompt(Severity.Info, message,
[{
label: extensionIds.length === 1 ? localize('install', 'Install') : localize('installAll', "Install All"),
label: localize('install', "Install"),
run: async () => {
for (const extensionId of extensionIds) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId });
}
if (extensionIds.length === 1) {
this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionAction, extensionIds[0]));
} else {
this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionsAction, InstallRecommendedExtensionsAction.ID, InstallRecommendedExtensionsAction.LABEL, extensionIds, 'install-recommendations'));
}
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
await Promise.all(extensions.map(async extension => {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id });
this.extensionsWorkbenchService.open(extension, { pinned: true });
await this.extensionManagementService.installFromGallery(extension.gallery!);
}));
}
}, {
label: extensionIds.length === 1 ? localize('moreInformation', "More Information") : localize('showRecommendations', "Show Recommendations"),
run: () => {
for (const extensionId of extensionIds) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId });
}
if (extensionIds.length === 1) {
this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionAction, extensionIds[0]));
} else {
this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL));
label: localize('show recommendations', "Show Recommendations"),
run: async () => {
for (const extension of extensions) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id });
this.extensionsWorkbenchService.open(extension, { pinned: true });
}
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
}
}, {
label: choiceNever,
isSecondary: true,
run: () => {
for (const extensionId of extensionIds) {
this.addToImportantRecommendationsIgnore(extensionId);
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId });
for (const extension of extensions) {
this.addToImportantRecommendationsIgnore(extension.identifier.id);
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id });
}
this.notificationService.prompt(
Severity.Info,
@@ -115,20 +132,78 @@ export abstract class ExtensionRecommendations extends Disposable {
{
sticky: true,
onCancel: () => {
for (const extensionId of extensionIds) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId });
for (const extension of extensions) {
this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id });
}
}
}
);
}
protected hasToIgnoreRecommendationNotifications(): boolean {
async promptWorkspaceRecommendations(recommendations: string[]): Promise<void> {
if (this.hasToIgnoreWorkspaceRecommendationNotifications()) {
return;
}
let installed = await this.extensionManagementService.getInstalled();
installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
recommendations = recommendations.filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier)));
if (!recommendations.length) {
return;
}
const extensions = await this.getInstallableExtensions(recommendations);
if (!extensions.length) {
return;
}
const searchValue = '@recommended ';
this.notificationService.prompt(
Severity.Info,
localize('workspaceRecommended', "Do you want to install the recommended extensions for this repository?"),
[{
label: localize('install', "Install"),
run: async () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' });
await Promise.all(extensions.map(async extension => {
this.extensionsWorkbenchService.open(extension, { pinned: true });
await this.extensionManagementService.installFromGallery(extension.gallery!);
}));
}
}, {
label: localize('showRecommendations', "Show Recommendations"),
run: async () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' });
this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue));
}
}, {
label: localize('neverShowAgain', "Don't Show Again"),
isSecondary: true,
run: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' });
this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE);
}
}],
{
sticky: true,
onCancel: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' });
}
}
);
}
hasToIgnoreRecommendationNotifications(): boolean {
const config = this.configurationService.getValue<IExtensionsConfiguration>(ConfigurationKey);
return config.ignoreRecommendations || config.showRecommendationsOnlyOnDemand;
}
protected filterIgnoredOrNotAllowed(recommendationsToSuggest: string[]): string[] {
hasToIgnoreWorkspaceRecommendationNotifications(): boolean {
return this.hasToIgnoreRecommendationNotifications() || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false);
}
filterIgnoredOrNotAllowed(recommendationsToSuggest: string[]): string[] {
const importantRecommendationsIgnoreList = (<string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendation, StorageScope.GLOBAL, '[]'))).map(e => e.toLowerCase());
return recommendationsToSuggest.filter(id => {
if (importantRecommendationsIgnoreList.indexOf(id) !== -1) {
@@ -141,6 +216,27 @@ export abstract class ExtensionRecommendations extends Disposable {
});
}
private async getInstallableExtensions(extensionIds: string[]): Promise<IExtension[]> {
const extensions: IExtension[] = [];
if (extensionIds.length) {
const pager = await this.extensionsWorkbenchService.queryGallery({ names: extensionIds, pageSize: extensionIds.length, source: 'install-recommendations' }, CancellationToken.None);
for (const extension of pager.firstPage) {
if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) {
extensions.push(extension);
}
}
}
return extensions;
}
private async runAction(action: IAction): Promise<void> {
try {
await action.run();
} finally {
action.dispose();
}
}
private addToImportantRecommendationsIgnore(id: string) {
const importantRecommendationsIgnoreList = <string[]>JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendation, StorageScope.GLOBAL, '[]'));
importantRecommendationsIgnoreList.push(id.toLowerCase());

View File

@@ -22,12 +22,12 @@ import { ExperimentalRecommendations } from 'vs/workbench/contrib/extensions/bro
import { WorkspaceRecommendations } from 'vs/workbench/contrib/extensions/browser/workspaceRecommendations';
import { FileBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/fileBasedRecommendations';
import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/keymapRecommendations';
import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/platform/extensions/common/extensions';
import { StaticRecommendations } from 'sql/workbench/contrib/extensions/browser/staticRecommendations';
import { ScenarioRecommendations } from 'sql/workbench/contrib/extensions/browser/scenarioRecommendations';
import { ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations';
import { StaticRecommendations } from 'sql/workbench/contrib/extensions/browser/staticRecommendations';
import { ScenarioRecommendations } from 'sql/workbench/contrib/extensions/browser/scenarioRecommendations';
import { ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/platform/extensions/common/extensions';
type IgnoreRecommendationClassification = {
recommendationReason: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
@@ -40,6 +40,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
declare readonly _serviceBrand: undefined;
private readonly promptedExtensionRecommendations: PromptedExtensionRecommendations;
// Recommendations
private readonly fileBasedRecommendations: FileBasedRecommendations;
private readonly workspaceRecommendations: WorkspaceRecommendations;
@@ -54,7 +56,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
// Ignored Recommendations
private globallyIgnoredRecommendations: string[] = [];
public loadWorkspaceConfigPromise: Promise<void>;
public readonly activationPromise: Promise<void>;
private sessionSeed: number;
private readonly _onRecommendationChange = this._register(new Emitter<RecommendationChangeNotification>());
@@ -62,7 +64,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@ILifecycleService lifecycleService: ILifecycleService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@IStorageService private readonly storageService: IStorageService,
@@ -76,19 +78,20 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
storageKeysSyncRegistryService.registerStorageKey({ key: ignoredRecommendationsStorageKey, version: 1 });
const isExtensionAllowedToBeRecommended = (extensionId: string) => this.isExtensionAllowedToBeRecommended(extensionId);
this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations, isExtensionAllowedToBeRecommended);
this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations, isExtensionAllowedToBeRecommended);
this.experimentalRecommendations = instantiationService.createInstance(ExperimentalRecommendations, isExtensionAllowedToBeRecommended);
this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations, isExtensionAllowedToBeRecommended);
this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations, isExtensionAllowedToBeRecommended);
this.dynamicWorkspaceRecommendations = instantiationService.createInstance(DynamicWorkspaceRecommendations, isExtensionAllowedToBeRecommended);
this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations, isExtensionAllowedToBeRecommended);
this.staticRecommendations = instantiationService.createInstance(StaticRecommendations, isExtensionAllowedToBeRecommended); // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations = instantiationService.createInstance(ScenarioRecommendations, isExtensionAllowedToBeRecommended); // {{SQL CARBON EDIT}} add ours
this.promptedExtensionRecommendations = instantiationService.createInstance(PromptedExtensionRecommendations, isExtensionAllowedToBeRecommended);
this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations, this.promptedExtensionRecommendations);
this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations, this.promptedExtensionRecommendations);
this.experimentalRecommendations = instantiationService.createInstance(ExperimentalRecommendations, this.promptedExtensionRecommendations);
this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations, this.promptedExtensionRecommendations);
this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations, this.promptedExtensionRecommendations);
this.dynamicWorkspaceRecommendations = instantiationService.createInstance(DynamicWorkspaceRecommendations, this.promptedExtensionRecommendations);
this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations, this.promptedExtensionRecommendations);
this.staticRecommendations = instantiationService.createInstance(StaticRecommendations, this.promptedExtensionRecommendations); // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations = instantiationService.createInstance(ScenarioRecommendations, this.promptedExtensionRecommendations); // {{SQL CARBON EDIT}} add ours
if (!this.isEnabled()) {
this.sessionSeed = 0;
this.loadWorkspaceConfigPromise = Promise.resolve();
this.activationPromise = Promise.resolve();
return;
}
@@ -96,19 +99,35 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
this.globallyIgnoredRecommendations = this.getCachedIgnoredRecommendations();
// Activation
this.loadWorkspaceConfigPromise = this.workspaceRecommendations.activate().then(() => this.fileBasedRecommendations.activate());
this.experimentalRecommendations.activate();
this.keymapRecommendations.activate();
this.staticRecommendations.activate(); // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations.activate(); // {{SQL CARBON EDIT}} add ours
if (!this.configurationService.getValue<boolean>(ShowRecommendationsOnlyOnDemandKey)) {
lifecycleService.when(LifecyclePhase.Eventually).then(() => this.activateProactiveRecommendations());
}
this.activationPromise = this.activate();
this._register(this.extensionManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e)));
this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
}
private async activate(): Promise<void> {
await this.lifecycleService.when(LifecyclePhase.Restored);
// activate all recommendations
await Promise.all([
this.workspaceRecommendations.activate(),
this.fileBasedRecommendations.activate(),
this.experimentalRecommendations.activate(),
this.keymapRecommendations.activate(),
this.staticRecommendations.activate(), // {{SQL CARBON EDIT}} add ours
this.scenarioRecommendations.activate(), // {{SQL CARBON EDIT}} add ours
this.lifecycleService.when(LifecyclePhase.Eventually)
.then(() => {
if (!this.configurationService.getValue<boolean>(ShowRecommendationsOnlyOnDemandKey)) {
this.activateProactiveRecommendations();
}
})
]);
await this.promptWorkspaceRecommendations();
this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations)(() => this.promptWorkspaceRecommendations()));
}
private isEnabled(): boolean {
return this.galleryService.isEnabled() && !this.environmentService.extensionDevelopmentLocationURI && this.configurationService.getValue<string>(ExtensionsPolicyKey) !== ExtensionsPolicy.allowNone;
}
@@ -143,9 +162,12 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
return output;
}
async getConfigBasedRecommendations(): Promise<IExtensionRecommendation[]> {
async getConfigBasedRecommendations(): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> {
await this.configBasedRecommendations.activate();
return this.toExtensionRecommendations(this.configBasedRecommendations.recommendations);
return {
important: this.toExtensionRecommendations(this.configBasedRecommendations.importantRecommendations),
others: this.toExtensionRecommendations(this.configBasedRecommendations.otherRecommendations)
};
}
async getOtherRecommendations(): Promise<IExtensionRecommendation[]> {
@@ -202,6 +224,13 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
return this.toExtensionRecommendations(this.workspaceRecommendations.recommendations);
}
async getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> {
await this.exeBasedRecommendations.activate();
const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe)
: { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations };
return { important: this.toExtensionRecommendations(important), others: this.toExtensionRecommendations(others) };
}
getFileBasedRecommendations(): IExtensionRecommendation[] {
return this.toExtensionRecommendations(this.fileBasedRecommendations.recommendations);
}
@@ -265,6 +294,16 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
return allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1;
}
private async promptWorkspaceRecommendations(): Promise<void> {
const allowedRecommendations = [...this.workspaceRecommendations.recommendations, ...this.configBasedRecommendations.importantRecommendations]
.map(({ extensionId }) => extensionId)
.filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId));
if (allowedRecommendations.length) {
await this.promptedExtensionRecommendations.promptWorkspaceRecommendations(allowedRecommendations);
}
}
private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void {
if (e.key === ignoredRecommendationsStorageKey && e.scope === StorageScope.GLOBAL
&& this.ignoredRecommendationsValue !== this.getStoredIgnoredRecommendationsValue() /* This checks if current window changed the value or not */) {

View File

@@ -15,11 +15,10 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo
import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
import {
OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction,
ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction,
EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, ShowAzureExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction
EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction
} from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor';
@@ -55,7 +54,7 @@ import { MultiCommand } from 'vs/editor/browser/editorExtensions';
import { Webview } from 'vs/workbench/contrib/webview/browser/webview';
// Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);
// registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben uncomment when 'semver-umd' can be loaded
registerSingleton(IExtensionRecommendationsService, ExtensionRecommendationsService);
Registry.as<IOutputChannelRegistry>(OutputExtensions.OutputChannels)
@@ -113,9 +112,6 @@ actionRegistry.registerWorkbenchAction(keymapRecommendationsActionDescriptor, 'P
const languageExtensionsActionDescriptor = SyncActionDescriptor.from(ShowLanguageExtensionsAction);
actionRegistry.registerWorkbenchAction(languageExtensionsActionDescriptor, 'Preferences: Language Extensions', PreferencesLabel);
const azureExtensionsActionDescriptor = SyncActionDescriptor.from(ShowAzureExtensionsAction);
actionRegistry.registerWorkbenchAction(azureExtensionsActionDescriptor, 'Preferences: Azure Extensions', PreferencesLabel);
const popularActionDescriptor = SyncActionDescriptor.from(ShowPopularExtensionsAction);
actionRegistry.registerWorkbenchAction(popularActionDescriptor, 'Extensions: Show Popular Extensions', ExtensionsLabel);

View File

@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
// TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);

View File

@@ -35,7 +35,6 @@ import { Color } from 'vs/base/common/color';
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { PagedModel } from 'vs/base/common/paging';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { MenuRegistry, MenuId, IMenuService } from 'vs/platform/actions/common/actions';
@@ -928,7 +927,7 @@ export class EnableForWorkspaceAction extends ExtensionAction {
if (this.extension && this.extension.local) {
this.enabled = this.extension.state === ExtensionState.Installed
&& !this.extensionEnablementService.isEnabled(this.extension.local)
&& this.extensionEnablementService.canChangeEnablement(this.extension.local);
&& this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local);
}
}
@@ -989,7 +988,7 @@ export class DisableForWorkspaceAction extends ExtensionAction {
if (this.extension && this.extension.local && this.runningExtensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY)) {
this.enabled = this.extension.state === ExtensionState.Installed
&& (this.extension.enablementState === EnablementState.EnabledGlobally || this.extension.enablementState === EnablementState.EnabledWorkspace)
&& this.extensionEnablementService.canChangeEnablement(this.extension.local);
&& this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local);
}
}
@@ -1850,86 +1849,6 @@ export class ShowRecommendedExtensionsAction extends Action {
}
}
export class InstallRecommendedExtensionsAction extends Action {
static readonly ID = 'workbench.extensions.action.installRecommendedExtensions';
static readonly LABEL = localize('installRecommendedExtensions', "Install Recommended Extensions");
private _recommendations: string[] = [];
get recommendations(): string[] { return this._recommendations; }
set recommendations(recommendations: string[]) { this._recommendations = recommendations; this.enabled = this._recommendations.length > 0; }
constructor(
id: string,
label: string,
recommendations: string[],
private readonly source: string,
@IViewletService private readonly viewletService: IViewletService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@IProductService private readonly productService: IProductService,
) {
super(id, label, 'extension-action');
this.recommendations = recommendations;
}
run(): Promise<any> {
return this.viewletService.openViewlet(VIEWLET_ID, true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet.search('@recommended ');
viewlet.focus();
const names = this.recommendations;
return this.extensionWorkbenchService.queryGallery({ names, source: this.source }, CancellationToken.None).then(pager => {
let installPromises: Promise<any>[] = [];
let model = new PagedModel(pager);
for (let i = 0; i < pager.total; i++) {
installPromises.push(model.resolve(i, CancellationToken.None).then(e => this.installExtension(e)));
}
return Promise.all(installPromises);
});
});
}
private async installExtension(extension: IExtension): Promise<void> {
try {
if (extension.local && extension.gallery) {
if (prefersExecuteOnUI(extension.local.manifest, this.productService, this.configurationService)) {
if (this.extensionManagementServerService.localExtensionManagementServer) {
await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(extension.gallery);
return;
}
} else if (this.extensionManagementServerService.remoteExtensionManagementServer) {
await this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(extension.gallery);
return;
}
}
await this.extensionWorkbenchService.install(extension);
} catch (err) {
console.error(err);
return promptDownloadManually(extension.gallery, localize('failedToInstall', "Failed to install \'{0}\'.", extension.identifier.id), err, this.instantiationService);
}
}
}
export class InstallWorkspaceRecommendedExtensionsAction extends InstallRecommendedExtensionsAction {
constructor(
recommendations: string[],
@IViewletService viewletService: IViewletService,
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionsWorkbenchService extensionWorkbenchService: IExtensionsWorkbenchService,
@IConfigurationService configurationService: IConfigurationService,
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IProductService productService: IProductService,
) {
super('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), recommendations, 'install-all-workspace-recommendations',
viewletService, instantiationService, extensionWorkbenchService, configurationService, extensionManagementServerService, productService);
}
}
export class ShowRecommendedExtensionAction extends Action {
static readonly ID = 'workbench.extensions.action.showRecommendedExtension';
@@ -1942,7 +1861,7 @@ export class ShowRecommendedExtensionAction extends Action {
@IViewletService private readonly viewletService: IViewletService,
@IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService,
) {
super(InstallRecommendedExtensionAction.ID, InstallRecommendedExtensionAction.LABEL, undefined, false);
super(ShowRecommendedExtensionAction.ID, ShowRecommendedExtensionAction.LABEL, undefined, false);
this.extensionId = extensionId;
}
@@ -2096,29 +2015,6 @@ export class ShowLanguageExtensionsAction extends Action {
}
}
export class ShowAzureExtensionsAction extends Action {
static readonly ID = 'workbench.extensions.action.showAzureExtensions';
static readonly LABEL = localize('showAzureExtensionsShort', "Azure Extensions");
constructor(
id: string,
label: string,
@IViewletService private readonly viewletService: IViewletService
) {
super(id, label, undefined, true);
}
run(): Promise<void> {
return this.viewletService.openViewlet(VIEWLET_ID, true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet.search('@sort:installs azure ');
viewlet.focus();
});
}
}
export class SearchCategoryAction extends Action {
constructor(
@@ -2131,12 +2027,23 @@ export class SearchCategoryAction extends Action {
}
run(): Promise<void> {
return this.viewletService.openViewlet(VIEWLET_ID, true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet.search(`@category:"${this.category.toLowerCase()}"`);
viewlet.focus();
});
return new SearchExtensionsAction(`@category:"${this.category.toLowerCase()}"`, this.viewletService).run();
}
}
export class SearchExtensionsAction extends Action {
constructor(
private readonly searchValue: string,
@IViewletService private readonly viewletService: IViewletService
) {
super('extensions.searchExtensions', localize('search recommendations', "Search Extensions"), undefined, true);
}
async run(): Promise<void> {
const viewPaneContainer = (await this.viewletService.openViewlet(VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer;
viewPaneContainer.search(this.searchValue);
viewPaneContainer.focus();
}
}

View File

@@ -60,6 +60,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr
import { DragAndDropObserver } from 'vs/workbench/browser/dnd';
import { URI } from 'vs/base/common/uri';
import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
const NonEmptyWorkspaceContext = new RawContextKey<boolean>('nonEmptyWorkspace', false);
const DefaultViewsContext = new RawContextKey<boolean>('defaultExtensionViews', true);
@@ -128,14 +129,18 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio
if (this.extensionManagementServerService.localExtensionManagementServer) {
servers.push(this.extensionManagementServerService.localExtensionManagementServer);
}
if (this.extensionManagementServerService.webExtensionManagementServer) {
servers.push(this.extensionManagementServerService.webExtensionManagementServer);
}
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
servers.push(this.extensionManagementServerService.remoteExtensionManagementServer);
}
if (servers.length === 0 && this.extensionManagementServerService.webExtensionManagementServer) {
servers.push(this.extensionManagementServerService.webExtensionManagementServer);
}
const getViewName = (viewTitle: string, server: IExtensionManagementServer): string => {
return servers.length > 1 ? `${server.label} - ${viewTitle}` : viewTitle;
if (servers.length) {
const serverLabel = server === this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer ? localize('local', "Local") : server.label;
return servers.length > 1 ? `${serverLabel} - ${viewTitle}` : viewTitle;
}
return viewTitle;
};
for (const server of servers) {
const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server);
@@ -350,6 +355,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
@IInstantiationService instantiationService: IInstantiationService,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@INotificationService private readonly notificationService: INotificationService,
@IViewletService private readonly viewletService: IViewletService,
@@ -520,14 +527,19 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
]);
if (this.extensionGalleryService.isEnabled()) {
filterActions.splice(0, 0, ...[
const galleryFilterActions = [
// this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.featured', localize('featured filter', "Featured"), '@featured'), // {{SQL CARBON EDIT}}
// this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.popular', localize('most popular filter', "Most Popular"), '@popular'), // {{SQL CARBON EDIT}}
this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.recommended', localize('most popular recommended', "Recommended"), '@recommended'),
// this.instantiationService.createInstance(RecentlyPublishedExtensionsAction, RecentlyPublishedExtensionsAction.ID, localize('recently published filter', "Recently Published")), // {{SQL CARBON EDIT}}
new Separator(),
new SubmenuAction('workbench.extensions.action.filterExtensionsByCategory', localize('filter by category', "Category"), EXTENSION_CATEGORIES.map(category => this.instantiationService.createInstance(SearchCategoryAction, `extensions.actions.searchByCategory.${category}`, category, category))),
new Separator(),
]);
];
if (this.extensionManagementServerService.webExtensionManagementServer || !this.environmentService.isBuilt) {
galleryFilterActions.splice(4, 0, this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.web', localize('web filter', "Web"), '@web'));
}
filterActions.splice(0, 0, ...galleryFilterActions);
filterActions.push(...[
new Separator(),
new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), this.sortActions),
@@ -582,6 +594,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
.replace(/@tag:/g, 'tag:')
.replace(/@ext:/g, 'ext:')
.replace(/@featured/g, 'featured')
.replace(/@web/g, 'tag:"__web_extension"')
.replace(/@popular/g, '@sort:installs')
: '';
}

View File

@@ -9,7 +9,7 @@ import { assign } from 'vs/base/common/objects';
import { Event, Emitter } from 'vs/base/common/event';
import { isPromiseCanceledError, getErrorMessage } from 'vs/base/common/errors';
import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging';
import { SortBy, SortOrder, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { SortBy, SortOrder, IQueryOptions, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementServer, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionRecommendation, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
@@ -17,7 +17,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { append, $, toggleClass, addClass } from 'vs/base/browser/dom';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Delegate, Renderer, IExtensionsViewState } from 'vs/workbench/contrib/extensions/browser/extensionsList';
import { IExtension, IExtensionsWorkbenchService, ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions';
import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery';
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
import { IThemeService } from 'vs/platform/theme/common/themeService';
@@ -25,13 +25,13 @@ import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { WorkbenchPagedList, ListResourceNavigator } from 'vs/platform/list/browser/listService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { distinct, coalesce, firstIndex } from 'vs/base/common/arrays';
import { coalesce, distinct, flatten, firstIndex } from 'vs/base/common/arrays'; // {{ SQL CARBON EDIT }}
import { IExperimentService, IExperiment, ExperimentActionType } from 'vs/workbench/contrib/experiments/common/experimentService';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list';
@@ -53,6 +53,10 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
// Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions
const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result'];
type WorkspaceRecommendationsClassification = {
count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', 'isMeasurement': true };
};
class ExtensionsViewState extends Disposable implements IExtensionsViewState {
private readonly _onFocus: Emitter<IExtension> = this._register(new Emitter<IExtension>());
@@ -98,12 +102,13 @@ export class ExtensionsListView extends ViewPane {
@IThemeService themeService: IThemeService,
@IExtensionService private readonly extensionService: IExtensionService,
@IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionRecommendationsService protected tipsService: IExtensionRecommendationsService,
@IExtensionRecommendationsService protected extensionRecommendationsService: IExtensionRecommendationsService,
@ITelemetryService telemetryService: ITelemetryService,
@IConfigurationService configurationService: IConfigurationService,
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IExperimentService private readonly experimentService: IExperimentService,
@IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService,
@IExtensionManagementService protected readonly extensionManagementService: IExtensionManagementService,
@IProductService protected readonly productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@@ -471,14 +476,9 @@ export class ExtensionsListView extends ViewPane {
}
// {{SQL CARBON EDIT}} - End
if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) {
return this.getWorkspaceRecommendationsModel(query, options, token);
} else if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {
return this.getKeymapRecommendationsModel(query, options, token);
} else if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) {
return this.getAllRecommendationsModel(query, options, token);
} else if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {
return this.getRecommendationsModel(query, options, token);
if (this.isRecommendationsQuery(query)) {
return this.queryRecommendations(query, options, token);
} else if (ExtensionsListView.isAllMarketplaceExtensionsQuery(query.value)) { // {{SQL CARBON EDIT}} add if
return this.getAllMarketplaceModel(query, options, token);
}
@@ -557,51 +557,6 @@ export class ExtensionsListView extends ViewPane {
return extensions;
}
// Get All types of recommendations, trimmed to show a max of 8 at any given time
private getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended:all/g, '').replace(/@recommended/g, '').trim().toLowerCase();
return this.extensionsWorkbenchService.queryLocal(this.server)
.then(result => result.filter(e => e.type === ExtensionType.User))
.then(local => {
const fileBasedRecommendations = this.tipsService.getFileBasedRecommendations();
const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations();
const othersPromise = this.tipsService.getOtherRecommendations();
const workspacePromise = this.tipsService.getWorkspaceRecommendations();
const importantRecommendationsPromise = this.tipsService.getImportantRecommendations();
return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise, importantRecommendationsPromise])
.then(([others, workspaceRecommendations, configBasedRecommendations, importantRecommendations]) => {
const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, workspaceRecommendations);
const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason();
/* __GDPR__
"extensionAllRecommendations:open" : {
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"recommendations": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('extensionAllRecommendations:open', {
count: names.length,
recommendations: names.map(id => {
return {
id,
recommendationReason: recommendationsWithReason[id.toLowerCase()].reasonId
};
})
});
if (!names.length) {
return Promise.resolve(new PagedModel([]));
}
options.source = 'recommendations-all';
return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token)
.then(pager => {
this.sortFirstPage(pager, names);
return this.getPagedModel(pager || []);
});
});
});
}
private async getCuratedModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/curated:/g, '').trim();
const names = await this.experimentService.getCuratedExtensionsList(value);
@@ -614,55 +569,13 @@ export class ExtensionsListView extends ViewPane {
return new PagedModel([]);
}
// Get All types of recommendations other than Workspace recommendations, trimmed to show a max of 8 at any given time
private getRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended/g, '').trim().toLowerCase();
return this.extensionsWorkbenchService.queryLocal(this.server)
.then(result => result.filter(e => e.type === ExtensionType.User))
.then(local => {
let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations();
const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations();
const othersPromise = this.tipsService.getOtherRecommendations();
const workspacePromise = this.tipsService.getWorkspaceRecommendations();
const importantRecommendationsPromise = this.tipsService.getImportantRecommendations();
return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise, importantRecommendationsPromise])
.then(([others, workspaceRecommendations, configBasedRecommendations, importantRecommendations]) => {
configBasedRecommendations = configBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId));
fileBasedRecommendations = fileBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId));
others = others.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId));
const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, []);
const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason();
/* __GDPR__
"extensionRecommendations:open" : {
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"recommendations": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('extensionRecommendations:open', {
count: names.length,
recommendations: names.map(id => {
return {
id,
recommendationReason: recommendationsWithReason[id.toLowerCase()].reasonId
};
})
});
if (!names.length) {
return Promise.resolve(new PagedModel([]));
}
options.source = 'recommendations';
return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token)
.then(pager => {
this.sortFirstPage(pager, names);
return this.getPagedModel(pager || []);
});
});
});
private isRecommendationsQuery(query: Query): boolean {
return ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)
|| ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)
|| ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)
|| /@recommended:all/i.test(query.value)
|| ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)
|| ExtensionsListView.isRecommendedExtensionsQuery(query.value);
}
// {{SQL CARBON EDIT}}
@@ -671,7 +584,7 @@ export class ExtensionsListView extends ViewPane {
return this.extensionsWorkbenchService.queryLocal()
.then(result => result.filter(e => e.type === ExtensionType.User))
.then(local => {
return this.tipsService.getOtherRecommendations().then((recommmended) => {
return this.extensionRecommendationsService.getOtherRecommendations().then((recommmended) => {
const installedExtensions = local.map(x => `${x.publisher}.${x.name}`);
options = assign(options, { text: value, source: 'searchText' });
return this.extensionsWorkbenchService.queryGallery(options, token).then((pager) => {
@@ -709,7 +622,7 @@ export class ExtensionsListView extends ViewPane {
return this.extensionsWorkbenchService.queryLocal()
.then(result => result.filter(e => e.type === ExtensionType.User))
.then(local => {
return this.tipsService.getRecommendedExtensionsByScenario(scenarioType).then((recommmended) => {
return this.extensionRecommendationsService.getRecommendedExtensionsByScenario(scenarioType).then((recommmended) => {
const installedExtensions = local.map(x => `${x.publisher}.${x.name}`);
return this.extensionsWorkbenchService.queryGallery(token).then((pager) => {
// filter out installed extensions and the extensions not in the recommended list
@@ -726,88 +639,129 @@ export class ExtensionsListView extends ViewPane {
}
// {{SQL CARBON EDIT}} - End
// Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions
private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, importantRecommendations: IExtensionRecommendation[], fileBasedRecommendations: IExtensionRecommendation[], configBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workspaceRecommendations: IExtensionRecommendation[]): string[] {
const totalCount = 10;
workspaceRecommendations = workspaceRecommendations
.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
importantRecommendations = importantRecommendations
.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
configBasedRecommendations = configBasedRecommendations
.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
fileBasedRecommendations = fileBasedRecommendations.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId)
&& configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
otherRecommendations = otherRecommendations.filter(recommendation => {
return !this.isRecommendationInstalled(recommendation, installedExtensions)
&& fileBasedRecommendations.every(fileBasedRecommendation => fileBasedRecommendation.extensionId !== recommendation.extensionId)
&& workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId)
&& importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId)
&& configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId)
&& recommendation.extensionId.toLowerCase().indexOf(value) > -1;
});
const otherCount = Math.min(2, otherRecommendations.length);
const fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workspaceRecommendations.length - importantRecommendations.length - configBasedRecommendations.length - otherCount);
const recommendations = [...workspaceRecommendations, ...importantRecommendations, ...configBasedRecommendations];
recommendations.push(...fileBasedRecommendations.splice(0, fileBasedCount));
recommendations.push(...otherRecommendations.splice(0, otherCount));
return distinct(recommendations.map(({ extensionId }) => extensionId));
}
private isRecommendationInstalled(recommendation: IExtensionRecommendation, installed: IExtension[]): boolean {
return installed.some(i => areSameExtensions(i.identifier, { id: recommendation.extensionId }));
}
private getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase();
return this.tipsService.getWorkspaceRecommendations()
.then(recommendations => {
const names = recommendations.map(({ extensionId }) => extensionId).filter(name => name.toLowerCase().indexOf(value) > -1);
/* __GDPR__
"extensionWorkspaceRecommendations:open" : {
"count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
this.telemetryService.publicLog('extensionWorkspaceRecommendations:open', { count: names.length });
if (!names.length) {
return Promise.resolve(new PagedModel([]));
}
options.source = 'recommendations-workspace';
return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token)
.then(pager => this.getPagedModel(pager || []));
});
}
private getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase();
const names: string[] = this.tipsService.getKeymapRecommendations().map(({ extensionId }) => extensionId)
.filter(extensionId => extensionId.toLowerCase().indexOf(value) > -1);
if (!names.length) {
return Promise.resolve(new PagedModel([]));
private async queryRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
// Workspace recommendations
if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) {
return this.getWorkspaceRecommendationsModel(query, options, token);
}
options.source = 'recommendations-keymaps';
return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token)
.then(result => this.getPagedModel(result));
// Keymap recommendations
if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) {
return this.getKeymapRecommendationsModel(query, options, token);
}
// Exe recommendations
if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) {
return this.getExeRecommendationsModel(query, options, token);
}
// All recommendations
if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) {
return this.getAllRecommendationsModel(query, options, token);
}
// Other recommendations
if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {
return this.getOtherRecommendationsModel(query, options, token);
}
return new PagedModel([]);
}
protected async getInstallableRecommendations(recommendations: IExtensionRecommendation[], options: IQueryOptions, token: CancellationToken): Promise<IExtension[]> {
const extensions: IExtension[] = [];
if (recommendations.length) {
const names = recommendations.map(({ extensionId }) => extensionId);
const pager = await this.extensionsWorkbenchService.queryGallery({ ...options, names, pageSize: names.length }, token);
for (const extension of pager.firstPage) {
if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) {
extensions.push(extension);
}
}
}
return extensions;
}
protected async getWorkspaceRecommendations(): Promise<IExtensionRecommendation[]> {
const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations();
const { important } = await this.extensionRecommendationsService.getConfigBasedRecommendations();
for (const configBasedRecommendation of important) {
if (recommendations.some(r => r.extensionId !== configBasedRecommendation.extensionId)) {
recommendations.push(configBasedRecommendation);
}
}
return recommendations;
}
private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase();
const recommendations = await this.getWorkspaceRecommendations();
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token))
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
this.telemetryService.publicLog2<{ count: number }, WorkspaceRecommendationsClassification>('extensionWorkspaceRecommendations:open', { count: installableRecommendations.length });
const result: IExtension[] = coalesce(recommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
return new PagedModel(result);
}
private async getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase();
const recommendations = this.extensionRecommendationsService.getKeymapRecommendations();
const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-keymaps' }, token))
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
return new PagedModel(installableRecommendations);
}
private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase();
const { important, others } = await this.extensionRecommendationsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe);
const installableRecommendations = await this.getInstallableRecommendations([...important, ...others], { ...options, source: 'recommendations-exe' }, token);
return new PagedModel(installableRecommendations);
}
private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const value = query.value.replace(/@recommended/g, '').trim().toLowerCase();
const local = (await this.extensionsWorkbenchService.queryLocal(this.server))
.filter(e => e.type === ExtensionType.User)
.map(e => e.identifier.id.toLowerCase());
const workspaceRecommendations = (await this.getWorkspaceRecommendations())
.map(r => r.extensionId.toLowerCase());
const otherRecommendations = distinct(
flatten(await Promise.all([
// Order is important
this.extensionRecommendationsService.getImportantRecommendations(),
this.extensionRecommendationsService.getFileBasedRecommendations(),
this.extensionRecommendationsService.getOtherRecommendations()
])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase())
), r => r.extensionId.toLowerCase());
const installableRecommendations = (await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token))
.filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1);
const result: IExtension[] = coalesce(otherRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
return new PagedModel(result);
}
// Get All types of recommendations, trimmed to show a max of 8 at any given time
private async getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise<IPagedModel<IExtension>> {
const local = (await this.extensionsWorkbenchService.queryLocal(this.server))
.filter(e => e.type === ExtensionType.User)
.map(e => e.identifier.id.toLowerCase());
const allRecommendations = distinct(
flatten(await Promise.all([
// Order is important
this.getWorkspaceRecommendations(),
this.extensionRecommendationsService.getImportantRecommendations(),
this.extensionRecommendationsService.getFileBasedRecommendations(),
this.extensionRecommendationsService.getOtherRecommendations()
])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase())
), r => r.extensionId.toLowerCase());
const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token);
const result: IExtension[] = coalesce(allRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id }))));
return new PagedModel(result.slice(0, 8));
}
// Sorts the firstPage of the pager in the same order as given array of extension ids
@@ -942,6 +896,10 @@ export class ExtensionsListView extends ViewPane {
return /@recommended:workspace/i.test(query);
}
static isExeRecommendedExtensionsQuery(query: string): boolean {
return /@exe:.+/i.test(query);
}
static isKeymapsRecommendedExtensionsQuery(query: string): boolean {
return /@recommended:keymaps/i.test(query);
}
@@ -983,6 +941,7 @@ export class ServerExtensionsView extends ExtensionsListView {
@IExperimentService experimentService: IExperimentService,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService,
@IExtensionManagementService extensionManagementService: IExtensionManagementService,
@IProductService productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService,
@IMenuService menuService: IMenuService,
@@ -991,7 +950,9 @@ export class ServerExtensionsView extends ExtensionsListView {
@IPreferencesService preferencesService: IPreferencesService,
) {
options.server = server;
super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService, telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService, openerService, preferencesService);
super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService,
telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, extensionManagementService, productService,
contextKeyService, viewDescriptorService, menuService, openerService, preferencesService);
this._register(onDidChangeTitle(title => this.updateTitle(title)));
}
@@ -1076,7 +1037,7 @@ export class DefaultRecommendedExtensionsView extends ExtensionsListView {
renderBody(container: HTMLElement): void {
super.renderBody(container);
this._register(this.tipsService.onRecommendationChange(() => {
this._register(this.extensionRecommendationsService.onRecommendationChange(() => {
this.show('');
}));
}
@@ -1101,7 +1062,7 @@ export class RecommendedExtensionsView extends ExtensionsListView {
renderBody(container: HTMLElement): void {
super.renderBody(container);
this._register(this.tipsService.onRecommendationChange(() => {
this._register(this.extensionRecommendationsService.onRecommendationChange(() => {
this.show('');
}));
}
@@ -1114,20 +1075,18 @@ export class RecommendedExtensionsView extends ExtensionsListView {
export class WorkspaceRecommendedExtensionsView extends ExtensionsListView {
private readonly recommendedExtensionsQuery = '@recommended:workspace';
private installAllAction: InstallWorkspaceRecommendedExtensionsAction | undefined;
private installAllAction: Action | undefined;
renderBody(container: HTMLElement): void {
super.renderBody(container);
this._register(this.tipsService.onRecommendationChange(() => this.update()));
this._register(this.extensionsWorkbenchService.onChange(() => this.setRecommendationsToInstall()));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.update()));
this._register(this.extensionRecommendationsService.onRecommendationChange(() => this.show(this.recommendedExtensionsQuery)));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery)));
}
getActions(): IAction[] {
if (!this.installAllAction) {
this.installAllAction = this._register(this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, []));
this.installAllAction.class = 'codicon codicon-cloud-download';
this.installAllAction = this._register(new Action('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), 'codicon codicon-cloud-download', false, () => this.installWorkspaceRecommendations()));
}
const configureWorkspaceFolderAction = this._register(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL));
@@ -1139,33 +1098,28 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView {
let shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace';
let model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery));
this.setExpanded(model.length > 0);
await this.setRecommendationsToInstall();
return model;
}
private update(): void {
this.show(this.recommendedExtensionsQuery);
this.setRecommendationsToInstall();
}
private async setRecommendationsToInstall(): Promise<void> {
const recommendations = await this.getRecommendationsToInstall();
const installableRecommendations = await this.getInstallableWorkspaceRecommendations();
if (this.installAllAction) {
this.installAllAction.recommendations = recommendations.map(({ extensionId }) => extensionId);
this.installAllAction.enabled = installableRecommendations.length > 0;
}
}
private getRecommendationsToInstall(): Promise<IExtensionRecommendation[]> {
return this.tipsService.getWorkspaceRecommendations()
.then(recommendations => recommendations.filter(({ extensionId }) => {
const extension = this.extensionsWorkbenchService.local.filter(i => areSameExtensions({ id: extensionId }, i.identifier))[0];
if (!extension
|| !extension.local
|| extension.state !== ExtensionState.Installed
|| extension.enablementState === EnablementState.DisabledByExtensionKind
) {
return true;
}
return false;
}));
private async getInstallableWorkspaceRecommendations() {
const installed = (await this.extensionsWorkbenchService.queryLocal())
.filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
const recommendations = (await this.getWorkspaceRecommendations())
.filter(({ extensionId }) => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier)));
return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None);
}
private async installWorkspaceRecommendations(): Promise<void> {
const installableRecommendations = await this.getInstallableWorkspaceRecommendations();
await Promise.all(installableRecommendations.map(extension => this.extensionManagementService.installFromGallery(extension.gallery!)));
}
}

View File

@@ -235,7 +235,11 @@ class Extension implements IExtension {
return Promise.resolve(null);
}
return Promise.resolve(this.local!.manifest);
if (this.local) {
return Promise.resolve(this.local.manifest);
}
return Promise.resolve(null);
}
hasReadme(): boolean {
@@ -694,9 +698,18 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
}
const extensionsToChoose = enabledExtensions.length ? enabledExtensions : extensions;
const manifest = extensionsToChoose.find(e => e.local && e.local.manifest)?.local?.manifest;
// Manifest is not found which should not happen.
// In which case return the first extension.
if (!manifest) {
return extensionsToChoose[0];
}
const extensionKinds = getExtensionKind(manifest, this.productService, this.configurationService);
let extension = extensionsToChoose.find(extension => {
for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) {
for (const extensionKind of extensionKinds) {
switch (extensionKind) {
case 'ui':
/* UI extension is chosen only if it is installed locally */
@@ -723,7 +736,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
if (!extension && this.extensionManagementServerService.localExtensionManagementServer) {
extension = extensionsToChoose.find(extension => {
for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) {
for (const extensionKind of extensionKinds) {
switch (extensionKind) {
case 'workspace':
/* Choose local workspace extension if exists */
@@ -745,7 +758,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
if (!extension && this.extensionManagementServerService.remoteExtensionManagementServer) {
extension = extensionsToChoose.find(extension => {
for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) {
for (const extensionKind of extensionKinds) {
switch (extensionKind) {
case 'web':
/* Choose remote web extension if exists */

View File

@@ -4,28 +4,26 @@
*--------------------------------------------------------------------------------------------*/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { ExtensionRecommendationSource, ExtensionRecommendationReason, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
import { CancellationToken } from 'vs/base/common/cancellation';
import { localize } from 'vs/nls';
import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IProductService } from 'vs/platform/product/common/productService';
import { ImportantExtensionTip, IProductService } from 'vs/platform/product/common/productService';
import { forEach, IStringDictionary } from 'vs/base/common/collections';
import { ITextModel } from 'vs/editor/common/model';
import { Schemas } from 'vs/base/common/network';
import { extname } from 'vs/base/common/resources';
import { basename, extname } from 'vs/base/common/resources';
import { match } from 'vs/base/common/glob';
import { URI } from 'vs/base/common/uri';
import { MIME_UNKNOWN, guessMimeTypes } from 'vs/base/common/mime';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { setImmediate } from 'vs/base/common/platform';
import { IModeService } from 'vs/editor/common/services/modeService';
type FileExtensionSuggestionClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
@@ -35,32 +33,34 @@ type FileExtensionSuggestionClassification = {
const recommendationsStorageKey = 'extensionsAssistant/recommendations';
const searchMarketplace = localize('searchMarketplace', "Search Marketplace");
const milliSecondsInADay = 1000 * 60 * 60 * 24;
const processedFileExtensions: string[] = [];
export class FileBasedRecommendations extends ExtensionRecommendations {
private readonly extensionTips: IStringDictionary<string> = Object.create(null);
private readonly importantExtensionTips: IStringDictionary<{ name: string; pattern: string; isExtensionPack?: boolean }> = Object.create(null);
private readonly extensionTips = new Map<string, string>();
private readonly importantExtensionTips = new Map<string, ImportantExtensionTip>();
private fileBasedRecommendationsByPattern: IStringDictionary<string[]> = Object.create(null);
private fileBasedRecommendations: IStringDictionary<{ recommendedTime: number, sources: ExtensionRecommendationSource[] }> = Object.create(null);
private readonly fileBasedRecommendationsByPattern = new Map<string, string[]>();
private readonly fileBasedRecommendationsByLanguage = new Map<string, string[]>();
private readonly fileBasedRecommendations = new Map<string, { recommendedTime: number, sources: ExtensionRecommendationSource[] }>();
private readonly processedFileExtensions: string[] = [];
private readonly processedLanguages: string[] = [];
get recommendations(): ReadonlyArray<ExtensionRecommendation> {
const recommendations: ExtensionRecommendation[] = [];
Object.keys(this.fileBasedRecommendations)
[...this.fileBasedRecommendations.keys()]
.sort((a, b) => {
if (this.fileBasedRecommendations[a].recommendedTime === this.fileBasedRecommendations[b].recommendedTime) {
if (this.importantExtensionTips[a]) {
if (this.fileBasedRecommendations.get(a)!.recommendedTime === this.fileBasedRecommendations.get(b)!.recommendedTime) {
if (this.importantExtensionTips.has(a)) {
return -1;
}
if (this.importantExtensionTips[b]) {
if (this.importantExtensionTips.has(b)) {
return 1;
}
}
return this.fileBasedRecommendations[a].recommendedTime > this.fileBasedRecommendations[b].recommendedTime ? -1 : 1;
return this.fileBasedRecommendations.get(a)!.recommendedTime > this.fileBasedRecommendations.get(b)!.recommendedTime ? -1 : 1;
})
.forEach(extensionId => {
for (const source of this.fileBasedRecommendations[extensionId].sources) {
for (const source of this.fileBasedRecommendations.get(extensionId)!.sources) {
recommendations.push({
extensionId,
source,
@@ -75,53 +75,62 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
}
get importantRecommendations(): ReadonlyArray<ExtensionRecommendation> {
return this.recommendations.filter(e => this.importantExtensionTips[e.extensionId]);
return this.recommendations.filter(e => this.importantExtensionTips.has(e.extensionId));
}
get otherRecommendations(): ReadonlyArray<ExtensionRecommendation> {
return this.recommendations.filter(e => !this.importantExtensionTips[e.extensionId]);
return this.recommendations.filter(e => !this.importantExtensionTips.has(e.extensionId));
}
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionService private readonly extensionService: IExtensionService,
@IViewletService private readonly viewletService: IViewletService,
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService,
@IProductService productService: IProductService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@INotificationService private readonly notificationService: INotificationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IStorageService private readonly storageService: IStorageService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
if (productService.extensionTips) {
forEach(productService.extensionTips, ({ key, value }) => this.extensionTips[key.toLowerCase()] = value);
forEach(productService.extensionTips, ({ key, value }) => this.extensionTips.set(key.toLowerCase(), value));
}
if (productService.extensionImportantTips) {
forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips[key.toLowerCase()] = value);
forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips.set(key.toLowerCase(), value));
}
}
protected async doActivate(): Promise<void> {
await this.extensionService.whenInstalledExtensionsRegistered();
const allRecommendations: string[] = [];
// group extension recommendations by pattern, like {**/*.md} -> [ext.foo1, ext.bar2]
forEach(this.extensionTips, ({ key: extensionId, value: pattern }) => {
const ids = this.fileBasedRecommendationsByPattern[pattern] || [];
for (const [extensionId, pattern] of this.extensionTips) {
const ids = this.fileBasedRecommendationsByPattern.get(pattern) || [];
ids.push(extensionId);
this.fileBasedRecommendationsByPattern[pattern] = ids;
this.fileBasedRecommendationsByPattern.set(pattern, ids);
allRecommendations.push(extensionId);
});
forEach(this.importantExtensionTips, ({ key: extensionId, value }) => {
const ids = this.fileBasedRecommendationsByPattern[value.pattern] || [];
ids.push(extensionId);
this.fileBasedRecommendationsByPattern[value.pattern] = ids;
}
for (const [extensionId, value] of this.importantExtensionTips) {
if (value.pattern) {
const ids = this.fileBasedRecommendationsByPattern.get(value.pattern) || [];
ids.push(extensionId);
this.fileBasedRecommendationsByPattern.set(value.pattern, ids);
}
if (value.languages) {
for (const language of value.languages) {
const ids = this.fileBasedRecommendationsByLanguage.get(language) || [];
ids.push(extensionId);
this.fileBasedRecommendationsByLanguage.set(language, ids);
}
}
allRecommendations.push(extensionId);
});
}
const cachedRecommendations = this.getCachedRecommendations();
const now = Date.now();
@@ -129,12 +138,17 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
forEach(cachedRecommendations, ({ key, value }) => {
const diff = (now - value) / milliSecondsInADay;
if (diff <= 7 && allRecommendations.indexOf(key) > -1) {
this.fileBasedRecommendations[key] = { recommendedTime: value, sources: ['cached'] };
this.fileBasedRecommendations.set(key.toLowerCase(), { recommendedTime: value, sources: ['cached'] });
}
});
this._register(this.modelService.onModelAdded(this.promptRecommendationsForModel, this));
this.modelService.getModels().forEach(model => this.promptRecommendationsForModel(model));
this._register(this.modelService.onModelAdded(model => this.onModelAdded(model)));
this.modelService.getModels().forEach(model => this.onModelAdded(model));
}
private onModelAdded(model: ITextModel): void {
this.promptRecommendationsForModel(model);
this._register(model.onDidChangeLanguage(() => this.promptRecommendationsForModel(model)));
}
/**
@@ -144,63 +158,72 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
private promptRecommendationsForModel(model: ITextModel): void {
const uri = model.uri;
const supportedSchemes = [Schemas.untitled, Schemas.file, Schemas.vscodeRemote];
if (!uri || supportedSchemes.indexOf(uri.scheme) === -1) {
if (!uri || !supportedSchemes.includes(uri.scheme)) {
return;
}
let fileExtension = extname(uri);
if (fileExtension) {
if (processedFileExtensions.indexOf(fileExtension) > -1) {
return;
}
processedFileExtensions.push(fileExtension);
const language = model.getLanguageIdentifier().language;
const fileExtension = extname(uri);
if (this.processedLanguages.includes(language) && this.processedFileExtensions.includes(fileExtension)) {
return;
}
this.processedLanguages.push(language);
this.processedFileExtensions.push(fileExtension);
// re-schedule this bit of the operation to be off the critical path - in case glob-match is slow
setImmediate(() => this.promptRecommendations(uri, fileExtension));
setImmediate(() => this.promptRecommendations(uri, language, fileExtension));
}
private async promptRecommendations(uri: URI, fileExtension: string): Promise<void> {
const recommendationsToPrompt: string[] = [];
forEach(this.fileBasedRecommendationsByPattern, ({ key: pattern, value: extensionIds }) => {
if (match(pattern, uri.toString())) {
for (const extensionId of extensionIds) {
// Add to recommendation to prompt if it is an important tip
// Only prompt if the pattern matches the extensionImportantTips pattern
// Otherwise, assume pattern is from extensionTips, which means it should be a file based "passive" recommendation
if (this.importantExtensionTips[extensionId]?.pattern === pattern) {
recommendationsToPrompt.push(extensionId);
}
// Update file based recommendations
const filedBasedRecommendation = this.fileBasedRecommendations[extensionId] || { recommendedTime: Date.now(), sources: [] };
filedBasedRecommendation.recommendedTime = Date.now();
if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) {
filedBasedRecommendation.sources.push(uri);
}
this.fileBasedRecommendations[extensionId.toLowerCase()] = filedBasedRecommendation;
private async promptRecommendations(uri: URI, language: string, fileExtension: string): Promise<void> {
const importantRecommendations: string[] = (this.fileBasedRecommendationsByLanguage.get(language) || []).filter(extensionId => this.importantExtensionTips.has(extensionId));
let languageName: string | null = importantRecommendations.length ? this.modeService.getLanguageName(language) : null;
const fileBasedRecommendations: string[] = [...importantRecommendations];
for (let [pattern, extensionIds] of this.fileBasedRecommendationsByPattern) {
extensionIds = extensionIds.filter(extensionId => !importantRecommendations.includes(extensionId));
if (!extensionIds.length) {
continue;
}
if (!match(pattern, uri.toString())) {
continue;
}
for (const extensionId of extensionIds) {
fileBasedRecommendations.push(extensionId);
const importantExtensionTip = this.importantExtensionTips.get(extensionId);
if (importantExtensionTip && importantExtensionTip.pattern === pattern) {
importantRecommendations.push(extensionId);
}
}
});
}
// Update file based recommendations
for (const recommendation of fileBasedRecommendations) {
const filedBasedRecommendation = this.fileBasedRecommendations.get(recommendation) || { recommendedTime: Date.now(), sources: [] };
filedBasedRecommendation.recommendedTime = Date.now();
if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) {
filedBasedRecommendation.sources.push(uri);
}
this.fileBasedRecommendations.set(recommendation, filedBasedRecommendation);
}
this.storeCachedRecommendations();
if (this.hasToIgnoreRecommendationNotifications()) {
if (this.promptedExtensionRecommendations.hasToIgnoreRecommendationNotifications()) {
return;
}
const installed = await this.extensionsWorkbenchService.queryLocal();
if (await this.promptRecommendedExtensionForFileType(recommendationsToPrompt, installed)) {
if (importantRecommendations.length &&
await this.promptRecommendedExtensionForFileType(languageName || basename(uri), importantRecommendations, installed)) {
return;
}
if (fileExtension) {
fileExtension = fileExtension.substr(1); // Strip the dot
}
fileExtension = fileExtension.substr(1); // Strip the dot
if (!fileExtension) {
return;
}
await this.extensionService.whenInstalledExtensionsRegistered();
const mimeTypes = guessMimeTypes(uri);
if (mimeTypes.length !== 1 || mimeTypes[0] !== MIME_UNKNOWN) {
return;
@@ -209,9 +232,9 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
this.promptRecommendedExtensionForFileExtension(fileExtension, installed);
}
private async promptRecommendedExtensionForFileType(recommendations: string[], installed: IExtension[]): Promise<boolean> {
private async promptRecommendedExtensionForFileType(name: string, recommendations: string[], installed: IExtension[]): Promise<boolean> {
recommendations = this.filterIgnoredOrNotAllowed(recommendations);
recommendations = this.promptedExtensionRecommendations.filterIgnoredOrNotAllowed(recommendations);
if (recommendations.length === 0) {
return false;
}
@@ -222,17 +245,12 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
}
const extensionId = recommendations[0];
const entry = this.importantExtensionTips[extensionId];
const entry = this.importantExtensionTips.get(extensionId);
if (!entry) {
return false;
}
const extensionName = entry.name;
let message = localize('reallyRecommended2', "The '{0}' extension is recommended for this file type.", extensionName);
if (entry.isExtensionPack) {
message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", extensionName);
}
this.promptImportantExtensionsInstallNotification([extensionId], message);
this.promptedExtensionRecommendations.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install the recommended extensions for {0}?", name), `@id:${extensionId}`);
return true;
}
@@ -310,7 +328,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
private getCachedRecommendations(): IStringDictionary<number> {
let storedRecommendations = JSON.parse(this.storageService.get(recommendationsStorageKey, StorageScope.GLOBAL, '[]'));
if (Array.isArray<string>(storedRecommendations)) {
if (Array.isArray(storedRecommendations)) {
storedRecommendations = storedRecommendations.reduce((result, id) => { result[id] = Date.now(); return result; }, <IStringDictionary<number>>{});
}
const result: IStringDictionary<number> = {};
@@ -324,7 +342,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations {
private storeCachedRecommendations(): void {
const storedRecommendations: IStringDictionary<number> = {};
forEach(this.fileBasedRecommendations, ({ key, value }) => storedRecommendations[key] = value.recommendedTime);
this.fileBasedRecommendations.forEach((value, key) => storedRecommendations[key] = value.recommendedTime);
this.storageService.store(recommendationsStorageKey, JSON.stringify(storedRecommendations), StorageScope.GLOBAL);
}
}

View File

@@ -3,15 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IProductService } from 'vs/platform/product/common/productService';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
export class KeymapRecommendations extends ExtensionRecommendations {
@@ -19,16 +13,10 @@ export class KeymapRecommendations extends ExtensionRecommendations {
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IProductService private readonly productService: IProductService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
}
protected async doActivate(): Promise<void> {

View File

@@ -3,63 +3,45 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EXTENSION_IDENTIFIER_PATTERN, IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { EXTENSION_IDENTIFIER_PATTERN, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { IFileService } from 'vs/platform/files/common/files';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { distinct, flatten, coalesce } from 'vs/base/common/arrays';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason, IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { parse } from 'vs/base/common/json';
import { EXTENSIONS_CONFIG } from 'vs/workbench/contrib/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationToken } from 'vs/base/common/cancellation';
import { localize } from 'vs/nls';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
type ExtensionWorkspaceRecommendationsNotificationClassification = {
userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
const choiceNever = localize('neverShowAgain', "Don't Show Again");
const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore';
import { Emitter } from 'vs/base/common/event';
export class WorkspaceRecommendations extends ExtensionRecommendations {
private _recommendations: ExtensionRecommendation[] = [];
get recommendations(): ReadonlyArray<ExtensionRecommendation> { return this._recommendations; }
private _onDidChangeRecommendations = this._register(new Emitter<void>());
readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event;
private _ignoredRecommendations: string[] = [];
get ignoredRecommendations(): ReadonlyArray<string> { return this._ignoredRecommendations; }
constructor(
isExtensionAllowedToBeRecommended: (extensionId: string) => boolean,
promptedExtensionRecommendations: PromptedExtensionRecommendations,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
@ILogService private readonly logService: ILogService,
@IFileService private readonly fileService: IFileService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@INotificationService notificationService: INotificationService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@INotificationService private readonly notificationService: INotificationService,
) {
super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService);
super(promptedExtensionRecommendations);
}
protected async doActivate(): Promise<void> {
await this.fetch();
this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e)));
this.promptWorkspaceRecommendations();
}
/**
@@ -71,7 +53,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
const { invalidRecommendations, message } = await this.validateExtensions(extensionsConfigBySource.map(({ contents }) => contents));
if (invalidRecommendations.length) {
this.notificationService.warn(`The below ${invalidRecommendations.length} extension(s) in workspace recommendations have issues:\n${message}`);
this.notificationService.warn(`The ${invalidRecommendations.length} extension(s) below, in workspace recommendations have issues:\n${message}`);
}
this._ignoredRecommendations = [];
@@ -97,63 +79,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
}
}
private async promptWorkspaceRecommendations(): Promise<void> {
const allowedRecommendations = this.recommendations.filter(rec => this.isExtensionAllowedToBeRecommended(rec.extensionId));
if (allowedRecommendations.length === 0 || this.hasToIgnoreWorkspaceRecommendationNotifications()) {
return;
}
let installed = await this.extensionManagementService.getInstalled();
installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind
const recommendations = allowedRecommendations.filter(({ extensionId }) => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier)));
if (!recommendations.length) {
return;
}
return new Promise<void>(c => {
this.notificationService.prompt(
Severity.Info,
localize('workspaceRecommended', "This workspace has extension recommendations."),
[{
label: localize('installAll', "Install All"),
run: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' });
const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, recommendations.map(({ extensionId }) => extensionId));
installAllAction.run();
installAllAction.dispose();
c(undefined);
}
}, {
label: localize('showRecommendations', "Show Recommendations"),
run: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' });
const showAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations"));
showAction.run();
showAction.dispose();
c(undefined);
}
}, {
label: choiceNever,
isSecondary: true,
run: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' });
this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE);
c(undefined);
}
}],
{
sticky: true,
onCancel: () => {
this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' });
c(undefined);
}
}
);
});
}
private async fetchExtensionsConfigBySource(): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource }[]> {
const workspace = this.contextService.getWorkspace();
const result = await Promise.all([
@@ -235,7 +160,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
await this.fetch();
// Suggest only if at least one of the newly added recommendations was not suggested before
if (this._recommendations.some(current => oldWorkspaceRecommended.every(old => current.extensionId !== old.extensionId))) {
this.promptWorkspaceRecommendations();
this._onDidChangeRecommendations.fire();
}
}
}
@@ -250,8 +175,5 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
return null;
}
private hasToIgnoreWorkspaceRecommendationNotifications(): boolean {
return this.hasToIgnoreRecommendationNotifications() || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false);
}
}

View File

@@ -24,8 +24,11 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions';
import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
// Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded
registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, true);
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);

View File

@@ -8,7 +8,7 @@ import * as nls from 'vs/nls';
import * as os from 'os';
import { IProductService } from 'vs/platform/product/common/productService';
import { Action, IAction, Separator } from 'vs/base/common/actions';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
@@ -17,7 +17,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { IExtensionService, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
import { append, $, addClass, toggleClass, Dimension, clearNode } from 'vs/base/browser/dom';
import { append, $, reset, addClass, toggleClass, Dimension, clearNode } from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
@@ -38,8 +38,7 @@ import { randomPort } from 'vs/base/node/ports';
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ILabelService } from 'vs/platform/label/common/label';
import { renderCodicons } from 'vs/base/common/codicons';
import { escape } from 'vs/base/common/strings';
import { renderCodiconsAsElement } from 'vs/base/browser/codicons';
import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions';
@@ -100,7 +99,7 @@ interface IRuntimeExtension {
unresponsiveProfile?: IExtensionHostProfile;
}
export class RuntimeExtensionsEditor extends BaseEditor {
export class RuntimeExtensionsEditor extends EditorPane {
public static readonly ID: string = 'workbench.editor.runtimeExtensions';
@@ -233,11 +232,24 @@ export class RuntimeExtensionsEditor extends BaseEditor {
result = result.filter(element => element.status.activationTimes);
// bubble up extensions that have caused slowness
const isUnresponsive = (extension: IRuntimeExtension): boolean =>
extension.unresponsiveProfile === this._profileInfo;
const profileTime = (extension: IRuntimeExtension): number =>
extension.profileInfo?.totalTime ?? 0;
const activationTime = (extension: IRuntimeExtension): number =>
(extension.status.activationTimes?.codeLoadingTime ?? 0) +
(extension.status.activationTimes?.activateCallTime ?? 0);
result = result.sort((a, b) => {
if (a.unresponsiveProfile === this._profileInfo && !b.unresponsiveProfile) {
return -1;
} else if (!a.unresponsiveProfile && b.unresponsiveProfile === this._profileInfo) {
return 1;
if (isUnresponsive(a) || isUnresponsive(b)) {
return +isUnresponsive(b) - +isUnresponsive(a);
} else if (profileTime(a) || profileTime(b)) {
return profileTime(b) - profileTime(a);
} else if (activationTime(a) || activationTime(b)) {
return activationTime(b) - activationTime(a);
}
return a.originalIndex - b.originalIndex;
});
@@ -397,32 +409,28 @@ export class RuntimeExtensionsEditor extends BaseEditor {
clearNode(data.msgContainer);
if (this._extensionHostProfileService.getUnresponsiveProfile(element.description.identifier)) {
const el = $('span');
el.innerHTML = renderCodicons(escape(` $(alert) Unresponsive`));
const el = $('span', undefined, ...renderCodiconsAsElement(` $(alert) Unresponsive`));
el.title = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze.");
data.msgContainer.appendChild(el);
}
if (isNonEmptyArray(element.status.runtimeErrors)) {
const el = $('span');
el.innerHTML = renderCodicons(escape(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`));
const el = $('span', undefined, ...renderCodiconsAsElement(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`));
data.msgContainer.appendChild(el);
}
if (element.status.messages && element.status.messages.length > 0) {
const el = $('span');
el.innerHTML = renderCodicons(escape(`$(alert) ${element.status.messages[0].message}`));
const el = $('span', undefined, ...renderCodiconsAsElement(`$(alert) ${element.status.messages[0].message}`));
data.msgContainer.appendChild(el);
}
if (element.description.extensionLocation.scheme !== 'file') {
const el = $('span');
el.innerHTML = renderCodicons(escape(`$(remote) ${element.description.extensionLocation.authority}`));
const el = $('span', undefined, ...renderCodiconsAsElement(`$(remote) ${element.description.extensionLocation.authority}`));
data.msgContainer.appendChild(el);
const hostLabel = this._labelService.getHostLabel(REMOTE_HOST_SCHEME, this._environmentService.configuration.remoteAuthority);
if (hostLabel) {
el.innerHTML = renderCodicons(escape(`$(remote) ${hostLabel}`));
reset(el, ...renderCodiconsAsElement(`$(remote) ${hostLabel}`));
}
}

View File

@@ -33,8 +33,7 @@ import { IPager } from 'vs/base/common/paging';
import { assign } from 'vs/base/common/objects';
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { ConfigurationKey, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test';
import { IURLService } from 'vs/platform/url/common/url';
import { ITextModel } from 'vs/editor/common/model';
@@ -58,6 +57,8 @@ import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions
import { NoOpWorkspaceTagsService } from 'vs/workbench/contrib/tags/browser/workspaceTagsService';
import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags';
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
const mockExtensionGallery: IGalleryExtension[] = [
aGalleryExtension('MockExtension1', {
@@ -199,11 +200,18 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
testConfigurationService = new TestConfigurationService();
instantiationService.stub(IConfigurationService, testConfigurationService);
instantiationService.stub(INotificationService, new TestNotificationService());
instantiationService.stub(IExtensionManagementService, ExtensionManagementService);
instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event);
instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event);
instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event);
instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event);
instantiationService.stub(IExtensionManagementService, <Partial<IExtensionManagementService>>{
onInstallExtension: installEvent.event,
onDidInstallExtension: didInstallEvent.event,
onUninstallExtension: uninstallEvent.event,
onDidUninstallExtension: didUninstallEvent.event,
async getInstalled() { return []; },
async canInstall() { return true; },
async getExtensionsReport() { return []; },
});
instantiationService.stub(IExtensionService, <Partial<IExtensionService>>{
async whenInstalledExtensionsRegistered() { return true; }
});
instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService));
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IURLService, NativeURLService);
@@ -231,6 +239,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
experimentService = instantiationService.createInstance(TestExperimentService);
instantiationService.stub(IExperimentService, experimentService);
instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService));
instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService));
onModelAddedEvent = new Emitter<ITextModel>();
@@ -302,7 +311,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
function testNoPromptForValidRecommendations(recommendations: string[]) {
return setUpFolderWorkspace('myFolder', recommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length);
assert.ok(!prompted);
});
@@ -338,20 +347,18 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return testNoPromptForValidRecommendations([]);
});
test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', () => {
return setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
const recommendations = Object.keys(testObject.getAllRecommendationsWithReason());
test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', async () => {
await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions);
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
await testObject.activationPromise;
assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length);
mockTestData.validRecommendedExtensions.forEach(x => {
assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true);
});
assert.ok(prompted);
});
const recommendations = Object.keys(testObject.getAllRecommendationsWithReason());
assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length);
mockTestData.validRecommendedExtensions.forEach(x => {
assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true);
});
assert.ok(prompted);
});
test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if they are already installed', () => {
@@ -373,7 +380,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true });
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
assert.ok(!prompted);
});
});
@@ -391,7 +398,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been globally ignored
assert.ok(recommendations['ms-python.python']); // stored recommendation
@@ -409,7 +416,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been workspace ignored
assert.ok(recommendations['ms-python.python']); // stored recommendation
@@ -430,7 +437,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
@@ -449,7 +456,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getAllRecommendationsWithReason();
assert.ok(recommendations['ms-python.python']);
assert.ok(recommendations['mockpublisher1.mockextension1']);
@@ -486,7 +493,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
testObject.onRecommendationChange(changeHandlerTarget);
testObject.toggleIgnoredRecommendation(ignoredExtensionId, true);
await testObject.loadWorkspaceConfigPromise;
await testObject.activationPromise;
assert.ok(changeHandlerTarget.calledOnce);
assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: ignoredExtensionId.toLowerCase(), isRecommended: false }));
@@ -498,7 +505,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', []).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getFileBasedRecommendations();
assert.equal(recommendations.length, 2);
assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips
@@ -517,7 +524,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT}
return setUpFolderWorkspace('myFolder', []).then(() => {
testObject = instantiationService.createInstance(ExtensionRecommendationsService);
return testObject.loadWorkspaceConfigPromise.then(() => {
return testObject.activationPromise.then(() => {
const recommendations = testObject.getFileBasedRecommendations();
assert.equal(recommendations.length, 2);
assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips

View File

@@ -101,7 +101,7 @@ async function setupTest() {
instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService {
#localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' };
constructor() {
super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IConfigurationService), instantiationService.get(IProductService), instantiationService.get(ILogService), instantiationService.get(ILabelService));
super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IProductService), instantiationService.get(IConfigurationService), instantiationService.get(ILogService));
}
get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; }
set localExtensionManagementServer(server: IExtensionManagementServer) { }

View File

@@ -16,7 +16,6 @@ import {
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IURLService } from 'vs/platform/url/common/url';
@@ -40,13 +39,13 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browse
import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { ExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService';
import { IProductService } from 'vs/platform/product/common/productService';
import { ILabelService } from 'vs/platform/label/common/label';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { IMenuService } from 'vs/platform/actions/common/actions';
import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices';
import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
import { IProductService } from 'vs/platform/product/common/productService';
suite('ExtensionsListView Tests', () => {
@@ -68,6 +67,7 @@ suite('ExtensionsListView Tests', () => {
const workspaceRecommendationA = aGalleryExtension('workspace-recommendation-A');
const workspaceRecommendationB = aGalleryExtension('workspace-recommendation-B');
const configBasedRecommendationA = aGalleryExtension('configbased-recommendation-A');
const configBasedRecommendationB = aGalleryExtension('configbased-recommendation-B');
const fileBasedRecommendationA = aGalleryExtension('filebased-recommendation-A');
const fileBasedRecommendationB = aGalleryExtension('filebased-recommendation-B');
const otherRecommendationA = aGalleryExtension('other-recommendation-A');
@@ -89,11 +89,15 @@ suite('ExtensionsListView Tests', () => {
instantiationService.stub(ISharedProcessService, TestSharedProcessService);
instantiationService.stub(IExperimentService, ExperimentService);
instantiationService.stub(IExtensionManagementService, ExtensionManagementService);
instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event);
instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event);
instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event);
instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event);
instantiationService.stub(IExtensionManagementService, <Partial<IExtensionManagementService>>{
onInstallExtension: installEvent.event,
onDidInstallExtension: didInstallEvent.event,
onUninstallExtension: uninstallEvent.event,
onDidUninstallExtension: didUninstallEvent.event,
async getInstalled() { return []; },
async canInstall() { return true; },
async getExtensionsReport() { return []; },
});
instantiationService.stub(IRemoteAgentService, RemoteAgentService);
instantiationService.stub(IContextKeyService, new MockContextKeyService());
instantiationService.stub(IMenuService, new TestMenuService());
@@ -101,7 +105,7 @@ suite('ExtensionsListView Tests', () => {
instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService {
#localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' };
constructor() {
super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IConfigurationService), instantiationService.get(IProductService), instantiationService.get(ILogService), instantiationService.get(ILabelService));
super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IProductService), instantiationService.get(IConfigurationService), instantiationService.get(ILogService));
}
get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; }
set localExtensionManagementServer(server: IExtensionManagementServer) { }
@@ -123,9 +127,10 @@ suite('ExtensionsListView Tests', () => {
{ extensionId: workspaceRecommendationB.identifier.id }]);
},
getConfigBasedRecommendations() {
return Promise.resolve([
{ extensionId: configBasedRecommendationA.identifier.id }
]);
return Promise.resolve({
important: [{ extensionId: configBasedRecommendationA.identifier.id }],
others: [{ extensionId: configBasedRecommendationB.identifier.id }],
});
},
getImportantRecommendations(): Promise<IExtensionRecommendation[]> {
return Promise.resolve([]);
@@ -138,6 +143,7 @@ suite('ExtensionsListView Tests', () => {
},
getOtherRecommendations() {
return Promise.resolve([
{ extensionId: configBasedRecommendationB.identifier.id },
{ extensionId: otherRecommendationA.identifier.id }
]);
},
@@ -333,7 +339,8 @@ suite('ExtensionsListView Tests', () => {
test('Test @recommended:workspace query', () => {
const workspaceRecommendedExtensions = [
workspaceRecommendationA,
workspaceRecommendationB
workspaceRecommendationB,
configBasedRecommendationA,
];
const target = <SinonStub>instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...workspaceRecommendedExtensions));
@@ -351,9 +358,9 @@ suite('ExtensionsListView Tests', () => {
test('Test @recommended query', () => {
const allRecommendedExtensions = [
configBasedRecommendationA,
fileBasedRecommendationA,
fileBasedRecommendationB,
configBasedRecommendationB,
otherRecommendationA
];
const target = <SinonStub>instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...allRecommendedExtensions));
@@ -379,7 +386,8 @@ suite('ExtensionsListView Tests', () => {
configBasedRecommendationA,
fileBasedRecommendationA,
fileBasedRecommendationB,
otherRecommendationA
configBasedRecommendationB,
otherRecommendationA,
];
const target = <SinonStub>instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...allRecommendedExtensions));

View File

@@ -18,18 +18,6 @@ import { DEFAULT_TERMINAL_OSX } from 'vs/workbench/contrib/externalTerminal/node
const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console");
type LazyProcess = {
/**
* The lazy environment is a promise that resolves to `process.env`
* once the process is resolved. The use-case is VS Code running
* on Linux/macOS when being launched via a launcher. Then the env
* (as defined in .bashrc etc) isn't properly set and needs to be
* resolved lazy.
*/
lazyEnv: Promise<typeof process.env> | undefined;
};
export class WindowsExternalTerminalService implements IExternalTerminalService {
public _serviceBrand: undefined;
@@ -318,7 +306,6 @@ export class LinuxExternalTerminalService implements IExternalTerminalService {
LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise(async r => {
if (env.isLinux) {
const isDebian = await pfs.exists('/etc/debian_version');
await (process as unknown as LazyProcess).lazyEnv;
if (isDebian) {
r('x-terminal-emulator');
} else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') {

View File

@@ -12,7 +12,7 @@ import { Action } from 'vs/base/common/actions';
import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files';
import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles';
import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor';
import { EditorOptions, TextEditorOptions, IEditorInput } from 'vs/workbench/common/editor';
import { EditorOptions, TextEditorOptions, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor';
import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel';
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
@@ -91,13 +91,13 @@ export class TextFileEditor extends BaseTextEditor {
return this._input as FileEditorInput;
}
async setInput(input: FileEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
async setInput(input: FileEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
// Update/clear view settings if input changes
this.doSaveOrClearTextEditorViewState(this.input);
// Set input and resolve
await super.setInput(input, options, token);
await super.setInput(input, options, context, token);
try {
const resolvedModel = await input.resolve();
@@ -119,10 +119,12 @@ export class TextFileEditor extends BaseTextEditor {
const textEditor = assertIsDefined(this.getControl());
textEditor.setModel(textFileModel.textEditorModel);
// Always restore View State if any associated
const editorViewState = this.loadTextEditorViewState(input.resource);
if (editorViewState) {
textEditor.restoreViewState(editorViewState);
// Always restore View State if any associated and not disabled via settings
if (this.shouldRestoreTextEditorViewState(input, context)) {
const editorViewState = this.loadTextEditorViewState(input.resource);
if (editorViewState) {
textEditor.restoreViewState(editorViewState);
}
}
// TextOptions (avoiding instanceof here for a reason, do not change!)
@@ -242,7 +244,7 @@ export class TextFileEditor extends BaseTextEditor {
// If the user configured to not restore view state, we clear the view
// state unless the editor is still opened in the group.
if (!this.shouldRestoreViewState && (!this.group || !this.group.isOpened(input))) {
if (!this.shouldRestoreTextEditorViewState(input) && (!this.group || !this.group.isOpened(input))) {
this.clearTextEditorViewState([input.resource], this.group);
}

View File

@@ -643,7 +643,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
handler: async (accessor, args?: { viewType: string }) => {
const editorService = accessor.get(IEditorService);
if (args && args.viewType) { // {{SQL CARBON EDIT}} explicitly check for viewtype
if (typeof args?.viewType === 'string') {
const editorGroupsService = accessor.get(IEditorGroupsService);
const configurationService = accessor.get(IConfigurationService);
const quickInputService = accessor.get(IQuickInputService);

View File

@@ -34,7 +34,7 @@ import { fillResourceDataTransfers, CodeDataTransfers, extractResources, contain
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd';
import { Schemas } from 'vs/base/common/network';
import { DesktopDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { NativeDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { isMacintosh, isWeb } from 'vs/base/common/platform';
import { IDialogService, IConfirmation, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs';
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
@@ -839,11 +839,11 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
private handleDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
const fromDesktop = data instanceof DesktopDragAndDropData;
const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
const isNative = data instanceof NativeDragAndDropData;
const effect = (isNative || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
// Desktop DND
if (fromDesktop) {
// Native DND
if (isNative) {
if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) {
return false;
}
@@ -979,7 +979,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
}
// Desktop DND (Import file)
if (data instanceof DesktopDragAndDropData) {
if (data instanceof NativeDragAndDropData) {
if (isWeb) {
this.handleWebExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
} else {
@@ -992,7 +992,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
}
}
private async handleWebExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
private async handleWebExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
const items = (originalEvent.dataTransfer as unknown as IWebkitDataTransfer).items;
// Somehow the items thing is being modified at random, maybe as a security
@@ -1205,7 +1205,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
});
}
private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
private async handleExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
// Check for dropped external files to be folders
const droppedResources = extractResources(originalEvent, true);

View File

@@ -38,7 +38,7 @@ import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd';
import { memoize } from 'vs/base/common/decorators';
import { ElementsDragAndDropData, DesktopDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { ElementsDragAndDropData, NativeDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { URI } from 'vs/base/common/uri';
import { withUndefinedAsNull } from 'vs/base/common/types';
import { isWeb } from 'vs/base/common/platform';
@@ -667,7 +667,7 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop<OpenEditor | IEditorGro
}
onDragOver(data: IDragAndDropData, _targetElement: OpenEditor | IEditorGroup, _targetIndex: number, originalEvent: DragEvent): boolean | IListDragOverReaction {
if (data instanceof DesktopDragAndDropData) {
if (data instanceof NativeDragAndDropData) {
if (isWeb) {
return false; // dropping files into editor is unsupported on web
}

View File

@@ -33,7 +33,7 @@ export default class Messages {
public static MARKERS_PANEL_ACTION_TOOLTIP_FILTER: string = nls.localize('markers.panel.action.filter', "Filter Problems");
public static MARKERS_PANEL_ACTION_TOOLTIP_QUICKFIX: string = nls.localize('markers.panel.action.quickfix', "Show fixes");
public static MARKERS_PANEL_FILTER_ARIA_LABEL: string = nls.localize('markers.panel.filter.ariaLabel', "Filter Problems");
public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter. E.g.: text, **/*.ts, !**/node_modules/**");
public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter (e.g. text, **/*.ts, !**/node_modules/**)");
public static MARKERS_PANEL_FILTER_ERRORS: string = nls.localize('markers.panel.filter.errors', "errors");
public static MARKERS_PANEL_FILTER_WARNINGS: string = nls.localize('markers.panel.filter.warnings', "warnings");
public static MARKERS_PANEL_FILTER_INFOS: string = nls.localize('markers.panel.filter.infos', "infos");

View File

@@ -24,6 +24,7 @@ export const CELL_BOTTOM_MARGIN = 6;
// Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight`
export const EDITOR_TOP_PADDING = 12;
export const EDITOR_BOTTOM_PADDING = 4;
export const EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR = 12;
export const CELL_OUTPUT_PADDING = 14;

View File

@@ -18,7 +18,7 @@ import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/context
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { BaseCellRenderTemplate, CellEditState, CellFocusMode, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED, EXPAND_CELL_CONTENT_COMMAND_ID, NOTEBOOK_CELL_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { BaseCellRenderTemplate, CellEditState, CellFocusMode, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED, EXPAND_CELL_CONTENT_COMMAND_ID, NOTEBOOK_CELL_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { CellKind, CellUri, NotebookCellRunState, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
@@ -1296,7 +1296,7 @@ registerAction2(class extends NotebookCellAction {
editor.viewModel.notebookDocument.clearCellOutput(context.cell.handle);
if (context.cell.metadata && context.cell.metadata?.runState !== NotebookCellRunState.Running) {
context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, {
context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, {
runState: NotebookCellRunState.Idle,
runStartTime: undefined,
lastRunDuration: undefined,
@@ -1331,7 +1331,7 @@ export class ChangeCellLanguageAction extends NotebookCellAction {
const modelService = accessor.get(IModelService);
const quickInputService = accessor.get(IQuickInputService);
const providerLanguages = [...context.notebookEditor.viewModel!.notebookDocument.languages, 'markdown'];
const providerLanguages = [...context.notebookEditor.viewModel!.notebookDocument.resolvedLanguages, 'markdown'];
providerLanguages.forEach(languageId => {
let description: string;
if (context.cell.cellKind === CellKind.Markdown ? (languageId === 'markdown') : (languageId === context.cell.language)) {
@@ -1446,7 +1446,7 @@ registerAction2(class extends NotebookCellAction {
title: localize('notebookActions.splitCell', "Split Cell"),
menu: {
id: MenuId.NotebookCellTitle,
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_FOCUSED, InputFocusedContext),
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED),
order: CellToolbarOrder.SplitCell,
group: CELL_TITLE_CELL_GROUP_ID,
// alt: {
@@ -1456,7 +1456,7 @@ registerAction2(class extends NotebookCellAction {
},
icon: { id: 'codicon/split-vertical' },
keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, InputFocusedContext),
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED),
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH),
weight: KeybindingWeight.WorkbenchContrib
},
@@ -1554,7 +1554,7 @@ registerAction2(class extends NotebookCellAction {
title: localize('notebookActions.collapseCellInput', "Collapse Cell Input"),
keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED.toNegated(), InputFocusedContext.toNegated()),
primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_C),
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C),
weight: KeybindingWeight.WorkbenchContrib
},
menu: {
@@ -1566,7 +1566,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise<void> {
context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: true });
context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: true });
}
});
@@ -1577,7 +1577,7 @@ registerAction2(class extends NotebookCellAction {
title: localize('notebookActions.expandCellContent', "Expand Cell Content"),
keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED),
primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_C),
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C),
weight: KeybindingWeight.WorkbenchContrib
},
menu: {
@@ -1589,7 +1589,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise<void> {
context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: false });
context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: false });
}
});
@@ -1600,7 +1600,7 @@ registerAction2(class extends NotebookCellAction {
title: localize('notebookActions.collapseCellOutput', "Collapse Cell Output"),
keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED.toNegated(), InputFocusedContext.toNegated(), NOTEBOOK_CELL_HAS_OUTPUTS),
primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_O),
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_T),
weight: KeybindingWeight.WorkbenchContrib
},
menu: {
@@ -1612,7 +1612,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise<void> {
context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: true });
context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: true });
}
});
@@ -1623,7 +1623,7 @@ registerAction2(class extends NotebookCellAction {
title: localize('notebookActions.expandCellOutput', "Expand Cell Output"),
keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED),
primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_O),
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_T),
weight: KeybindingWeight.WorkbenchContrib
},
menu: {
@@ -1635,7 +1635,7 @@ registerAction2(class extends NotebookCellAction {
}
async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise<void> {
context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: false });
context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: false });
}
});

View File

@@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { INotebookEditor, INotebookEditorMouseEvent, ICellRange, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { INotebookEditor, INotebookEditorMouseEvent, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import * as DOM from 'vs/base/browser/dom';
import { CellFoldingState, FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
@@ -18,6 +18,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { getActiveNotebookEditor, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions';
import { localize } from 'vs/nls';
import { FoldingRegion } from 'vs/editor/contrib/folding/foldingRanges';
export class FoldingController extends Disposable implements INotebookEditorContribution {
static id: string = 'workbench.notebook.findController';
@@ -65,19 +66,31 @@ export class FoldingController extends Disposable implements INotebookEditorCont
this._updateEditorFoldingRanges();
}
setFoldingState(index: number, state: CellFoldingState) {
setFoldingStateDown(index: number, state: CellFoldingState, levels: number) {
const doCollapse = state === CellFoldingState.Collapsed;
let region = this._foldingModel!.getRegionAtLine(index + 1);
let regions: FoldingRegion[] = [];
if (region) {
if (region.isCollapsed !== doCollapse) {
regions.push(region);
}
if (levels > 1) {
let regionsInside = this._foldingModel!.getRegionsInside(region, (r, level: number) => r.isCollapsed !== doCollapse && level < levels);
regions.push(...regionsInside);
}
}
regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed));
this._updateEditorFoldingRanges();
}
setFoldingStateUp(index: number, state: CellFoldingState, levels: number) {
if (!this._foldingModel) {
return;
}
const range = this._foldingModel.regions.findRange(index + 1);
const startIndex = this._foldingModel.regions.getStartLineNumber(range) - 1;
if (startIndex !== index) {
return;
}
this._foldingModel.setCollapsed(range, state === CellFoldingState.Collapsed);
let regions = this._foldingModel.getAllRegionsAtLine(index + 1, (region, level) => region.isCollapsed !== (state === CellFoldingState.Collapsed) && level <= levels);
regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed));
this._updateEditorFoldingRanges();
}
@@ -121,7 +134,7 @@ export class FoldingController extends Disposable implements INotebookEditorCont
return;
}
this.setFoldingState(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed);
this.setFoldingStateUp(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed, 1);
}
return;
@@ -130,6 +143,10 @@ export class FoldingController extends Disposable implements INotebookEditorCont
registerNotebookContribution(FoldingController.id, FoldingController);
const NOTEBOOK_FOLD_COMMAND_LABEL = localize('fold.cell', "Fold Cell");
const NOTEBOOK_UNFOLD_COMMAND_LABEL = localize('unfold.cell', "Unfold Cell");
registerAction2(class extends Action2 {
constructor() {
super({
@@ -146,12 +163,39 @@ registerAction2(class extends Action2 {
secondary: [KeyCode.LeftArrow],
weight: KeybindingWeight.WorkbenchContrib
},
description: {
description: NOTEBOOK_FOLD_COMMAND_LABEL,
args: [
{
name: 'index',
description: 'The cell index',
schema: {
'type': 'object',
'required': ['index', 'direction'],
'properties': {
'index': {
'type': 'number'
},
'direction': {
'type': 'string',
'enum': ['up', 'down'],
'default': 'down'
},
'levels': {
'type': 'number',
'default': 1
},
}
}
}
]
},
precondition: NOTEBOOK_IS_ACTIVE_EDITOR,
f1: true
});
}
async run(accessor: ServicesAccessor): Promise<void> {
async run(accessor: ServicesAccessor, args?: { index: number, levels: number, direction: 'up' | 'down' }): Promise<void> {
const editorService = accessor.get(IEditorService);
const editor = getActiveNotebookEditor(editorService);
@@ -159,17 +203,27 @@ registerAction2(class extends Action2 {
return;
}
const activeCell = editor.getActiveCell();
if (!activeCell) {
return;
const levels = args && args.levels || 1;
const direction = args && args.direction === 'up' ? 'up' : 'down';
let index: number | undefined = undefined;
if (args) {
index = args.index;
} else {
const activeCell = editor.getActiveCell();
if (!activeCell) {
return;
}
index = editor.viewModel?.viewCells.indexOf(activeCell);
}
const controller = editor.getContribution<FoldingController>(FoldingController.id);
const index = editor.viewModel?.viewCells.indexOf(activeCell);
if (index !== undefined) {
controller.setFoldingState(index, CellFoldingState.Collapsed);
if (direction === 'up') {
controller.setFoldingStateUp(index, CellFoldingState.Collapsed, levels);
} else {
controller.setFoldingStateDown(index, CellFoldingState.Collapsed, levels);
}
}
}
});
@@ -178,7 +232,7 @@ registerAction2(class extends Action2 {
constructor() {
super({
id: 'notebook.unfold',
title: { value: localize('unfold.cell', "Unfold Cell"), original: 'Unfold Cell' },
title: { value: NOTEBOOK_UNFOLD_COMMAND_LABEL, original: 'Unfold Cell' },
category: NOTEBOOK_ACTIONS_CATEGORY,
keybinding: {
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
@@ -190,12 +244,39 @@ registerAction2(class extends Action2 {
secondary: [KeyCode.RightArrow],
weight: KeybindingWeight.WorkbenchContrib
},
description: {
description: NOTEBOOK_UNFOLD_COMMAND_LABEL,
args: [
{
name: 'index',
description: 'The cell index',
schema: {
'type': 'object',
'required': ['index', 'direction'],
'properties': {
'index': {
'type': 'number'
},
'direction': {
'type': 'string',
'enum': ['up', 'down'],
'default': 'down'
},
'levels': {
'type': 'number',
'default': 1
},
}
}
}
]
},
precondition: NOTEBOOK_IS_ACTIVE_EDITOR,
f1: true
});
}
async run(accessor: ServicesAccessor): Promise<void> {
async run(accessor: ServicesAccessor, args?: { index: number, levels: number, direction: 'up' | 'down' }): Promise<void> {
const editorService = accessor.get(IEditorService);
const editor = getActiveNotebookEditor(editorService);
@@ -203,17 +284,27 @@ registerAction2(class extends Action2 {
return;
}
const activeCell = editor.getActiveCell();
if (!activeCell) {
return;
const levels = args && args.levels || 1;
const direction = args && args.direction === 'up' ? 'up' : 'down';
let index: number | undefined = undefined;
if (args) {
index = args.index;
} else {
const activeCell = editor.getActiveCell();
if (!activeCell) {
return;
}
index = editor.viewModel?.viewCells.indexOf(activeCell);
}
const controller = editor.getContribution<FoldingController>(FoldingController.id);
const index = editor.viewModel?.viewCells.indexOf(activeCell);
if (index !== undefined) {
controller.setFoldingState(index, CellFoldingState.Expanded);
if (direction === 'up') {
controller.setFoldingStateUp(index, CellFoldingState.Expanded, levels);
} else {
controller.setFoldingStateDown(index, CellFoldingState.Expanded, levels);
}
}
}
});

View File

@@ -6,11 +6,14 @@
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { TrackedRangeStickiness } from 'vs/editor/common/model';
import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges';
import { FoldingRegion, FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges';
import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/syntaxRangeProvider';
import { ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon';
type RegionFilter = (r: FoldingRegion) => boolean;
type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean;
export class FoldingModel extends Disposable {
private _viewModel: NotebookViewModel | null = null;
@@ -73,7 +76,70 @@ export class FoldingModel extends Disposable {
this.recompute();
}
public setCollapsed(index: number, newState: boolean) {
getRegionAtLine(lineNumber: number): FoldingRegion | null {
if (this._regions) {
let index = this._regions.findRange(lineNumber);
if (index >= 0) {
return this._regions.toRegion(index);
}
}
return null;
}
getRegionsInside(region: FoldingRegion | null, filter?: RegionFilter | RegionFilterWithLevel): FoldingRegion[] {
let result: FoldingRegion[] = [];
let index = region ? region.regionIndex + 1 : 0;
let endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE;
if (filter && filter.length === 2) {
const levelStack: FoldingRegion[] = [];
for (let i = index, len = this._regions.length; i < len; i++) {
let current = this._regions.toRegion(i);
if (this._regions.getStartLineNumber(i) < endLineNumber) {
while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) {
levelStack.pop();
}
levelStack.push(current);
if (filter(current, levelStack.length)) {
result.push(current);
}
} else {
break;
}
}
} else {
for (let i = index, len = this._regions.length; i < len; i++) {
let current = this._regions.toRegion(i);
if (this._regions.getStartLineNumber(i) < endLineNumber) {
if (!filter || (filter as RegionFilter)(current)) {
result.push(current);
}
} else {
break;
}
}
}
return result;
}
getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] {
let result: FoldingRegion[] = [];
if (this._regions) {
let index = this._regions.findRange(lineNumber);
let level = 1;
while (index >= 0) {
let current = this._regions.toRegion(index);
if (!filter || filter(current, level)) {
result.push(current);
}
level++;
index = current.parentIndex;
}
}
return result;
}
setCollapsed(index: number, newState: boolean) {
this._regions.setCollapsed(index, newState);
}

View File

@@ -4,9 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor';
import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel';
@@ -17,7 +16,7 @@ function updateFoldingStateAtIndex(foldingModel: FoldingModel, index: number, co
}
suite('Notebook Folding', () => {
const instantiationService = new TestInstantiationService();
const instantiationService = setupInstantiationService();
const blukEditService = instantiationService.get(IBulkEditService);
const undoRedoService = instantiationService.stub(IUndoRedoService, () => { });
instantiationService.spy(IUndoRedoService, 'pushElement');
@@ -28,13 +27,13 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.1'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['## header 2.1', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingController = new FoldingModel();
@@ -57,13 +56,13 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.1\n# header3'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['## header 2.1\n# header3', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingController = new FoldingModel();
@@ -91,13 +90,13 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.1'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['## header 2.1', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingModel = new FoldingModel();
@@ -115,13 +114,13 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['## header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingModel = new FoldingModel();
@@ -140,13 +139,13 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingModel = new FoldingModel();
@@ -167,13 +166,13 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingModel = new FoldingModel();
@@ -224,18 +223,18 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingModel = new FoldingModel();
@@ -255,18 +254,18 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingModel = new FoldingModel();
@@ -290,18 +289,18 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingModel = new FoldingModel();
@@ -327,18 +326,18 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingModel = new FoldingModel();
@@ -366,18 +365,18 @@ suite('Notebook Folding', () => {
blukEditService,
undoRedoService,
[
[['# header 1'], 'markdown', CellKind.Markdown, [], {}],
[['body'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
[['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}],
[['body 2'], 'markdown', CellKind.Markdown, [], {}],
[['body 3'], 'markdown', CellKind.Markdown, [], {}],
[['## header 2.2'], 'markdown', CellKind.Markdown, [], {}],
[['var e = 7;'], 'markdown', CellKind.Markdown, [], {}],
['# header 1', 'markdown', CellKind.Markdown, [], {}],
['body', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}],
['body 2', 'markdown', CellKind.Markdown, [], {}],
['body 3', 'markdown', CellKind.Markdown, [], {}],
['## header 2.2', 'markdown', CellKind.Markdown, [], {}],
['var e = 7;', 'markdown', CellKind.Markdown, [], {}],
],
(editor, viewModel) => {
const foldingModel = new FoldingModel();

View File

@@ -17,8 +17,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle';
import { getDocumentFormattingEditsUntilResult, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { WorkspaceTextEdit } from 'vs/editor/common/modes';
import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
@@ -63,7 +62,7 @@ registerAction2(class extends Action2 {
const dispoables = new DisposableStore();
try {
const edits: WorkspaceTextEdit[] = [];
const edits: ResourceTextEdit[] = [];
for (const cell of notebook.cells) {
@@ -78,18 +77,13 @@ registerAction2(class extends Action2 {
);
if (formatEdits) {
formatEdits.forEach(edit => edits.push({
edit,
resource: model.uri,
modelVersionId: model.getVersionId()
}));
for (let edit of formatEdits) {
edits.push(new ResourceTextEdit(model.uri, edit, model.getVersionId()));
}
}
}
await bulkEditService.apply(
{ edits },
{ label: localize('label', "Format Notebook") }
);
await bulkEditService.apply(edits, { label: localize('label', "Format Notebook") });
} finally {
dispoables.dispose();

View File

@@ -0,0 +1,167 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { INotebookEditorContribution, INotebookEditor } from '../../notebookBrowser';
import { registerNotebookContribution } from '../../notebookEditorExtensions';
import { ISCMService } from 'vs/workbench/contrib/scm/common/scm';
import { createProviderComparer } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator';
import { first, ThrottledDelayer } from 'vs/base/common/async';
import { INotebookService } from '../../../common/notebookService';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { FileService } from 'vs/platform/files/common/fileService';
import { IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
export class SCMController extends Disposable implements INotebookEditorContribution {
static id: string = 'workbench.notebook.findController';
private _lastDecorationId: string[] = [];
private _localDisposable = new DisposableStore();
private _originalDocument: NotebookTextModel | undefined = undefined;
private _originalResourceDisposableStore = new DisposableStore();
private _diffDelayer = new ThrottledDelayer<void>(200);
private _lastVersion = -1;
constructor(
private readonly _notebookEditor: INotebookEditor,
@IFileService private readonly _fileService: FileService,
@ISCMService private readonly _scmService: ISCMService,
@INotebookService private readonly _notebookService: INotebookService
) {
super();
if (!this._notebookEditor.isEmbedded) {
this._register(this._notebookEditor.onDidChangeModel(() => {
this._localDisposable.clear();
this._originalResourceDisposableStore.clear();
this._diffDelayer.cancel();
this.update();
if (this._notebookEditor.textModel) {
this._localDisposable.add(this._notebookEditor.textModel.onDidChangeContent(() => {
this.update();
}));
this._localDisposable.add(this._notebookEditor.textModel.onDidChangeCells(() => {
this.update();
}));
}
}));
this._register(this._notebookEditor.onWillDispose(() => {
this._localDisposable.clear();
this._originalResourceDisposableStore.clear();
}));
this.update();
}
}
private async _resolveNotebookDocument(uri: URI, viewType: string) {
const providers = this._scmService.repositories.map(r => r.provider);
const rootedProviders = providers.filter(p => !!p.rootUri);
rootedProviders.sort(createProviderComparer(uri));
const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri)));
if (!result) {
this._originalDocument = undefined;
this._originalResourceDisposableStore.clear();
return;
}
if (result.toString() === this._originalDocument?.uri.toString()) {
// original document not changed
return;
}
this._originalResourceDisposableStore.add(this._fileService.watch(result));
this._originalResourceDisposableStore.add(this._fileService.onDidFilesChange(e => {
if (e.changes.find(change => change.resource.toString() === result.toString())) {
this._originalDocument = undefined;
this._originalResourceDisposableStore.clear();
this.update();
}
}));
const originalDocument = await this._notebookService.resolveNotebook(viewType, result, false);
this._originalResourceDisposableStore.add({
dispose: () => {
this._originalDocument?.dispose();
this._originalDocument = undefined;
}
});
this._originalDocument = originalDocument;
}
async update() {
if (!this._diffDelayer) {
return;
}
await this._diffDelayer
.trigger(async () => {
const modifiedDocument = this._notebookEditor.textModel;
if (!modifiedDocument) {
return;
}
if (this._lastVersion >= modifiedDocument.versionId) {
return;
}
this._lastVersion = modifiedDocument.versionId;
await this._resolveNotebookDocument(modifiedDocument.uri, modifiedDocument.viewType);
if (!this._originalDocument) {
this._clear();
return;
}
// const diff = new LcsDiff(new CellSequence(this._originalDocument), new CellSequence(modifiedDocument));
// const diffResult = diff.ComputeDiff(false);
// const decorations: INotebookDeltaDecoration[] = [];
// diffResult.changes.forEach(change => {
// if (change.originalLength === 0) {
// // doesn't exist in original
// for (let i = 0; i < change.modifiedLength; i++) {
// decorations.push({
// handle: modifiedDocument.cells[change.modifiedStart + i].handle,
// options: { gutterClassName: 'nb-gutter-cell-inserted' }
// });
// }
// } else {
// if (change.modifiedLength === 0) {
// // diff.deleteCount
// // removed from original
// } else {
// // modification
// for (let i = 0; i < change.modifiedLength; i++) {
// decorations.push({
// handle: modifiedDocument.cells[change.modifiedStart + i].handle,
// options: { gutterClassName: 'nb-gutter-cell-changed' }
// });
// }
// }
// }
// });
// this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, decorations);
});
}
private _clear() {
this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, []);
}
}
registerNotebookContribution(SCMController.id, SCMController);

View File

@@ -14,7 +14,7 @@ import { INotebookEditor, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { INotebookKernelInfo2, INotebookKernelInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
@@ -50,24 +50,22 @@ registerAction2(class extends Action2 {
const tokenSource = new CancellationTokenSource();
const availableKernels2 = await notebookService.getContributedNotebookKernels2(editor.viewModel!.viewType, editor.viewModel!.uri, tokenSource.token);
const availableKernels = notebookService.getContributedNotebookKernels(editor.viewModel!.viewType, editor.viewModel!.uri);
const picks: QuickPickInput<IQuickPickItem & { run(): void; kernelProviderId?: string; }>[] = [...availableKernels2, ...availableKernels].map((a) => {
const picks: QuickPickInput<IQuickPickItem & { run(): void; kernelProviderId?: string; }>[] = [...availableKernels2].map((a) => {
return {
id: a.id,
label: a.label,
picked: a.id === activeKernel?.id,
description:
(a as INotebookKernelInfo2).description
? (a as INotebookKernelInfo2).description
a.description
? a.description
: a.extension.value + (a.id === activeKernel?.id
? nls.localize('currentActiveKernel', " (Currently Active)")
: ''),
detail: a.detail,
kernelProviderId: a.extension.value,
run: async () => {
editor.activeKernel = a;
if ((a as any).resolve) {
(a as INotebookKernelInfo2).resolve(editor.uri!, editor.getId(), tokenSource.token);
}
a.resolve(editor.uri!, editor.getId(), tokenSource.token);
},
buttons: [{
iconClass: 'codicon-settings-gear',
@@ -76,27 +74,6 @@ registerAction2(class extends Action2 {
};
});
const provider = notebookService.getContributedNotebookProviders(editor.viewModel!.uri)[0];
if (provider.kernel) {
picks.unshift({
id: provider.id,
label: provider.displayName,
picked: !activeKernel, // no active kernel, the builtin kernel of the provider is used
description: activeKernel === undefined
? nls.localize('currentActiveBuiltinKernel', " (Currently Active)")
: '',
kernelProviderId: provider.providerExtensionId,
run: () => {
editor.activeKernel = undefined;
},
buttons: [{
iconClass: 'codicon-settings-gear',
tooltip: nls.localize('notebook.promptKernel.setDefaultTooltip', "Set as default kernel provider for '{0}'", editor.viewModel!.viewType)
}]
});
}
const picker = quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>();
picker.items = picks;
picker.activeItems = picks.filter(pick => (pick as IQuickPickItem).picked) as (IQuickPickItem & { run(): void; kernelProviderId?: string; })[];
@@ -192,7 +169,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution {
}
}
showKernelStatus(kernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined) {
showKernelStatus(kernel: INotebookKernelInfo2 | undefined) {
this.kernelInfoElement.value = this._statusbarService.addEntry({
text: kernel ? kernel.label : 'Choose Kernel',
ariaLabel: kernel ? kernel.label : 'Choose Kernel',

View File

@@ -0,0 +1,932 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CellDiffViewModel, PropertyFoldingState } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel';
import { CellDiffRenderTemplate, CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common';
import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget';
import { renderCodiconsAsElement } from 'vs/base/browser/codicons';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
import { CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { hash } from 'vs/base/common/hash';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IAction } from 'vs/base/common/actions';
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
const fixedEditorOptions: IEditorOptions = {
padding: {
top: 12,
bottom: 12
},
scrollBeyondLastLine: false,
scrollbar: {
verticalScrollbarSize: 14,
horizontal: 'auto',
useShadows: true,
verticalHasArrows: false,
horizontalHasArrows: false,
alwaysConsumeMouseWheel: false
},
renderLineHighlightOnlyWhenFocus: true,
overviewRulerLanes: 0,
selectOnLineNumbers: false,
wordWrap: 'off',
lineNumbers: 'off',
lineDecorationsWidth: 0,
glyphMargin: false,
fixedOverflowWidgets: true,
minimap: { enabled: false },
renderValidationDecorations: 'on',
renderLineHighlight: 'none',
readOnly: true
};
const fixedDiffEditorOptions: IDiffEditorOptions = {
...fixedEditorOptions,
glyphMargin: true,
enableSplitViewResizing: false,
renderIndicators: false,
readOnly: false
};
class PropertyHeader extends Disposable {
protected _foldingIndicator!: HTMLElement;
protected _statusSpan!: HTMLElement;
protected _toolbar!: ToolBar;
protected _menu!: IMenu;
constructor(
readonly cell: CellDiffViewModel,
readonly metadataHeaderContainer: HTMLElement,
readonly notebookEditor: INotebookTextDiffEditor,
readonly accessor: {
updateInfoRendering: () => void;
checkIfModified: (cell: CellDiffViewModel) => boolean;
getFoldingState: (cell: CellDiffViewModel) => PropertyFoldingState;
updateFoldingState: (cell: CellDiffViewModel, newState: PropertyFoldingState) => void;
unChangedLabel: string;
changedLabel: string;
prefix: string;
menuId: MenuId;
},
@IContextMenuService readonly contextMenuService: IContextMenuService,
@IKeybindingService readonly keybindingService: IKeybindingService,
@INotificationService readonly notificationService: INotificationService,
@IMenuService readonly menuService: IMenuService,
@IContextKeyService readonly contextKeyService: IContextKeyService
) {
super();
}
buildHeader(): void {
let metadataChanged = this.accessor.checkIfModified(this.cell);
this._foldingIndicator = DOM.append(this.metadataHeaderContainer, DOM.$('.property-folding-indicator'));
DOM.addClass(this._foldingIndicator, this.accessor.prefix);
this._updateFoldingIcon();
const metadataStatus = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-status'));
this._statusSpan = DOM.append(metadataStatus, DOM.$('span'));
if (metadataChanged) {
this._statusSpan.textContent = this.accessor.changedLabel;
this._statusSpan.style.fontWeight = 'bold';
DOM.addClass(this.metadataHeaderContainer, 'modified');
} else {
this._statusSpan.textContent = this.accessor.unChangedLabel;
}
const cellToolbarContainer = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-toolbar'));
this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, {
actionViewItemProvider: action => {
if (action instanceof MenuItemAction) {
const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
return item;
}
return undefined;
}
});
this._toolbar.context = {
cell: this.cell
};
this._menu = this.menuService.createMenu(this.accessor.menuId, this.contextKeyService);
if (metadataChanged) {
const actions: IAction[] = [];
createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions);
this._toolbar.setActions(actions);
}
this._register(this.notebookEditor.onMouseUp(e => {
if (!e.event.target) {
return;
}
const target = e.event.target as HTMLElement;
if (DOM.hasClass(target, 'codicon-chevron-down') || DOM.hasClass(target, 'codicon-chevron-right')) {
const parent = target.parentElement as HTMLElement;
if (!parent) {
return;
}
if (!DOM.hasClass(parent, this.accessor.prefix)) {
return;
}
if (!DOM.hasClass(parent, 'property-folding-indicator')) {
return;
}
// folding icon
const cellViewModel = e.target;
if (cellViewModel === this.cell) {
const oldFoldingState = this.accessor.getFoldingState(this.cell);
this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded);
this._updateFoldingIcon();
this.accessor.updateInfoRendering();
}
}
return;
}));
this._updateFoldingIcon();
this.accessor.updateInfoRendering();
}
refresh() {
let metadataChanged = this.accessor.checkIfModified(this.cell);
if (metadataChanged) {
this._statusSpan.textContent = this.accessor.changedLabel;
this._statusSpan.style.fontWeight = 'bold';
DOM.addClass(this.metadataHeaderContainer, 'modified');
const actions: IAction[] = [];
createAndFillInActionBarActions(this._menu, undefined, actions);
this._toolbar.setActions(actions);
} else {
this._statusSpan.textContent = this.accessor.unChangedLabel;
this._statusSpan.style.fontWeight = 'normal';
this._toolbar.setActions([]);
}
}
private _updateFoldingIcon() {
if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) {
DOM.reset(this._foldingIndicator, ...renderCodiconsAsElement('$(chevron-right)'));
} else {
DOM.reset(this._foldingIndicator, ...renderCodiconsAsElement('$(chevron-down)'));
}
}
}
abstract class AbstractCellRenderer extends Disposable {
protected _metadataHeaderContainer!: HTMLElement;
protected _metadataHeader!: PropertyHeader;
protected _metadataInfoContainer!: HTMLElement;
protected _metadataEditorContainer?: HTMLElement;
protected _metadataEditorDisposeStore!: DisposableStore;
protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget;
protected _outputHeaderContainer!: HTMLElement;
protected _outputHeader!: PropertyHeader;
protected _outputInfoContainer!: HTMLElement;
protected _outputEditorContainer?: HTMLElement;
protected _outputEditorDisposeStore!: DisposableStore;
protected _outputEditor?: CodeEditorWidget | DiffEditorWidget;
protected _diffEditorContainer!: HTMLElement;
protected _diagonalFill?: HTMLElement;
protected _layoutInfo!: {
editorHeight: number;
editorMargin: number;
metadataStatusHeight: number;
metadataHeight: number;
outputStatusHeight: number;
outputHeight: number;
bodyMargin: number;
};
constructor(
readonly notebookEditor: INotebookTextDiffEditor,
readonly cell: CellDiffViewModel,
readonly templateData: CellDiffRenderTemplate,
readonly style: 'left' | 'right' | 'full',
protected readonly instantiationService: IInstantiationService,
protected readonly modeService: IModeService,
protected readonly modelService: IModelService,
) {
super();
// init
this._layoutInfo = {
editorHeight: 0,
editorMargin: 0,
metadataHeight: 0,
metadataStatusHeight: 25,
outputHeight: 0,
outputStatusHeight: 25,
bodyMargin: 32
};
this._metadataEditorDisposeStore = new DisposableStore();
this._outputEditorDisposeStore = new DisposableStore();
this._register(this._metadataEditorDisposeStore);
this.initData();
this.buildBody(templateData.container);
this._register(cell.onDidLayoutChange(e => this.onDidLayoutChange(e)));
}
buildBody(container: HTMLElement) {
const body = DOM.$('.cell-body');
DOM.append(container, body);
this._diffEditorContainer = DOM.$('.cell-diff-editor-container');
switch (this.style) {
case 'left':
DOM.addClass(body, 'left');
break;
case 'right':
DOM.addClass(body, 'right');
break;
default:
DOM.addClass(body, 'full');
break;
}
DOM.append(body, this._diffEditorContainer);
this._diagonalFill = DOM.append(body, DOM.$('.diagonal-fill'));
this.styleContainer(this._diffEditorContainer);
const sourceContainer = DOM.append(this._diffEditorContainer, DOM.$('.source-container'));
this.buildSourceEditor(sourceContainer);
this._metadataHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-header-container'));
this._metadataInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-info-container'));
this._metadataHeader = this.instantiationService.createInstance(
PropertyHeader,
this.cell,
this._metadataHeaderContainer,
this.notebookEditor,
{
updateInfoRendering: this.updateMetadataRendering.bind(this),
checkIfModified: (cell) => {
return cell.type !== 'delete' && cell.type !== 'insert' && hash(this._getFormatedMetadataJSON(cell.original?.metadata || {}, cell.original?.language)) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {}, cell.modified?.language));
},
getFoldingState: (cell) => {
return cell.metadataFoldingState;
},
updateFoldingState: (cell, state) => {
cell.metadataFoldingState = state;
},
unChangedLabel: 'Metadata',
changedLabel: 'Metadata changed',
prefix: 'metadata',
menuId: MenuId.NotebookDiffCellMetadataTitle
}
);
this._register(this._metadataHeader);
this._metadataHeader.buildHeader();
this._outputHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-header-container'));
this._outputInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-info-container'));
this._outputHeader = this.instantiationService.createInstance(
PropertyHeader,
this.cell,
this._outputHeaderContainer,
this.notebookEditor,
{
updateInfoRendering: this.updateOutputRendering.bind(this),
checkIfModified: (cell) => {
return cell.type !== 'delete' && cell.type !== 'insert' && !this.notebookEditor.textModel!.transientOptions.transientOutputs && cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []);
},
getFoldingState: (cell) => {
return this.cell.outputFoldingState;
},
updateFoldingState: (cell, state) => {
cell.outputFoldingState = state;
},
unChangedLabel: 'Outputs',
changedLabel: 'Outputs changed',
prefix: 'output',
menuId: MenuId.NotebookDiffCellOutputsTitle
}
);
this._register(this._outputHeader);
this._outputHeader.buildHeader();
}
updateMetadataRendering() {
if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) {
// we should expand the metadata editor
this._metadataInfoContainer.style.display = 'block';
if (!this._metadataEditorContainer || !this._metadataEditor) {
// create editor
this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container'));
this._buildMetadataEditor();
} else {
this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight();
this.layout({ metadataEditor: true });
}
} else {
// we should collapse the metadata editor
this._metadataInfoContainer.style.display = 'none';
this._metadataEditorDisposeStore.clear();
this._layoutInfo.metadataHeight = 0;
this.layout({});
}
}
updateOutputRendering() {
if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) {
this._outputInfoContainer.style.display = 'block';
if (!this._outputEditorContainer || !this._outputEditor) {
// create editor
this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container'));
this._buildOutputEditor();
} else {
this._layoutInfo.outputHeight = this._outputEditor.getContentHeight();
this.layout({ outputEditor: true });
}
} else {
this._outputInfoContainer.style.display = 'none';
this._outputEditorDisposeStore.clear();
this._layoutInfo.outputHeight = 0;
this.layout({});
}
}
protected _getFormatedMetadataJSON(metadata: NotebookCellMetadata, language?: string) {
const filteredMetadata: { [key: string]: any } = metadata;
const content = JSON.stringify({
language,
...filteredMetadata
});
const edits = format(content, undefined, {});
const metadataSource = applyEdits(content, edits);
return metadataSource;
}
private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) {
let result: { [key: string]: any } = {};
let newLangauge: string | undefined = undefined;
try {
const newMetadataObj = JSON.parse(newMetadata);
const keys = new Set([...Object.keys(newMetadataObj)]);
for (let key of keys) {
switch (key as keyof NotebookCellMetadata) {
case 'breakpointMargin':
case 'editable':
case 'hasExecutionOrder':
case 'inputCollapsed':
case 'outputCollapsed':
case 'runnable':
// boolean
if (typeof newMetadataObj[key] === 'boolean') {
result[key] = newMetadataObj[key];
} else {
result[key] = currentMetadata[key as keyof NotebookCellMetadata];
}
break;
case 'executionOrder':
case 'lastRunDuration':
// number
if (typeof newMetadataObj[key] === 'number') {
result[key] = newMetadataObj[key];
} else {
result[key] = currentMetadata[key as keyof NotebookCellMetadata];
}
break;
case 'runState':
// enum
if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) {
result[key] = newMetadataObj[key];
} else {
result[key] = currentMetadata[key as keyof NotebookCellMetadata];
}
break;
case 'statusMessage':
// string
if (typeof newMetadataObj[key] === 'string') {
result[key] = newMetadataObj[key];
} else {
result[key] = currentMetadata[key as keyof NotebookCellMetadata];
}
break;
default:
if (key === 'language') {
newLangauge = newMetadataObj[key];
}
result[key] = newMetadataObj[key];
break;
}
}
if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) {
this.notebookEditor.textModel!.changeCellLanguage(this.cell.modified!.handle, newLangauge);
}
this.notebookEditor.textModel!.changeCellMetadata(this.cell.modified!.handle, result, false);
} catch {
}
}
private _buildMetadataEditor() {
if (this.cell.type === 'modified' || this.cell.type === 'unchanged') {
const originalMetadataSource = this._getFormatedMetadataJSON(this.cell.original?.metadata || {}, this.cell.original?.language);
const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language);
this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, {
...fixedDiffEditorOptions,
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
readOnly: false,
originalEditable: false,
ignoreTrimWhitespace: false
});
DOM.addClass(this._metadataEditorContainer!, 'diff');
const mode = this.modeService.create('json');
const originalMetadataModel = this.modelService.createModel(originalMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.original!.uri, this.cell.original!.handle), false);
const modifiedMetadataModel = this.modelService.createModel(modifiedMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.modified!.uri, this.cell.modified!.handle), false);
this._metadataEditor.setModel({
original: originalMetadataModel,
modified: modifiedMetadataModel
});
this._register(originalMetadataModel);
this._register(modifiedMetadataModel);
this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight();
this.layout({ metadataEditor: true });
this._register(this._metadataEditor.onDidContentSizeChange((e) => {
if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) {
this._layoutInfo.metadataHeight = e.contentHeight;
this.layout({ metadataEditor: true });
}
}));
let respondingToContentChange = false;
this._register(modifiedMetadataModel.onDidChangeContent(() => {
respondingToContentChange = true;
const value = modifiedMetadataModel.getValue();
this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value);
this._metadataHeader.refresh();
respondingToContentChange = false;
}));
this._register(this.cell.modified!.onDidChangeMetadata(() => {
if (respondingToContentChange) {
return;
}
const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language);
modifiedMetadataModel.setValue(modifiedMetadataSource);
}));
return;
}
this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, {
...fixedEditorOptions,
dimension: {
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
height: 0
},
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
readOnly: false
}, {});
const mode = this.modeService.create('jsonc');
const originalMetadataSource = this._getFormatedMetadataJSON(
this.cell.type === 'insert'
? this.cell.modified!.metadata || {}
: this.cell.original!.metadata || {});
const uri = this.cell.type === 'insert'
? this.cell.modified!.uri
: this.cell.original!.uri;
const handle = this.cell.type === 'insert'
? this.cell.modified!.handle
: this.cell.original!.handle;
const modelUri = CellUri.generateCellMetadataUri(uri, handle);
const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false);
this._metadataEditor.setModel(metadataModel);
this._register(metadataModel);
this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight();
this.layout({ metadataEditor: true });
this._register(this._metadataEditor.onDidContentSizeChange((e) => {
if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) {
this._layoutInfo.metadataHeight = e.contentHeight;
this.layout({ metadataEditor: true });
}
}));
}
private _getFormatedOutputJSON(outputs: any[]) {
const content = JSON.stringify(outputs);
const edits = format(content, undefined, {});
const source = applyEdits(content, edits);
return source;
}
private _buildOutputEditor() {
if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) {
const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []);
const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []);
if (originalOutputsSource !== modifiedOutputsSource) {
this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, {
...fixedDiffEditorOptions,
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
readOnly: true,
ignoreTrimWhitespace: false
});
DOM.addClass(this._outputEditorContainer!, 'diff');
const mode = this.modeService.create('json');
const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true);
const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true);
this._outputEditor.setModel({
original: originalModel,
modified: modifiedModel
});
this._layoutInfo.outputHeight = this._outputEditor.getContentHeight();
this.layout({ outputEditor: true });
this._register(this._outputEditor.onDidContentSizeChange((e) => {
if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) {
this._layoutInfo.outputHeight = e.contentHeight;
this.layout({ outputEditor: true });
}
}));
this._register(this.cell.modified!.onDidChangeOutputs(() => {
const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []);
modifiedModel.setValue(modifiedOutputsSource);
this._outputHeader.refresh();
}));
return;
}
}
this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, {
...fixedEditorOptions,
dimension: {
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
height: 0
},
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode()
}, {});
const mode = this.modeService.create('json');
const originaloutputSource = this._getFormatedOutputJSON(
this.notebookEditor.textModel!.transientOptions
? []
: this.cell.type === 'insert'
? this.cell.modified!.outputs || []
: this.cell.original!.outputs || []);
const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true);
this._outputEditor.setModel(outputModel);
this._layoutInfo.outputHeight = this._outputEditor.getContentHeight();
this.layout({ outputEditor: true });
this._register(this._outputEditor.onDidContentSizeChange((e) => {
if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) {
this._layoutInfo.outputHeight = e.contentHeight;
this.layout({ outputEditor: true });
}
}));
}
protected layoutNotebookCell() {
this.notebookEditor.layoutNotebookCell(
this.cell,
this._layoutInfo.editorHeight
+ this._layoutInfo.editorMargin
+ this._layoutInfo.metadataHeight
+ this._layoutInfo.metadataStatusHeight
+ this._layoutInfo.outputHeight
+ this._layoutInfo.outputStatusHeight
+ this._layoutInfo.bodyMargin
);
}
abstract initData(): void;
abstract styleContainer(container: HTMLElement): void;
abstract buildSourceEditor(sourceContainer: HTMLElement): void;
abstract onDidLayoutChange(event: CellDiffViewModelLayoutChangeEvent): void;
abstract layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }): void;
}
export class DeletedCell extends AbstractCellRenderer {
private _editor!: CodeEditorWidget;
constructor(
readonly notebookEditor: INotebookTextDiffEditor,
readonly cell: CellDiffViewModel,
readonly templateData: CellDiffRenderTemplate,
@IModeService readonly modeService: IModeService,
@IModelService readonly modelService: IModelService,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
) {
super(notebookEditor, cell, templateData, 'left', instantiationService, modeService, modelService);
}
initData(): void {
}
styleContainer(container: HTMLElement) {
DOM.addClass(container, 'removed');
}
buildSourceEditor(sourceContainer: HTMLElement): void {
const originalCell = this.cell.original!;
const lineCount = originalCell.textBuffer.getLineCount();
const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17;
const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING;
const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container'));
this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, {
...fixedEditorOptions,
dimension: {
width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18,
height: editorHeight
},
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode()
}, {});
this._layoutInfo.editorHeight = editorHeight;
this._register(this._editor.onDidContentSizeChange((e) => {
if (e.contentHeightChanged) {
this._layoutInfo.editorHeight = e.contentHeight;
this.layout({ editorHeight: true });
}
}));
originalCell.resolveTextModelRef().then(ref => {
this._register(ref);
const textModel = ref.object.textEditorModel;
this._editor.setModel(textModel);
this._layoutInfo.editorHeight = this._editor.getContentHeight();
this.layout({ editorHeight: true });
});
}
onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) {
if (e.outerWidth !== undefined) {
this.layout({ outerWidth: true });
}
}
layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) {
if (state.editorHeight || state.outerWidth) {
this._editor.layout({
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
height: this._layoutInfo.editorHeight
});
}
if (state.metadataEditor || state.outerWidth) {
this._metadataEditor?.layout({
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
height: this._layoutInfo.metadataHeight
});
}
if (state.outputEditor || state.outerWidth) {
this._outputEditor?.layout({
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
height: this._layoutInfo.outputHeight
});
}
this.layoutNotebookCell();
}
}
export class InsertCell extends AbstractCellRenderer {
private _editor!: CodeEditorWidget;
constructor(
readonly notebookEditor: INotebookTextDiffEditor,
readonly cell: CellDiffViewModel,
readonly templateData: CellDiffRenderTemplate,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IModeService readonly modeService: IModeService,
@IModelService readonly modelService: IModelService,
) {
super(notebookEditor, cell, templateData, 'right', instantiationService, modeService, modelService);
}
initData(): void {
}
styleContainer(container: HTMLElement): void {
DOM.addClass(container, 'inserted');
}
buildSourceEditor(sourceContainer: HTMLElement): void {
const modifiedCell = this.cell.modified!;
const lineCount = modifiedCell.textBuffer.getLineCount();
const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17;
const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING;
const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container'));
this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, {
...fixedEditorOptions,
dimension: {
width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18,
height: editorHeight
},
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
readOnly: false
}, {});
this._layoutInfo.editorHeight = editorHeight;
this._register(this._editor.onDidContentSizeChange((e) => {
if (e.contentHeightChanged) {
this._layoutInfo.editorHeight = e.contentHeight;
this.layout({ editorHeight: true });
}
}));
modifiedCell.resolveTextModelRef().then(ref => {
this._register(ref);
const textModel = ref.object.textEditorModel;
this._editor.setModel(textModel);
this._layoutInfo.editorHeight = this._editor.getContentHeight();
this.layout({ editorHeight: true });
});
}
onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) {
if (e.outerWidth !== undefined) {
this.layout({ outerWidth: true });
}
}
layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) {
if (state.editorHeight || state.outerWidth) {
this._editor.layout({
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false),
height: this._layoutInfo.editorHeight
});
}
if (state.metadataEditor || state.outerWidth) {
this._metadataEditor?.layout({
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
height: this._layoutInfo.metadataHeight
});
}
if (state.outputEditor || state.outerWidth) {
this._outputEditor?.layout({
width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true),
height: this._layoutInfo.outputHeight
});
}
this.layoutNotebookCell();
}
}
export class ModifiedCell extends AbstractCellRenderer {
private _editor?: DiffEditorWidget;
private _editorContainer!: HTMLElement;
constructor(
readonly notebookEditor: INotebookTextDiffEditor,
readonly cell: CellDiffViewModel,
readonly templateData: CellDiffRenderTemplate,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IModeService readonly modeService: IModeService,
@IModelService readonly modelService: IModelService,
) {
super(notebookEditor, cell, templateData, 'full', instantiationService, modeService, modelService);
}
initData(): void {
}
styleContainer(container: HTMLElement): void {
}
buildSourceEditor(sourceContainer: HTMLElement): void {
const modifiedCell = this.cell.modified!;
const lineCount = modifiedCell.textBuffer.getLineCount();
const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17;
const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING;
this._editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container'));
this._editor = this.instantiationService.createInstance(DiffEditorWidget, this._editorContainer, {
...fixedDiffEditorOptions,
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(),
originalEditable: false,
ignoreTrimWhitespace: false
});
DOM.addClass(this._editorContainer, 'diff');
this._editor.layout({
width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN,
height: editorHeight
});
this._editorContainer.style.height = `${editorHeight}px`;
this._register(this._editor.onDidContentSizeChange((e) => {
if (e.contentHeightChanged) {
this._layoutInfo.editorHeight = e.contentHeight;
this.layout({ editorHeight: true });
}
}));
this._initializeSourceDiffEditor();
}
private async _initializeSourceDiffEditor() {
const originalCell = this.cell.original!;
const modifiedCell = this.cell.modified!;
const originalRef = await originalCell.resolveTextModelRef();
const modifiedRef = await modifiedCell.resolveTextModelRef();
const textModel = originalRef.object.textEditorModel;
const modifiedTextModel = modifiedRef.object.textEditorModel;
this._register(originalRef);
this._register(modifiedRef);
this._editor!.setModel({
original: textModel,
modified: modifiedTextModel
});
const contentHeight = this._editor!.getContentHeight();
this._layoutInfo.editorHeight = contentHeight;
this.layout({ editorHeight: true });
}
onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) {
if (e.outerWidth !== undefined) {
this.layout({ outerWidth: true });
}
}
layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) {
if (state.editorHeight || state.outerWidth) {
this._editorContainer.style.height = `${this._layoutInfo.editorHeight}px`;
this._editor!.layout();
}
if (state.metadataEditor || state.outerWidth) {
if (this._metadataEditorContainer) {
this._metadataEditorContainer.style.height = `${this._layoutInfo.metadataHeight}px`;
this._metadataEditor?.layout();
}
}
if (state.outputEditor || state.outerWidth) {
if (this._outputEditorContainer) {
this._outputEditorContainer.style.height = `${this._layoutInfo.outputHeight}px`;
this._outputEditor?.layout();
}
}
this.layoutNotebookCell();
}
}

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/diff/common';
import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget';
export enum PropertyFoldingState {
Expanded,
Collapsed
}
export class CellDiffViewModel extends Disposable {
public metadataFoldingState: PropertyFoldingState;
public outputFoldingState: PropertyFoldingState;
private _layoutInfoEmitter = new Emitter<CellDiffViewModelLayoutChangeEvent>();
onDidLayoutChange = this._layoutInfoEmitter.event;
constructor(
readonly original: NotebookCellTextModel | undefined,
readonly modified: NotebookCellTextModel | undefined,
readonly type: 'unchanged' | 'insert' | 'delete' | 'modified',
readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher
) {
super();
this.metadataFoldingState = PropertyFoldingState.Collapsed;
this.outputFoldingState = PropertyFoldingState.Collapsed;
this._register(this.editorEventDispatcher.onDidChangeLayout(e => {
this._layoutInfoEmitter.fire({ outerWidth: e.value.width });
}));
}
getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) {
if (fullWidth) {
return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2;
}
return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2;
}
}

View File

@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel';
import { Event } from 'vs/base/common/event';
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
export interface INotebookTextDiffEditor {
readonly textModel?: NotebookTextModel;
onMouseUp: Event<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>;
getOverflowContainerDomNode(): HTMLElement;
getLayoutInfo(): NotebookLayoutInfo;
layoutNotebookCell(cell: CellDiffViewModel, height: number): void;
}
export interface CellDiffRenderTemplate {
readonly container: HTMLElement;
readonly elementDisposables: DisposableStore;
}
export interface CellDiffViewModelLayoutChangeEvent {
font?: BareFontInfo;
outerWidth?: number;
}
export const DIFF_CELL_MARGIN = 16;

View File

@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* .notebook-diff-editor {
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
}
.notebook-diff-editor-modified,
.notebook-diff-editor-original {
display: flex;
height: 100%;
width: 50%;
} */
.notebook-text-diff-editor .cell-body {
display: flex;
flex-direction: row;
}
.notebook-text-diff-editor .cell-body.right {
flex-direction: row-reverse;
}
.notebook-text-diff-editor .cell-body .diagonal-fill {
display: none;
width: 50%;
}
.notebook-text-diff-editor .cell-body .cell-diff-editor-container {
width: 100%;
overflow: hidden;
}
.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container.diff,
.notebook-text-diff-editor .cell-body .cell-diff-editor-container .output-editor-container.diff,
.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff {
/** 100% + diffOverviewWidth */
width: calc(100% + 30px);
}
.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container .monaco-diff-editor .diffOverview,
.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff .monaco-diff-editor .diffOverview {
display: none;
}
.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container,
.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container {
box-sizing: border-box;
}
.notebook-text-diff-editor .cell-body.left .cell-diff-editor-container,
.notebook-text-diff-editor .cell-body.right .cell-diff-editor-container {
display: inline-block;
width: 50%;
}
.notebook-text-diff-editor .cell-body.left .diagonal-fill,
.notebook-text-diff-editor .cell-body.right .diagonal-fill {
display: inline-block;
width: 50%;
}
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container {
display: flex;
height: 24px;
align-items: center;
cursor: default;
}
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-folding-indicator .codicon,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-folding-indicator .codicon {
visibility: visible;
padding: 4px 0 0 10px;
cursor: pointer;
}
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container {
display: flex;
flex-direction: row;
align-items: center;
}
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-toolbar,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-toolbar {
margin-left: auto;
}
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status {
font-size: 12px;
}
.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status span,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status span {
margin: 0 8px;
line-height: 21px;
}
.notebook-text-diff-editor {
overflow: hidden;
}
.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row,
.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover,
.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused {
outline: none !important;
}

View File

@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
import { ActiveEditorContext } from 'vs/workbench/common/editor';
import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel';
import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor';
import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
// ActiveEditorContext.isEqualTo(SearchEditorConstants.SearchEditorID)
registerAction2(class extends Action2 {
constructor() {
super({
id: 'notebook.diff.switchToText',
icon: { id: 'codicon/file-code' },
title: { value: localize('notebook.diff.switchToText', "Open Text Diff Editor"), original: 'Open Text Diff Editor' },
precondition: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID),
menu: [{
id: MenuId.EditorTitle,
group: 'navigation',
when: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID)
}]
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(IEditorService);
const editorGroupService = accessor.get(IEditorGroupsService);
const activeEditor = editorService.activeEditorPane;
if (activeEditor && activeEditor instanceof NotebookTextDiffEditor) {
const leftResource = (activeEditor.input as NotebookDiffEditorInput).originalResource;
const rightResource = (activeEditor.input as NotebookDiffEditorInput).resource;
const options = {
preserveFocus: false
};
const label = localize('diffLeftRightLabel', "{0} ⟷ {1}", leftResource.toString(true), rightResource.toString(true));
await editorService.openEditor({ leftResource, rightResource, label, options }, viewColumnToEditorGroup(editorGroupService, undefined));
}
}
});
registerAction2(class extends Action2 {
constructor() {
super(
{
id: 'notebook.diff.cell.revertMetadata',
title: localize('notebook.diff.cell.revertMetadata', "Revert Metadata"),
icon: { id: 'codicon/discard' },
f1: false,
menu: {
id: MenuId.NotebookDiffCellMetadataTitle
}
}
);
}
run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) {
if (!context) {
return;
}
const original = context.cell.original;
const modified = context.cell.modified;
if (!original || !modified) {
return;
}
modified.metadata = original.metadata;
}
});
registerAction2(class extends Action2 {
constructor() {
super(
{
id: 'notebook.diff.cell.revertOutputs',
title: localize('notebook.diff.cell.revertOutputs', "Revert Outputs"),
icon: { id: 'codicon/discard' },
f1: false,
menu: {
id: MenuId.NotebookDiffCellOutputsTitle
}
}
);
}
run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) {
if (!context) {
return;
}
const original = context.cell.original;
const modified = context.cell.modified;
if (!original || !modified) {
return;
}
modified.spliceNotebookCellOutputs([[0, modified.outputs.length, original.outputs]]);
}
});

View File

@@ -0,0 +1,478 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor';
import { notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { NotebookDiffEditorInput } from '../notebookDiffEditorInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CellDiffRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { diffDiagonalFill, diffInserted, diffRemoved, editorBackground, focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry';
import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
import { getZoomLevel } from 'vs/base/browser/browser';
import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common';
import { Emitter } from 'vs/base/common/event';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { NotebookDiffEditorEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { FileService } from 'vs/platform/files/common/fileService';
import { IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey<boolean>('isInNotebookTextDiffEditor', false);
export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor {
static readonly ID: string = 'workbench.editor.notebookTextDiffEditor';
private _rootElement!: HTMLElement;
private _overflowContainer!: HTMLElement;
private _dimension: DOM.Dimension | null = null;
private _list!: WorkbenchList<CellDiffViewModel>;
private _fontInfo: BareFontInfo | undefined;
private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>());
public readonly onMouseUp = this._onMouseUp.event;
private _eventDispatcher: NotebookDiffEditorEventDispatcher | undefined;
protected _scopeContextKeyService!: IContextKeyService;
private _model: INotebookDiffEditorModel | null = null;
private _modifiedResourceDisposableStore = new DisposableStore();
get textModel() {
return this._model?.modified.notebook;
}
constructor(
@IInstantiationService readonly instantiationService: IInstantiationService,
@IThemeService readonly themeService: IThemeService,
@IContextKeyService readonly contextKeyService: IContextKeyService,
@INotebookEditorWorkerService readonly notebookEditorWorkerService: INotebookEditorWorkerService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IFileService private readonly _fileService: FileService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
) {
super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService);
const editorOptions = this.configurationService.getValue<IEditorOptions>('editor');
this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel());
this._register(this._modifiedResourceDisposableStore);
}
protected createEditor(parent: HTMLElement): void {
this._rootElement = DOM.append(parent, DOM.$('.notebook-text-diff-editor'));
this._overflowContainer = document.createElement('div');
DOM.addClass(this._overflowContainer, 'notebook-overflow-widget-container');
DOM.addClass(this._overflowContainer, 'monaco-editor');
DOM.append(parent, this._overflowContainer);
const renderer = this.instantiationService.createInstance(CellDiffRenderer, this);
this._list = this.instantiationService.createInstance(
NotebookTextDiffList,
'NotebookTextDiff',
this._rootElement,
this.instantiationService.createInstance(NotebookCellTextDiffListDelegate),
[
renderer
],
this.contextKeyService,
{
setRowLineHeight: false,
setRowHeight: false,
supportDynamicHeights: true,
horizontalScrolling: false,
keyboardSupport: false,
mouseSupport: true,
multipleSelectionSupport: false,
enableKeyboardNavigation: true,
additionalScrollHeight: 0,
// transformOptimization: (isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native',
styleController: (_suffix: string) => { return this._list!; },
overrideStyles: {
listBackground: editorBackground,
listActiveSelectionBackground: editorBackground,
listActiveSelectionForeground: foreground,
listFocusAndSelectionBackground: editorBackground,
listFocusAndSelectionForeground: foreground,
listFocusBackground: editorBackground,
listFocusForeground: foreground,
listHoverForeground: foreground,
listHoverBackground: editorBackground,
listHoverOutline: focusBorder,
listFocusOutline: focusBorder,
listInactiveSelectionBackground: editorBackground,
listInactiveSelectionForeground: foreground,
listInactiveFocusBackground: editorBackground,
listInactiveFocusOutline: editorBackground,
},
accessibilityProvider: {
getAriaLabel() { return null; },
getWidgetAriaLabel() {
return nls.localize('notebookTreeAriaLabel', "Notebook Text Diff");
}
},
// focusNextPreviousDelegate: {
// onFocusNext: (applyFocusNext: () => void) => this._updateForCursorNavigationMode(applyFocusNext),
// onFocusPrevious: (applyFocusPrevious: () => void) => this._updateForCursorNavigationMode(applyFocusPrevious),
// }
}
);
this._register(this._list.onMouseUp(e => {
if (e.element) {
this._onMouseUp.fire({ event: e.browserEvent, target: e.element });
}
}));
}
async setInput(input: NotebookDiffEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
await super.setInput(input, options, context, token);
this._model = await input.resolve();
if (this._model === null) {
return;
}
this._modifiedResourceDisposableStore.add(this._fileService.watch(this._model.modified.resource));
this._modifiedResourceDisposableStore.add(this._fileService.onDidFilesChange(async e => {
if (this._model === null) {
return;
}
if (e.contains(this._model!.modified.resource)) {
if (this._model.modified.isDirty()) {
return;
}
const modified = this._model.modified;
const lastResolvedFileStat = modified.lastResolvedFileStat;
const currFileStat = await this._resolveStats(modified.resource);
if (lastResolvedFileStat && currFileStat && currFileStat.mtime > lastResolvedFileStat.mtime) {
await this._model.resolveModifiedFromDisk();
await this.updateLayout();
return;
}
}
if (e.contains(this._model!.original.resource)) {
if (this._model.original.isDirty()) {
return;
}
const original = this._model.original;
const lastResolvedFileStat = original.lastResolvedFileStat;
const currFileStat = await this._resolveStats(original.resource);
if (lastResolvedFileStat && currFileStat && currFileStat.mtime > lastResolvedFileStat.mtime) {
await this._model.resolveOriginalFromDisk();
await this.updateLayout();
return;
}
}
}));
this._eventDispatcher = new NotebookDiffEditorEventDispatcher();
await this.updateLayout();
}
private async _resolveStats(resource: URI) {
if (resource.scheme === Schemas.untitled) {
return undefined;
}
try {
const newStats = await this._fileService.resolve(resource, { resolveMetadata: true });
return newStats;
} catch (e) {
return undefined;
}
}
async updateLayout() {
console.log('update layout');
if (!this._model) {
return;
}
const diffResult = await this.notebookEditorWorkerService.computeDiff(this._model.original.resource, this._model.modified.resource);
const cellChanges = diffResult.cellsDiff.changes;
const cellDiffViewModels: CellDiffViewModel[] = [];
const originalModel = this._model.original.notebook;
const modifiedModel = this._model.modified.notebook;
let originalCellIndex = 0;
let modifiedCellIndex = 0;
for (let i = 0; i < cellChanges.length; i++) {
const change = cellChanges[i];
// common cells
for (let j = 0; j < change.originalStart - originalCellIndex; j++) {
const originalCell = originalModel.cells[originalCellIndex + j];
const modifiedCell = modifiedModel.cells[modifiedCellIndex + j];
if (originalCell.getHashValue() === modifiedCell.getHashValue()) {
cellDiffViewModels.push(new CellDiffViewModel(
originalCell,
modifiedCell,
'unchanged',
this._eventDispatcher!
));
} else {
cellDiffViewModels.push(new CellDiffViewModel(
originalCell,
modifiedCell,
'modified',
this._eventDispatcher!
));
}
}
// modified cells
const modifiedLen = Math.min(change.originalLength, change.modifiedLength);
for (let j = 0; j < modifiedLen; j++) {
cellDiffViewModels.push(new CellDiffViewModel(
originalModel.cells[change.originalStart + j],
modifiedModel.cells[change.modifiedStart + j],
'modified',
this._eventDispatcher!
));
}
for (let j = modifiedLen; j < change.originalLength; j++) {
// deletion
cellDiffViewModels.push(new CellDiffViewModel(
originalModel.cells[change.originalStart + j],
undefined,
'delete',
this._eventDispatcher!
));
}
for (let j = modifiedLen; j < change.modifiedLength; j++) {
// insertion
cellDiffViewModels.push(new CellDiffViewModel(
undefined,
modifiedModel.cells[change.modifiedStart + j],
'insert',
this._eventDispatcher!
));
}
originalCellIndex = change.originalStart + change.originalLength;
modifiedCellIndex = change.modifiedStart + change.modifiedLength;
}
for (let i = originalCellIndex; i < originalModel.cells.length; i++) {
cellDiffViewModels.push(new CellDiffViewModel(
originalModel.cells[i],
modifiedModel.cells[i - originalCellIndex + modifiedCellIndex],
'unchanged',
this._eventDispatcher!
));
}
this._list.splice(0, this._list.length, cellDiffViewModels);
}
private pendingLayouts = new WeakMap<CellDiffViewModel, IDisposable>();
layoutNotebookCell(cell: CellDiffViewModel, height: number) {
const relayout = (cell: CellDiffViewModel, height: number) => {
const viewIndex = this._list!.indexOf(cell);
this._list?.updateElementHeight(viewIndex, height);
};
if (this.pendingLayouts.has(cell)) {
this.pendingLayouts.get(cell)!.dispose();
}
let r: () => void;
const layoutDisposable = DOM.scheduleAtNextAnimationFrame(() => {
this.pendingLayouts.delete(cell);
relayout(cell, height);
r();
});
this.pendingLayouts.set(cell, toDisposable(() => {
layoutDisposable.dispose();
r();
}));
return new Promise(resolve => { r = resolve; });
}
getDomNode() {
return this._rootElement;
}
getOverflowContainerDomNode(): HTMLElement {
return this._overflowContainer;
}
getControl(): NotebookEditorWidget | undefined {
return undefined;
}
setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void {
super.setEditorVisible(visible, group);
}
focus() {
super.focus();
}
clearInput(): void {
super.clearInput();
this._modifiedResourceDisposableStore.clear();
this._list?.splice(0, this._list?.length || 0);
}
getLayoutInfo(): NotebookLayoutInfo {
if (!this._list) {
throw new Error('Editor is not initalized successfully');
}
return {
width: this._dimension!.width,
height: this._dimension!.height,
fontInfo: this._fontInfo!
};
}
layout(dimension: DOM.Dimension): void {
this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600);
this._rootElement.classList.toggle('narrow-width', dimension.width < 600);
this._dimension = dimension;
this._rootElement.style.height = `${dimension.height}px`;
this._list?.layout(this._dimension.height, this._dimension.width);
this._eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]);
}
}
registerThemingParticipant((theme, collector) => {
const cellBorderColor = theme.getColor(notebookCellBorder);
if (cellBorderColor) {
collector.addRule(`.notebook-text-diff-editor .cell-body { border: 1px solid ${cellBorderColor};}`);
collector.addRule(`.notebook-text-diff-editor .cell-diff-editor-container .output-header-container,
.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container {
border-top: 1px solid ${cellBorderColor};
}`);
}
const diffDiagonalFillColor = theme.getColor(diffDiagonalFill);
collector.addRule(`
.notebook-text-diff-editor .diagonal-fill {
background-image: linear-gradient(
-45deg,
${diffDiagonalFillColor} 12.5%,
#0000 12.5%, #0000 50%,
${diffDiagonalFillColor} 50%, ${diffDiagonalFillColor} 62.5%,
#0000 62.5%, #0000 100%
);
background-size: 8px 8px;
}
`);
const added = theme.getColor(diffInserted);
if (added) {
collector.addRule(`
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container { background-color: ${added}; }
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .margin,
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .monaco-editor-background {
background-color: ${added};
}
`
);
collector.addRule(`
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container { background-color: ${added}; }
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container .monaco-editor .margin,
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container .monaco-editor .monaco-editor-background {
background-color: ${added};
}
`
);
collector.addRule(`
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container { background-color: ${added}; }
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container .monaco-editor .margin,
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container .monaco-editor .monaco-editor-background {
background-color: ${added};
}
`
);
collector.addRule(`
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-header-container { background-color: ${added}; }
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-header-container { background-color: ${added}; }
`
);
}
const removed = theme.getColor(diffRemoved);
if (added) {
collector.addRule(`
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container { background-color: ${removed}; }
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .margin,
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .monaco-editor-background {
background-color: ${removed};
}
`
);
collector.addRule(`
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container { background-color: ${removed}; }
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container .monaco-editor .margin,
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container .monaco-editor .monaco-editor-background {
background-color: ${removed};
}
`
);
collector.addRule(`
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container { background-color: ${removed}; }
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container .monaco-editor .margin,
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container .monaco-editor .monaco-editor-background {
background-color: ${removed};
}
`
);
collector.addRule(`
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-header-container { background-color: ${removed}; }
.notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-header-container { background-color: ${removed}; }
`
);
}
// const changed = theme.getColor(editorGutterModifiedBackground);
// if (changed) {
// collector.addRule(`
// .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container.modified {
// background-color: ${changed};
// }
// `);
// }
collector.addRule(`.notebook-text-diff-editor .cell-body { margin: ${DIFF_CELL_MARGIN}px; }`);
});

View File

@@ -0,0 +1,228 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./notebookDiff';
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import * as DOM from 'vs/base/browser/dom';
import { IListStyles, IStyleController } from 'vs/base/browser/ui/list/listWidget';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel';
import { CellDiffRenderTemplate, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common';
import { isMacintosh } from 'vs/base/common/platform';
import { DeletedCell, InsertCell, ModifiedCell } from 'vs/workbench/contrib/notebook/browser/diff/cellComponents';
export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate<CellDiffViewModel> {
// private readonly lineHeight: number;
constructor(
@IConfigurationService readonly configurationService: IConfigurationService
) {
// const editorOptions = this.configurationService.getValue<IEditorOptions>('editor');
// this.lineHeight = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight;
}
getHeight(element: CellDiffViewModel): number {
return 100;
}
hasDynamicHeight(element: CellDiffViewModel): boolean {
return false;
}
getTemplateId(element: CellDiffViewModel): string {
return CellDiffRenderer.TEMPLATE_ID;
}
}
export class CellDiffRenderer implements IListRenderer<CellDiffViewModel, CellDiffRenderTemplate> {
static readonly TEMPLATE_ID = 'cell_diff';
constructor(
readonly notebookEditor: INotebookTextDiffEditor,
@IInstantiationService protected readonly instantiationService: IInstantiationService
) { }
get templateId() {
return CellDiffRenderer.TEMPLATE_ID;
}
renderTemplate(container: HTMLElement): CellDiffRenderTemplate {
return {
container,
elementDisposables: new DisposableStore()
};
}
renderElement(element: CellDiffViewModel, index: number, templateData: CellDiffRenderTemplate, height: number | undefined): void {
templateData.container.innerText = '';
switch (element.type) {
case 'unchanged':
templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedCell, this.notebookEditor, element, templateData));
return;
case 'delete':
templateData.elementDisposables.add(this.instantiationService.createInstance(DeletedCell, this.notebookEditor, element, templateData));
return;
case 'insert':
templateData.elementDisposables.add(this.instantiationService.createInstance(InsertCell, this.notebookEditor, element, templateData));
return;
case 'modified':
templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedCell, this.notebookEditor, element, templateData));
return;
default:
break;
}
}
disposeTemplate(templateData: CellDiffRenderTemplate): void {
templateData.container.innerText = '';
}
disposeElement(element: CellDiffViewModel, index: number, templateData: CellDiffRenderTemplate): void {
templateData.elementDisposables.clear();
}
}
export class NotebookTextDiffList extends WorkbenchList<CellDiffViewModel> implements IDisposable, IStyleController {
private styleElement?: HTMLStyleElement;
constructor(
listUser: string,
container: HTMLElement,
delegate: IListVirtualDelegate<CellDiffViewModel>,
renderers: IListRenderer<CellDiffViewModel, CellDiffRenderTemplate>[],
contextKeyService: IContextKeyService,
options: IWorkbenchListOptions<CellDiffViewModel>,
@IListService listService: IListService,
@IThemeService themeService: IThemeService,
@IConfigurationService configurationService: IConfigurationService,
@IKeybindingService keybindingService: IKeybindingService) {
super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService);
}
style(styles: IListStyles) {
const selectorSuffix = this.view.domId;
if (!this.styleElement) {
this.styleElement = DOM.createStyleSheet(this.view.domNode);
}
const suffix = selectorSuffix && `.${selectorSuffix}`;
const content: string[] = [];
if (styles.listBackground) {
if (styles.listBackground.isOpaque()) {
content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows { background: ${styles.listBackground}; }`);
} else if (!isMacintosh) { // subpixel AA doesn't exist in macOS
console.warn(`List with id '${selectorSuffix}' was styled with a non-opaque background color. This will break sub-pixel antialiasing.`);
}
}
if (styles.listFocusBackground) {
content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`);
content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case!
}
if (styles.listFocusForeground) {
content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`);
}
if (styles.listActiveSelectionBackground) {
content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`);
content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case!
}
if (styles.listActiveSelectionForeground) {
content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`);
}
if (styles.listFocusAndSelectionBackground) {
content.push(`
.monaco-drag-image,
.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; }
`);
}
if (styles.listFocusAndSelectionForeground) {
content.push(`
.monaco-drag-image,
.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; }
`);
}
if (styles.listInactiveFocusBackground) {
content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`);
content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case!
}
if (styles.listInactiveSelectionBackground) {
content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { background-color: ${styles.listInactiveSelectionBackground}; }`);
content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case!
}
if (styles.listInactiveSelectionForeground) {
content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`);
}
if (styles.listHoverBackground) {
content.push(`.monaco-list${suffix}:not(.drop-target) > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`);
}
if (styles.listHoverForeground) {
content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`);
}
if (styles.listSelectionOutline) {
content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`);
}
if (styles.listFocusOutline) {
content.push(`
.monaco-drag-image,
.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }
`);
}
if (styles.listInactiveFocusOutline) {
content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`);
}
if (styles.listHoverOutline) {
content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`);
}
if (styles.listDropBackground) {
content.push(`
.monaco-list${suffix}.drop-target,
.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows.drop-target,
.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-row.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; }
`);
}
if (styles.listFilterWidgetBackground) {
content.push(`.monaco-list-type-filter { background-color: ${styles.listFilterWidgetBackground} }`);
}
if (styles.listFilterWidgetOutline) {
content.push(`.monaco-list-type-filter { border: 1px solid ${styles.listFilterWidgetOutline}; }`);
}
if (styles.listFilterWidgetNoMatchesOutline) {
content.push(`.monaco-list-type-filter.no-matches { border: 1px solid ${styles.listFilterWidgetNoMatchesOutline}; }`);
}
if (styles.listMatchesShadow) {
content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`);
}
const newStyles = content.join('\n');
if (newStyles !== this.styleElement.innerHTML) {
this.styleElement.innerHTML = newStyles;
}
}
}

View File

@@ -96,7 +96,7 @@ const notebookProviderContribution: IJSONSchema = {
const notebookRendererContribution: IJSONSchema = {
description: nls.localize('contributes.notebook.renderer', 'Contributes notebook output renderer provider.'),
type: 'array',
defaultSnippets: [{ body: [{ id: '', displayName: '', mimeTypes: [''] }] }],
defaultSnippets: [{ body: [{ id: '', displayName: '', mimeTypes: [''], entrypoint: '' }] }],
items: {
type: 'object',
required: [

View File

@@ -55,6 +55,12 @@
width: 100%;
}
.monaco-workbench .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row {
cursor: default;
overflow: visible !important;
width: 100%;
}
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image {
position: absolute;
top: -500px;
@@ -344,19 +350,46 @@
position: relative;
}
.monaco-workbench .notebookOverlay.cell-statusbar-hidden .cell-statusbar-container {
display: none;
}
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left {
display: flex;
flex-grow: 1;
}
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left,
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right {
padding-right: 12px;
display: flex;
z-index: 26;
}
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker {
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right .cell-contributed-items {
justify-content: flex-end;
}
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-contributed-items {
display: flex;
flex-wrap: wrap;
overflow: hidden;
}
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item {
display: flex;
align-items: center;
white-space: pre;
height: 21px; /* Editor outline is -1px in, don't overlap */
padding: 0px 6px;
}
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item.cell-status-item-has-command {
cursor: pointer;
}
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker {
cursor: pointer;
}
@@ -370,6 +403,10 @@
align-items: center;
}
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-message {
margin-right: 6px;
}
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status {
height: 100%;
display: flex;
@@ -391,34 +428,35 @@
bottom: 0px;
top: 0px;
}
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container {
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container {
position: relative;
height: 16px;
flex-shrink: 0;
top: 9px;
z-index: 27; /* Above the drag handle */
z-index: 27;
/* Above the drag handle */
}
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar {
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar {
visibility: hidden;
}
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon {
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .codicon {
margin: 0;
padding-right: 4px;
}
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .actions-container {
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .actions-container {
justify-content: center;
}
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell.runnable .run-button-container .monaco-toolbar,
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell.runnable .run-button-container .monaco-toolbar,
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell.runnable .run-button-container .monaco-toolbar {
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .runnable .run-button-container .monaco-toolbar,
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .runnable .run-button-container .monaco-toolbar,
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .runnable .run-button-container .monaco-toolbar {
visibility: visible;
}
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .execution-count-label {
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .execution-count-label {
position: absolute;
font-size: 10px;
font-family: var(--monaco-monospace-font);

View File

@@ -30,7 +30,7 @@ import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEd
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl';
import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority, NotebookTextDiffEditorPreview, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService';
@@ -40,6 +40,19 @@ import { CustomEditorInfo } from 'vs/workbench/contrib/customEditor/common/custo
import { INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { INotebookEditorModelResolverService, NotebookModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput';
import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor';
import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService';
import { NotebookEditorWorkerServiceImpl } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl';
import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService';
import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { Event } from 'vs/base/common/event';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
// Editor Contribution
@@ -50,13 +63,16 @@ import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting';
import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider';
import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider';
import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus';
// import 'vs/workbench/contrib/notebook/browser/contrib/scm/scm';
// Diff Editor Contribution
import 'vs/workbench/contrib/notebook/browser/diff/notebookDiffActions';
// Output renderers registration
import 'vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform';
import 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform';
import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
/*--------------------------------------------------------------------------------------------- */
@@ -71,6 +87,53 @@ Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
]
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
EditorDescriptor.create(
NotebookTextDiffEditor,
NotebookTextDiffEditor.ID,
'Notebook Diff Editor'
),
[
new SyncDescriptor(NotebookDiffEditorInput)
]
);
class NotebookDiffEditorFactory implements IEditorInputFactory {
canSerialize(): boolean {
return true;
}
serialize(input: EditorInput): string {
assertType(input instanceof NotebookDiffEditorInput);
return JSON.stringify({
resource: input.resource,
originalResource: input.originalResource,
name: input.name,
originalName: input.originalName,
viewType: input.viewType,
});
}
deserialize(instantiationService: IInstantiationService, raw: string) {
type Data = { resource: URI, originalResource: URI, name: string, originalName: string, viewType: string, group: number };
const data = <Data>parse(raw);
if (!data) {
return undefined;
}
const { resource, originalResource, name, originalName, viewType } = data;
if (!data || !URI.isUri(resource) || !URI.isUri(originalResource) || typeof name !== 'string' || typeof originalName !== 'string' || typeof viewType !== 'string') {
return undefined;
}
const input = NotebookDiffEditorInput.create(instantiationService, resource, name, originalResource, originalName, viewType);
return input;
}
static canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean {
return false;
}
}
class NotebookEditorFactory implements IEditorInputFactory {
canSerialize(): boolean {
return true;
@@ -133,6 +196,11 @@ Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactor
NotebookEditorFactory
);
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(
NotebookDiffEditorInput.ID,
NotebookDiffEditorFactory
);
function getFirstNotebookInfo(notebookService: INotebookService, uri: URI): NotebookProviderInfo | undefined {
return notebookService.getContributedNotebookProviders(uri)[0];
}
@@ -144,6 +212,7 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri
@INotebookService private readonly notebookService: INotebookService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
@IUndoRedoService undoRedoService: IUndoRedoService,
) {
super();
@@ -225,6 +294,10 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri
return undefined;
}
if (originalInput instanceof DiffEditorInput && this.configurationService.getValue(NotebookTextDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized()) {
return this._handleDiffEditorInput(originalInput, options, group);
}
if (!originalInput.resource) {
return undefined;
}
@@ -300,6 +373,49 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri
const notebookOptions = new NotebookEditorOptions({ ...options, cellOptions, override: false, index });
return { override: this.editorService.openEditor(notebookInput, notebookOptions, group) };
}
private _handleDiffEditorInput(diffEditorInput: DiffEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined {
const modifiedInput = diffEditorInput.modifiedInput;
const originalInput = diffEditorInput.originalInput;
const notebookUri = modifiedInput.resource;
const originalNotebookUri = originalInput.resource;
if (!notebookUri || !originalNotebookUri) {
return undefined;
}
const existingEditors = group.editors.filter(editor => editor.resource && isEqual(editor.resource, notebookUri) && !(editor instanceof NotebookEditorInput));
if (existingEditors.length) {
return { override: this.editorService.openEditor(existingEditors[0]) };
}
const userAssociatedEditors = this.getUserAssociatedEditors(notebookUri);
const notebookEditor = userAssociatedEditors.filter(association => this.notebookService.getContributedNotebookProvider(association.viewType));
if (userAssociatedEditors.length && !notebookEditor.length) {
// user pick a non-notebook editor for this resource
return undefined;
}
// user might pick a notebook editor
const associatedEditors = distinct([
...this.getUserAssociatedNotebookEditors(notebookUri),
...(this.getContributedEditors(notebookUri).filter(editor => editor.priority === NotebookEditorPriority.default))
], editor => editor.id);
if (!associatedEditors.length) {
// there is no notebook editor contribution which is enabled by default
return undefined;
}
const info = associatedEditors[0];
const notebookInput = NotebookDiffEditorInput.create(this.instantiationService, notebookUri, modifiedInput.getName(), originalNotebookUri, originalInput.getName(), info.id);
const notebookOptions = new NotebookEditorOptions({ ...options, override: false });
return { override: this.editorService.openEditor(notebookInput, notebookOptions, group) };
}
}
class CellContentProvider implements ITextModelContentProvider {
@@ -371,12 +487,120 @@ class CellContentProvider implements ITextModelContentProvider {
}
}
class RegisterSchemasContribution extends Disposable implements IWorkbenchContribution {
constructor() {
super();
this.registerMetadataSchemas();
}
private registerMetadataSchemas(): void {
const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
const metadataSchema: IJSONSchema = {
properties: {
['language']: {
type: 'string',
description: 'The language for the cell'
},
['editable']: {
type: 'boolean',
description: `Controls whether a cell's editor is editable/readonly`
},
['runnable']: {
type: 'boolean',
description: 'Controls if the cell is executable'
},
['breakpointMargin']: {
type: 'boolean',
description: 'Controls if the cell has a margin to support the breakpoint UI'
},
['hasExecutionOrder']: {
type: 'boolean',
description: 'Whether the execution order indicator will be displayed'
},
['executionOrder']: {
type: 'number',
description: 'The order in which this cell was executed'
},
['statusMessage']: {
type: 'string',
description: `A status message to be shown in the cell's status bar`
},
['runState']: {
type: 'integer',
description: `The cell's current run state`
},
['runStartTime']: {
type: 'number',
description: 'If the cell is running, the time at which the cell started running'
},
['lastRunDuration']: {
type: 'number',
description: `The total duration of the cell's last run`
},
['inputCollapsed']: {
type: 'boolean',
description: `Whether a code cell's editor is collapsed`
},
['outputCollapsed']: {
type: 'boolean',
description: `Whether a code cell's outputs are collapsed`
}
},
// patternProperties: allSettings.patternProperties,
additionalProperties: true,
allowTrailingCommas: true,
allowComments: true
};
jsonRegistry.registerSchema('vscode://schemas/notebook/cellmetadata', metadataSchema);
}
}
// makes sure that every dirty notebook gets an editor
class NotebookFileTracker implements IWorkbenchContribution {
private readonly _dirtyListener: IDisposable;
constructor(
@INotebookService private readonly _notebookService: INotebookService,
@IEditorService private readonly _editorService: IEditorService,
@IWorkingCopyService workingCopyService: IWorkingCopyService,
) {
this._dirtyListener = Event.debounce(workingCopyService.onDidChangeDirty, () => { }, 100)(() => {
const inputs = this._createMissingNotebookEditors();
this._editorService.openEditors(inputs);
});
}
dispose(): void {
this._dirtyListener.dispose();
}
private _createMissingNotebookEditors(): IResourceEditorInput[] {
const result: IResourceEditorInput[] = [];
for (const notebook of this._notebookService.getNotebookTextModels()) {
if (notebook.isDirty && !this._editorService.isOpen({ resource: notebook.uri })) {
result.push({
resource: notebook.uri,
options: { inactive: true, preserveFocus: true, pinned: true }
});
}
}
return result;
}
}
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting);
workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting);
workbenchContributionsRegistry.registerWorkbenchContribution(RegisterSchemasContribution, LifecyclePhase.Starting);
workbenchContributionsRegistry.registerWorkbenchContribution(NotebookFileTracker, LifecyclePhase.Ready);
registerSingleton(INotebookService, NotebookService);
registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl);
registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverService, true);
registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true);
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
configurationRegistry.registerConfiguration({
@@ -398,6 +622,16 @@ configurationRegistry.registerConfiguration({
type: 'string',
enum: ['left', 'right', 'hidden'],
default: 'right'
},
[ShowCellStatusBarKey]: {
description: nls.localize('notebook.showCellStatusbar.description', "Whether the cell status bar should be shown."),
type: 'boolean',
default: true
},
[NotebookTextDiffEditorPreview]: {
description: nls.localize('notebook.diff.enablePreview.description', "Whether to use the enhanced text diff editor for notebook."),
type: 'boolean',
default: true
}
}
});

View File

@@ -22,15 +22,15 @@ import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/outpu
import { RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer';
import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernelInfo, IEditor, INotebookKernelInfo2, IInsetRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, IEditor, INotebookKernelInfo2, IInsetRenderOutput, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { Webview } from 'vs/workbench/contrib/webview/browser/webview';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { IMenu } from 'vs/platform/actions/common/actions';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { CellLanguageStatusBarItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents';
import { EditorOptions } from 'vs/workbench/common/editor';
import { IResourceEditorInput } from 'vs/platform/editor/common/editor';
import { IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation';
import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets';
export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey<boolean>('notebookFindWidgetFocused', false);
@@ -50,6 +50,7 @@ export const NOTEBOOK_VIEW_TYPE = new RawContextKey<string>('notebookViewType',
export const NOTEBOOK_CELL_TYPE = new RawContextKey<string>('notebookCellType', undefined); // code, markdown
export const NOTEBOOK_CELL_EDITABLE = new RawContextKey<boolean>('notebookCellEditable', false); // bool
export const NOTEBOOK_CELL_FOCUSED = new RawContextKey<boolean>('notebookCellFocused', false); // bool
export const NOTEBOOK_CELL_EDITOR_FOCUSED = new RawContextKey<boolean>('notebookCellEditorFocused', false); // bool
export const NOTEBOOK_CELL_RUNNABLE = new RawContextKey<boolean>('notebookCellRunnable', false); // bool
export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE = new RawContextKey<boolean>('notebookCellMarkdownEditMode', false); // bool
export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey<string>('notebookCellRunState', undefined); // idle, running
@@ -165,6 +166,7 @@ export interface INotebookEditorContribution {
export interface INotebookCellDecorationOptions {
className?: string;
gutterClassName?: string;
outputClassName?: string;
}
@@ -195,12 +197,13 @@ export interface INotebookEditorContributionDescription {
ctor: INotebookEditorContributionCtor;
}
export interface INotebookEditorWidgetOptions {
contributions?: INotebookEditorContributionDescription[];
export interface INotebookEditorCreationOptions {
readonly isEmbedded?: boolean;
readonly contributions?: INotebookEditorContributionDescription[];
}
export interface INotebookEditor extends IEditor {
isEmbedded: boolean;
cursorNavigationMode: boolean;
@@ -215,13 +218,14 @@ export interface INotebookEditor extends IEditor {
*/
readonly onDidChangeModel: Event<NotebookTextModel | undefined>;
readonly onDidFocusEditorWidget: Event<void>;
isNotebookEditor: boolean;
activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined;
readonly isNotebookEditor: boolean;
activeKernel: INotebookKernelInfo2 | undefined;
multipleKernelsAvailable: boolean;
readonly onDidChangeAvailableKernels: Event<void>;
readonly onDidChangeKernel: Event<void>;
readonly onDidChangeActiveCell: Event<void>;
readonly onDidScroll: Event<ScrollEvent>;
readonly onWillDispose: Event<void>;
isDisposed: boolean;
@@ -425,6 +429,8 @@ export interface INotebookEditor extends IEditor {
setCellSelection(cell: ICellViewModel, selection: Range): void;
deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[];
/**
* Change the decorations on cells.
* The notebook is virtualized and this method should be called to create/delete editor decorations safely.
@@ -452,7 +458,7 @@ export interface INotebookEditor extends IEditor {
}
export interface INotebookCellList {
isDisposed: boolean
isDisposed: boolean;
readonly contextKeyService: IContextKeyService;
elementAt(position: number): ICellViewModel | undefined;
elementHeight(element: ICellViewModel): number;
@@ -460,6 +466,8 @@ export interface INotebookCellList {
onDidScroll: Event<ScrollEvent>;
onDidChangeFocus: Event<IListEvent<ICellViewModel>>;
onDidChangeContentHeight: Event<number>;
onDidChangeVisibleRanges: Event<void>;
visibleRanges: ICellRange[];
scrollTop: number;
scrollHeight: number;
scrollLeft: number;
@@ -512,6 +520,7 @@ export interface BaseCellRenderTemplate {
contextKeyService: IContextKeyService;
container: HTMLElement;
cellContainer: HTMLElement;
decorationContainer: HTMLElement;
toolbar: ToolBar;
deleteToolbar: ToolBar;
betweenCellToolbar: ToolBar;
@@ -520,8 +529,7 @@ export interface BaseCellRenderTemplate {
elementDisposables: DisposableStore;
bottomCellContainer: HTMLElement;
currentRenderedCell?: ICellViewModel;
statusBarContainer: HTMLElement;
languageStatusBarItem: CellLanguageStatusBarItem;
statusBar: CellEditorStatusBar;
titleMenu: IMenu;
toJSON: () => object;
}
@@ -534,7 +542,6 @@ export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate {
export interface CodeCellRenderTemplate extends BaseCellRenderTemplate {
cellRunState: RunStateRenderer;
cellStatusMessageContainer: HTMLElement;
runToolbar: ToolBar;
runButtonContainer: HTMLElement;
executionOrderLabel: HTMLElement;
@@ -563,7 +570,7 @@ export interface IOutputTransformContribution {
* This call is allowed to have side effects, such as placing output
* directly into the container element.
*/
render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput;
render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput;
}
export interface CellFindMatch {
@@ -619,19 +626,20 @@ export interface CellViewModelStateChangeEvent {
outputIsHoveredChanged?: boolean;
}
/**
* [start, end]
*/
export interface ICellRange {
/**
* zero based index
*/
start: number;
export function cellRangesEqual(a: ICellRange[], b: ICellRange[]) {
a = reduceCellRanges(a);
b = reduceCellRanges(b);
if (a.length !== b.length) {
return false;
}
/**
* zero based index
*/
end: number;
for (let i = 0; i < a.length; i++) {
if (a[i].start !== b[i].start || a[i].end !== b[i].end) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ResourceMap } from 'vs/base/common/map';
import { URI } from 'vs/base/common/uri';
import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService';
import { INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon';
export class NotebookCellStatusBarService extends Disposable implements INotebookCellStatusBarService {
private _onDidChangeEntriesForCell = new Emitter<URI>();
readonly onDidChangeEntriesForCell: Event<URI> = this._onDidChangeEntriesForCell.event;
private _entries = new ResourceMap<Set<INotebookCellStatusBarEntry>>();
private removeEntry(entry: INotebookCellStatusBarEntry) {
const existingEntries = this._entries.get(entry.cellResource);
if (existingEntries) {
existingEntries.delete(entry);
if (!existingEntries.size) {
this._entries.delete(entry.cellResource);
}
}
this._onDidChangeEntriesForCell.fire(entry.cellResource);
}
addEntry(entry: INotebookCellStatusBarEntry): IDisposable {
const existingEntries = this._entries.get(entry.cellResource) ?? new Set();
existingEntries.add(entry);
this._entries.set(entry.cellResource, existingEntries);
this._onDidChangeEntriesForCell.fire(entry.cellResource);
return {
dispose: () => {
this.removeEntry(entry);
}
};
}
getEntries(cell: URI): INotebookCellStatusBarEntry[] {
const existingEntries = this._entries.get(cell);
return existingEntries ?
Array.from(existingEntries.values()) :
[];
}
readonly _serviceBrand: undefined;
}

View File

@@ -0,0 +1,228 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorModel } from 'vs/workbench/common/editor';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { URI } from 'vs/base/common/uri';
import { isEqual } from 'vs/base/common/resources';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService';
import { IReference } from 'vs/base/common/lifecycle';
import { INotebookEditorModel, INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel';
interface NotebookEditorInputOptions {
startDirty?: boolean;
}
class NotebookDiffEditorModel extends EditorModel implements INotebookDiffEditorModel {
constructor(
readonly original: NotebookEditorModel,
readonly modified: NotebookEditorModel,
) {
super();
}
async load(): Promise<NotebookDiffEditorModel> {
await this.original.load();
await this.modified.load();
return this;
}
async resolveOriginalFromDisk() {
await this.original.load({ forceReadFromDisk: true });
}
async resolveModifiedFromDisk() {
await this.modified.load({ forceReadFromDisk: true });
}
dispose(): void {
}
}
export class NotebookDiffEditorInput extends EditorInput {
static create(instantiationService: IInstantiationService, resource: URI, name: string, originalResource: URI, originalName: string, viewType: string | undefined, options: NotebookEditorInputOptions = {}) {
return instantiationService.createInstance(NotebookDiffEditorInput, resource, name, originalResource, originalName, viewType, options);
}
static readonly ID: string = 'workbench.input.diffNotebookInput';
private _textModel: IReference<INotebookEditorModel> | null = null;
private _originalTextModel: IReference<INotebookEditorModel> | null = null;
private _defaultDirtyState: boolean = false;
constructor(
public readonly resource: URI,
public readonly name: string,
public readonly originalResource: URI,
public readonly originalName: string,
public readonly viewType: string | undefined,
public readonly options: NotebookEditorInputOptions,
@INotebookService private readonly _notebookService: INotebookService,
@INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService,
@IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService,
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
// @IInstantiationService private readonly _instantiationService: IInstantiationService
) {
super();
this._defaultDirtyState = !!options.startDirty;
}
getTypeId(): string {
return NotebookDiffEditorInput.ID;
}
getName(): string {
return nls.localize('sideBySideLabels', "{0} ↔ {1}", this.originalName, this.name);
}
isDirty() {
if (!this._textModel) {
return !!this._defaultDirtyState;
}
return this._textModel.object.isDirty();
}
isUntitled(): boolean {
return this._textModel?.object.isUntitled() || false;
}
isReadonly() {
return false;
}
isSaving(): boolean {
if (this.isUntitled()) {
return false; // untitled is never saving automatically
}
if (!this.isDirty()) {
return false; // the editor needs to be dirty for being saved
}
if (this._filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) {
return true; // a short auto save is configured, treat this as being saved
}
return false;
}
async save(group: GroupIdentifier, options?: ISaveOptions): Promise<IEditorInput | undefined> {
if (this._textModel) {
if (this.isUntitled()) {
return this.saveAs(group, options);
} else {
await this._textModel.object.save();
}
return this;
}
return undefined;
}
async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<IEditorInput | undefined> {
if (!this._textModel || !this.viewType) {
return undefined;
}
const provider = this._notebookService.getContributedNotebookProvider(this.viewType!);
if (!provider) {
return undefined;
}
const dialogPath = this._textModel.object.resource;
const target = await this._fileDialogService.pickFileToSave(dialogPath, options?.availableFileSystems);
if (!target) {
return undefined; // save cancelled
}
if (!provider.matches(target)) {
const patterns = provider.selector.map(pattern => {
if (pattern.excludeFileNamePattern) {
return `${pattern.filenamePattern} (exclude: ${pattern.excludeFileNamePattern})`;
}
return pattern.filenamePattern;
}).join(', ');
throw new Error(`File name ${target} is not supported by ${provider.providerDisplayName}.
Please make sure the file name matches following patterns:
${patterns}
`);
}
if (!await this._textModel.object.saveAs(target)) {
return undefined;
}
return this._move(group, target)?.editor;
}
// called when users rename a notebook document
rename(group: GroupIdentifier, target: URI): IMoveResult | undefined {
if (this._textModel) {
const contributedNotebookProviders = this._notebookService.getContributedNotebookProviders(target);
if (contributedNotebookProviders.find(provider => provider.id === this._textModel!.object.viewType)) {
return this._move(group, target);
}
}
return undefined;
}
private _move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined {
return undefined;
}
async revert(group: GroupIdentifier, options?: IRevertOptions): Promise<void> {
if (this._textModel && this._textModel.object.isDirty()) {
await this._textModel.object.revert(options);
}
return;
}
async resolve(editorId?: string): Promise<INotebookDiffEditorModel | null> {
if (!await this._notebookService.canResolve(this.viewType!)) {
return null;
}
if (!this._textModel) {
this._textModel = await this._notebookModelResolverService.resolve(this.resource, this.viewType!, editorId);
this._originalTextModel = await this._notebookModelResolverService.resolve(this.originalResource, this.viewType!, editorId);
}
return new NotebookDiffEditorModel(this._originalTextModel!.object as NotebookEditorModel, this._textModel.object as NotebookEditorModel);
}
matches(otherInput: unknown): boolean {
if (this === otherInput) {
return true;
}
if (otherInput instanceof NotebookDiffEditorInput) {
return this.viewType === otherInput.viewType
&& isEqual(this.resource, otherInput.resource);
}
return false;
}
dispose() {
if (this._textModel) {
this._textModel.dispose();
this._textModel = null;
}
super.dispose();
}
}

View File

@@ -15,8 +15,8 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorOptions, IEditorInput, IEditorMemento } from 'vs/workbench/common/editor';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { EditorOptions, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor';
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget';
import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService';
@@ -28,7 +28,7 @@ import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/not
const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState';
export class NotebookEditor extends BaseEditor {
export class NotebookEditor extends EditorPane {
static readonly ID: string = 'workbench.editor.notebook';
private readonly _editorMemento: IEditorMemento<INotebookEditorViewState>;
@@ -74,7 +74,7 @@ export class NotebookEditor extends BaseEditor {
get minimumWidth(): number { return 375; }
get maximumWidth(): number { return Number.POSITIVE_INFINITY; }
// these setters need to exist because this extends from BaseEditor
// these setters need to exist because this extends from EditorPane
set minimumWidth(value: number) { /*noop*/ }
set maximumWidth(value: number) { /*noop*/ }
@@ -126,12 +126,12 @@ export class NotebookEditor extends BaseEditor {
this._widget.value?.focus();
}
async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
const group = this.group!;
this._saveEditorViewState(this.input);
await super.setInput(input, options, token);
await super.setInput(input, options, context, token);
// Check for cancellation
if (token.isCancellationRequested) {

View File

@@ -6,11 +6,17 @@
import { getZoomLevel } from 'vs/base/browser/browser';
import * as DOM from 'vs/base/browser/dom';
import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list';
import { IAction, Separator } from 'vs/base/common/actions';
import { SequencerByKey } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Color, RGBA } from 'vs/base/common/color';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { combinedDisposable, DisposableStore, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ScrollEvent } from 'vs/base/common/scrollable';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import 'vs/css!./media/notebook';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
@@ -18,47 +24,41 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
import { Range } from 'vs/editor/common/core/range';
import { IEditor } from 'vs/editor/common/editorCommon';
import * as nls from 'vs/nls';
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, errorForeground, transparent, listFocusBackground, listInactiveSelectionBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground } from 'vs/platform/theme/common/colorRegistry';
import { contrastBorder, diffInserted, diffRemoved, editorBackground, errorForeground, focusBorder, foreground, listFocusBackground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, transparent } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane';
import { IEditorMemento } from 'vs/workbench/common/editor';
import { CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, SCROLLABLE_ELEMENT_PADDING_TOP, BOTTOM_CELL_TOOLBAR_GAP, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, BOTTOM_CELL_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants';
import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED, INotebookDeltaDecoration, NotebookEditorOptions, INotebookEditorWidgetOptions, INotebookEditorContributionDescription } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { Memento, MementoObject } from 'vs/workbench/common/memento';
import { PANEL_BORDER } from 'vs/workbench/common/theme';
import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar';
import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants';
import { CellEditState, CellFocusMode, ICellViewModel, INotebookCellList, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation';
import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList';
import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer';
import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView';
import { CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate, ListTopCellToolbar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer';
import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys';
import { CodeCellRenderer, ListTopCellToolbar, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer';
import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher';
import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { CellKind, IProcessedOutput, INotebookKernelInfo, INotebookKernelInfoDto, INotebookKernelInfo2, NotebookRunState, NotebookCellRunState, IInsetRenderOutput, CellToolbarLocKey } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { CellKind, CellToolbarLocKey, ICellRange, IInsetRenderOutput, INotebookKernelInfo2, IProcessedOutput, isTransformedDisplayOutput, NotebookCellRunState, NotebookRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator';
import { Webview } from 'vs/workbench/contrib/webview/browser/webview';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { generateUuid } from 'vs/base/common/uuid';
import { Memento, MementoObject } from 'vs/workbench/common/memento';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { URI } from 'vs/base/common/uri';
import { PANEL_BORDER } from 'vs/workbench/common/theme';
import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar';
import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys';
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { notebookKernelProviderAssociationsSettingId, NotebookKernelProviderAssociations } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation';
import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { IAction, Separator } from 'vs/base/common/actions';
import { isMacintosh, isNative } from 'vs/base/common/platform';
import { getTitleBarStyle } from 'vs/platform/windows/common/windows';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ScrollEvent } from 'vs/base/common/scrollable';
import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd';
const $ = DOM.$;
@@ -98,6 +98,19 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
private readonly _activeKernelMemento: Memento;
private readonly _onDidFocusEmitter = this._register(new Emitter<void>());
public readonly onDidFocus = this._onDidFocusEmitter.event;
private readonly _onWillScroll = this._register(new Emitter<ScrollEvent>());
public readonly onWillScroll: Event<ScrollEvent> = this._onWillScroll.event;
private readonly _onWillDispose = this._register(new Emitter<void>());
public readonly onWillDispose: Event<void> = this._onWillDispose.event;
private readonly _insetModifyQueueByOutputId = new SequencerByKey<string>();
set scrollTop(top: number) {
if (this._list) {
this._list.scrollTop = top;
}
}
private _cellContextKeyManager: CellContextKeyManager | null = null;
private _isVisible = false;
private readonly _uuid = generateUuid();
@@ -132,7 +145,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return this._notebookViewModel?.notebookDocument;
}
private _activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined = undefined;
private _activeKernel: INotebookKernelInfo2 | undefined = undefined;
private readonly _onDidChangeKernel = this._register(new Emitter<void>());
readonly onDidChangeKernel: Event<void> = this._onDidChangeKernel.event;
private readonly _onDidChangeAvailableKernels = this._register(new Emitter<void>());
@@ -142,7 +155,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return this._activeKernel;
}
set activeKernel(kernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined) {
set activeKernel(kernel: INotebookKernelInfo2 | undefined) {
if (this._isDisposed) {
return;
}
@@ -203,19 +216,29 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._cursorNavigationMode = v;
}
private readonly _onDidChangeVisibleRanges = this._register(new Emitter<void>());
onDidChangeVisibleRanges: Event<void> = this._onDidChangeVisibleRanges.event;
get visibleRanges() {
return this._list?.visibleRanges || [];
}
readonly isEmbedded: boolean;
constructor(
private readonly editorWidgetOptions: INotebookEditorWidgetOptions,
readonly creationOptions: INotebookEditorCreationOptions,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
@INotebookService private notebookService: INotebookService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IContextKeyService readonly contextKeyService: IContextKeyService,
@ILayoutService private readonly layoutService: ILayoutService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@IMenuService private readonly menuService: IMenuService,
@IQuickInputService private readonly quickInputService: IQuickInputService
) {
super();
this.isEmbedded = creationOptions.isEmbedded || false;
this._memento = new Memento(NotebookEditorWidget.ID, storageService);
this._activeKernelMemento = new Memento(NotebookEditorActiveKernelCache, storageService);
@@ -231,7 +254,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
}
}
if (e.affectsConfiguration(CellToolbarLocKey)) {
if (e.affectsConfiguration(CellToolbarLocKey) || e.affectsConfiguration(ShowCellStatusBarKey)) {
this._updateForNotebookConfiguration();
}
});
@@ -285,6 +308,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
if (cellToolbarLocation === 'left' || cellToolbarLocation === 'right' || cellToolbarLocation === 'hidden') {
this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocation}`);
}
const showCellStatusBar = this.configurationService.getValue<boolean>(ShowCellStatusBarKey);
this._overlayContainer.classList.toggle('cell-statusbar-hidden', !showCellStatusBar);
}
updateEditorFocus() {
@@ -361,8 +387,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._notebookHasMultipleKernels.set(false);
let contributions: INotebookEditorContributionDescription[];
if (Array.isArray(this.editorWidgetOptions.contributions)) {
contributions = this.editorWidgetOptions.contributions;
if (Array.isArray(this.creationOptions.contributions)) {
contributions = this.creationOptions.contributions;
} else {
contributions = NotebookEditorExtensionsRegistry.getEditorContributions();
}
@@ -409,6 +435,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._list = this.instantiationService.createInstance(
NotebookCellList,
'NotebookCellList',
this._overlayContainer,
this._body,
this.instantiationService.createInstance(NotebookCellListDelegate),
renderers,
@@ -423,7 +450,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
multipleSelectionSupport: false,
enableKeyboardNavigation: true,
additionalScrollHeight: 0,
transformOptimization: (isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native',
transformOptimization: false, //(isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native',
styleController: (_suffix: string) => { return this._list!; },
overrideStyles: {
listBackground: editorBackground,
@@ -507,6 +534,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._onDidScroll.fire(e);
}));
this._register(this._list.onDidChangeVisibleRanges(() => {
this._onDidChangeVisibleRanges.fire();
}));
const widgetFocusTracker = DOM.trackFocus(this.getDomNode());
this._register(widgetFocusTracker);
this._register(widgetFocusTracker.onDidFocus(() => this._onDidFocusEmitter.fire()));
@@ -610,7 +641,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
// we don't await for it, otherwise it will slow down the file opening
this._setKernels(textModel, this._currentKernelTokenSource);
this._localStore.add(this.notebookService.onDidChangeKernels(async () => {
this._localStore.add(this.notebookService.onDidChangeKernels(async (e) => {
if (e && e.toString() !== this.textModel?.uri.toString()) {
// kernel update is not for current document.
return;
}
this._currentKernelTokenSource?.cancel();
this._currentKernelTokenSource = new CancellationTokenSource();
await this._setKernels(textModel, this._currentKernelTokenSource);
@@ -680,16 +715,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
const availableKernels = this.notebookService.getContributedNotebookKernels(textModel.viewType, textModel.uri);
if (tokenSource.token.isCancellationRequested) {
return;
}
if (provider.kernel && (availableKernels.length + availableKernels2.length) > 0) {
this._notebookHasMultipleKernels!.set(true);
this.multipleKernelsAvailable = true;
} else if ((availableKernels.length + availableKernels2.length) > 1) {
if ((availableKernels2.length) > 1) {
this._notebookHasMultipleKernels!.set(true);
this.multipleKernelsAvailable = true;
} else {
@@ -697,15 +727,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this.multipleKernelsAvailable = false;
}
// @deprecated
if (provider && provider.kernel) {
// it has a builtin kernel, don't automatically choose a kernel
await this._loadKernelPreloads(provider.providerExtensionLocation, provider.kernel);
tokenSource.dispose();
return;
}
const activeKernelStillExist = [...availableKernels2, ...availableKernels].find(kernel => kernel.id === this.activeKernel?.id && this.activeKernel?.id !== undefined);
const activeKernelStillExist = [...availableKernels2].find(kernel => kernel.id === this.activeKernel?.id && this.activeKernel?.id !== undefined);
if (activeKernelStillExist) {
// the kernel still exist, we don't want to modify the selection otherwise user's temporary preference is lost
@@ -717,10 +739,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
}
// the provider doesn't have a builtin kernel, choose a kernel
this.activeKernel = availableKernels[0];
if (this.activeKernel) {
await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel);
}
// this.activeKernel = availableKernels[0];
// if (this.activeKernel) {
// await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel);
// }
tokenSource.dispose();
}
@@ -809,7 +831,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
tokenSource.dispose();
}
private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfoDto) {
private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfo2) {
if (kernel.preloads && kernel.preloads.length) {
await this._resolveWebview();
this._webview?.updateKernelPreloads([extensionLocation], kernel.preloads.map(preload => URI.revive(preload)));
@@ -913,6 +935,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
}
this._localStore.add(this._list!.onWillScroll(e => {
this._onWillScroll.fire(e);
if (!this._webviewResolved) {
return;
}
@@ -1239,14 +1262,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
const index = cell ? this._notebookViewModel!.getCellIndex(cell) : 0;
const nextIndex = ui ? this._notebookViewModel!.getNextVisibleCellIndex(index) : index + 1;
const newLanguages = this._notebookViewModel!.languages;
const newLanguages = this._notebookViewModel!.resolvedLanguages;
const language = (cell?.cellKind === CellKind.Code && type === CellKind.Code)
? cell.language
: ((type === CellKind.Code && newLanguages && newLanguages.length) ? newLanguages[0] : 'markdown');
const insertIndex = cell ?
(direction === 'above' ? index : nextIndex) :
index;
const newCell = this._notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, undefined, true);
const focused = this._list?.getFocusedElements();
const newCell = this._notebookViewModel!.createCell(insertIndex, initialText, language, type, undefined, true, undefined, focused);
return newCell as CellViewModel;
}
@@ -1400,26 +1424,98 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return undefined;
}
private async _ensureActiveKernel() {
if (this._activeKernel) {
if (this._activeKernelResolvePromise) {
await this._activeKernelResolvePromise;
}
return;
}
// pick active kernel
const tokenSource = new CancellationTokenSource();
const availableKernels2 = await this.notebookService.getContributedNotebookKernels2(this.viewModel!.viewType, this.viewModel!.uri, tokenSource.token);
const picks: QuickPickInput<IQuickPickItem & { run(): void; kernelProviderId?: string; }>[] = availableKernels2.map((a) => {
return {
id: a.id,
label: a.label,
picked: false,
description:
a.description
? a.description
: a.extension.value,
detail: a.detail,
kernelProviderId: a.extension.value,
run: async () => {
this.activeKernel = a;
this._activeKernelResolvePromise = this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token);
},
buttons: [{
iconClass: 'codicon-settings-gear',
tooltip: nls.localize('notebook.promptKernel.setDefaultTooltip', "Set as default kernel provider for '{0}'", this.viewModel!.viewType)
}]
};
});
const picker = this.quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>();
picker.items = picks;
picker.placeholder = nls.localize('notebook.runCell.selectKernel', "Select a notebook kernel to run this notebook");
picker.matchOnDetail = true;
const pickedItem = await new Promise<(IQuickPickItem & { run(): void; kernelProviderId?: string; }) | undefined>(resolve => {
picker.onDidAccept(() => {
resolve(picker.selectedItems.length === 1 ? picker.selectedItems[0] : undefined);
picker.dispose();
});
picker.onDidTriggerItemButton(e => {
const pick = e.item;
const id = pick.id;
resolve(pick); // open the view
picker.dispose();
// And persist the setting
if (pick && id && pick.kernelProviderId) {
const newAssociation: NotebookKernelProviderAssociation = { viewType: this.viewModel!.viewType, kernelProvider: pick.kernelProviderId };
const currentAssociations = [...this.configurationService.getValue<NotebookKernelProviderAssociations>(notebookKernelProviderAssociationsSettingId)];
// First try updating existing association
for (let i = 0; i < currentAssociations.length; ++i) {
const existing = currentAssociations[i];
if (existing.viewType === newAssociation.viewType) {
currentAssociations.splice(i, 1, newAssociation);
this.configurationService.updateValue(notebookKernelProviderAssociationsSettingId, currentAssociations);
return;
}
}
// Otherwise, create a new one
currentAssociations.unshift(newAssociation);
this.configurationService.updateValue(notebookKernelProviderAssociationsSettingId, currentAssociations);
}
});
picker.show();
});
tokenSource.dispose();
if (pickedItem) {
await pickedItem.run();
}
return;
}
async cancelNotebookExecution(): Promise<void> {
if (this._notebookViewModel?.metadata.runState !== NotebookRunState.Running) {
return;
}
return this._cancelNotebookExecution();
}
private async _cancelNotebookExecution(): Promise<void> {
const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0];
if (provider) {
const viewType = provider.id;
const notebookUri = this._notebookViewModel!.uri;
if (this._activeKernel) {
await (this._activeKernel as INotebookKernelInfo2).cancelNotebookCell!(this._notebookViewModel!.uri, undefined);
} else if (provider.kernel) {
return await this.notebookService.cancelNotebook(viewType, notebookUri);
}
}
await this._ensureActiveKernel();
await this._activeKernel?.cancelNotebookCell!(this._notebookViewModel!.uri, undefined);
}
async executeNotebook(): Promise<void> {
@@ -1427,30 +1523,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
return this._executeNotebook();
}
private async _executeNotebook(): Promise<void> {
const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0];
if (provider) {
const viewType = provider.id;
const notebookUri = this._notebookViewModel!.uri;
if (this._activeKernel) {
// TODO@rebornix temp any cast, should be removed once we remove legacy kernel support
if ((this._activeKernel as INotebookKernelInfo2).executeNotebookCell) {
if (this._activeKernelResolvePromise) {
await this._activeKernelResolvePromise;
}
await (this._activeKernel as INotebookKernelInfo2).executeNotebookCell!(this._notebookViewModel!.uri, undefined);
} else {
await this.notebookService.executeNotebook2(this._notebookViewModel!.viewType, this._notebookViewModel!.uri, this._activeKernel.id);
}
} else if (provider.kernel) {
return await this.notebookService.executeNotebook(viewType, notebookUri);
}
}
await this._ensureActiveKernel();
await this._activeKernel?.executeNotebookCell!(this._notebookViewModel!.uri, undefined);
}
async cancelNotebookCellExecution(cell: ICellViewModel): Promise<void> {
@@ -1467,21 +1541,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
await this._cancelNotebookCell(cell);
}
private async _cancelNotebookCell(cell: ICellViewModel): Promise<void> {
const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0];
if (provider) {
const viewType = provider.id;
const notebookUri = this._notebookViewModel!.uri;
if (this._activeKernel) {
return await (this._activeKernel as INotebookKernelInfo2).cancelNotebookCell!(this._notebookViewModel!.uri, cell.handle);
} else if (provider.kernel) {
return await this.notebookService.cancelNotebookCell(viewType, notebookUri, cell.handle);
}
}
await this._ensureActiveKernel();
await this._activeKernel?.cancelNotebookCell!(this._notebookViewModel!.uri, cell.handle);
}
async executeNotebookCell(cell: ICellViewModel): Promise<void> {
@@ -1494,27 +1555,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
await this._executeNotebookCell(cell);
}
private async _executeNotebookCell(cell: ICellViewModel): Promise<void> {
const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0];
if (provider) {
const viewType = provider.id;
const notebookUri = this._notebookViewModel!.uri;
if (this._activeKernel) {
// TODO@rebornix temp any cast, should be removed once we remove legacy kernel support
if ((this._activeKernel as INotebookKernelInfo2).executeNotebookCell) {
await (this._activeKernel as INotebookKernelInfo2).executeNotebookCell!(this._notebookViewModel!.uri, cell.handle);
} else {
return await this.notebookService.executeNotebookCell2(viewType, notebookUri, cell.handle, this._activeKernel.id);
}
} else if (provider.kernel) {
return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle);
}
}
await this._ensureActiveKernel();
await this._activeKernel?.executeNotebookCell!(this._notebookViewModel!.uri, cell.handle);
}
focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') {
@@ -1584,30 +1626,37 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._list?.triggerScrollFromMouseWheelEvent(event);
}
async createInset(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number) {
if (!this._webview) {
return;
}
async createInset(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number): Promise<void> {
this._insetModifyQueueByOutputId.queue(output.source.outputId, async () => {
if (!this._webview) {
return;
}
await this._resolveWebview();
await this._resolveWebview();
if (!this._webview!.insetMapping.has(output.source)) {
const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0;
await this._webview!.createInset(cell, output, cellTop, offset);
} else {
const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0;
const scrollTop = this._list?.scrollTop || 0;
if (!this._webview!.insetMapping.has(output.source)) {
const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0;
await this._webview!.createInset(cell, output, cellTop, offset);
} else {
const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0;
const scrollTop = this._list?.scrollTop || 0;
this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell, output: output.source, cellTop }]);
}
this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell, output: output.source, cellTop }]);
}
});
}
removeInset(output: IProcessedOutput) {
if (!this._webview || !this._webviewResolved) {
if (!isTransformedDisplayOutput(output)) {
return;
}
this._webview!.removeInset(output);
this._insetModifyQueueByOutputId.queue(output.outputId, async () => {
if (!this._webview || !this._webviewResolved) {
return;
}
this._webview!.removeInset(output);
});
}
hideInset(output: IProcessedOutput) {
@@ -1615,7 +1664,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
this._webview!.hideInset(output);
if (!isTransformedDisplayOutput(output)) {
return;
}
this._insetModifyQueueByOutputId.queue(output.outputId, async () => {
this._webview!.hideInset(output);
});
}
getOutputRenderer(): OutputRenderer {
@@ -1658,6 +1713,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
dispose() {
this._isDisposed = true;
this._onWillDispose.fire();
// dispose webview first
this._webview?.dispose();
@@ -1786,7 +1842,8 @@ export const cellSymbolHighlight = registerColor('notebook.symbolHighlightBackgr
}, nls.localize('notebook.symbolHighlightBackground', "Background color of highlighted cell"));
registerThemingParticipant((theme, collector) => {
collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element {
collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element,
.notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element {
padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px;
box-sizing: border-box;
}`);
@@ -1864,10 +1921,10 @@ registerThemingParticipant((theme, collector) => {
}
const focusedCellBorderColor = theme.getColor(focusedCellBorder);
collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-top:before,
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-bottom:before,
.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused:before,
.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused:after {
collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-focus-indicator-top:before,
.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-focus-indicator-bottom:before,
.monaco-workbench .notebookOverlay .monaco-list:focus-within .markdown-cell-row.focused:before,
.monaco-workbench .notebookOverlay .monaco-list:focus-within .markdown-cell-row.focused:after {
border-color: ${focusedCellBorderColor} !important;
}`);
@@ -1907,7 +1964,8 @@ registerThemingParticipant((theme, collector) => {
const cellStatusBarHoverBg = theme.getColor(cellStatusBarItemHover);
if (cellStatusBarHoverBg) {
collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: ${cellStatusBarHoverBg}; }`);
collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker:hover,
.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item.cell-status-item-has-command:hover { background-color: ${cellStatusBarHoverBg}; }`);
}
const cellInsertionIndicatorColor = theme.getColor(cellInsertionIndicator);
@@ -1933,6 +1991,46 @@ registerThemingParticipant((theme, collector) => {
collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider.active:before { content: ""; width: 100%; height: 100%; position: absolute; background: ${scrollbarSliderActiveBackgroundColor}; } `); /* hack to not have cells see through scroller */
}
// case ChangeType.Modify: return theme.getColor(editorGutterModifiedBackground);
// case ChangeType.Add: return theme.getColor(editorGutterAddedBackground);
// case ChangeType.Delete: return theme.getColor(editorGutterDeletedBackground);
// diff
const modifiedBackground = theme.getColor(editorGutterModifiedBackground);
if (modifiedBackground) {
collector.addRule(`
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-modified .cell-focus-indicator {
background-color: ${modifiedBackground} !important;
}
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-modified {
background-color: ${modifiedBackground} !important;
}`);
}
const addedBackground = theme.getColor(diffInserted);
if (addedBackground) {
collector.addRule(`
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-added .cell-focus-indicator {
background-color: ${addedBackground} !important;
}
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-added {
background-color: ${addedBackground} !important;
}`);
}
const deletedBackground = theme.getColor(diffRemoved);
if (deletedBackground) {
collector.addRule(`
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-deleted .cell-focus-indicator {
background-color: ${deletedBackground} !important;
}
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-deleted {
background-color: ${deletedBackground} !important;
}`);
}
// Cell Margin
collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin: 0px ${CELL_MARGIN * 2}px 0px ${CELL_MARGIN}px; }`);
collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN}px; }`);

View File

@@ -126,7 +126,7 @@ class NotebookEditorWidgetService implements INotebookEditorWidgetService {
if (!value) {
// NEW widget
const instantiationService = accessor.get(IInstantiationService);
const widget = instantiationService.createInstance(NotebookEditorWidget, {});
const widget = instantiationService.createInstance(NotebookEditorWidget, { isEmbedded: false });
widget.createEditor();
const token = this._tokenPool++;
value = { widget, token };

View File

@@ -6,10 +6,9 @@
import { flatten } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import * as glob from 'vs/base/common/glob';
import { Iterable } from 'vs/base/common/iterator';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { basename } from 'vs/base/common/path';
import { ResourceMap } from 'vs/base/common/map';
import { URI } from 'vs/base/common/uri';
import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions';
import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard';
@@ -27,7 +26,7 @@ import { NotebookKernelProviderAssociationRegistry, NotebookViewTypesExtensionRe
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellOutputKind, CellUri, DisplayOrderKey, ICellEditOperation, IDisplayOutput, INotebookKernelInfo, INotebookKernelInfo2, INotebookKernelProvider, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellOutputsSplice, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellOutputKind, CellUri, DisplayOrderKey, ICellEditOperation, IDisplayOutput, INotebookKernelInfo2, INotebookKernelProvider, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellOutputsSplice, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer';
import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
@@ -35,10 +34,6 @@ import { ICustomEditorInfo, ICustomEditorViewTypesHandler, IEditorService } from
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
function MODEL_ID(resource: URI): string {
return resource.toString();
}
export class NotebookKernelProviderInfoStore extends Disposable {
private readonly _notebookKernelProviders: INotebookKernelProvider[] = [];
@@ -232,11 +227,10 @@ export class NotebookService extends Disposable implements INotebookService, ICu
declare readonly _serviceBrand: undefined;
static mainthreadNotebookDocumentHandle: number = 0;
private readonly _notebookProviders = new Map<string, { controller: IMainNotebookController, extensionData: NotebookExtensionDescription }>();
private readonly _notebookKernels = new Map<string, INotebookKernelInfo>();
notebookProviderInfoStore: NotebookProviderInfoStore;
notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore();
notebookKernelProviderInfoStore: NotebookKernelProviderInfoStore = new NotebookKernelProviderInfoStore();
private readonly _models = new Map<string, ModelData>();
private readonly _models = new ResourceMap<ModelData>();
private _onDidChangeActiveEditor = new Emitter<string | null>();
onDidChangeActiveEditor: Event<string | null> = this._onDidChangeActiveEditor.event;
private _activeEditorDisposables = new DisposableStore();
@@ -257,8 +251,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu
private readonly _onDidChangeViewTypes = new Emitter<void>();
onDidChangeViewTypes: Event<void> = this._onDidChangeViewTypes.event;
private readonly _onDidChangeKernels = new Emitter<void>();
onDidChangeKernels: Event<void> = this._onDidChangeKernels.event;
private readonly _onDidChangeKernels = new Emitter<URI | undefined>();
onDidChangeKernels: Event<URI | undefined> = this._onDidChangeKernels.event;
private readonly _onDidChangeNotebookActiveKernel = new Emitter<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }>();
onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }> = this._onDidChangeNotebookActiveKernel.event;
private cutItems: NotebookCellTextModel[] | undefined;
@@ -541,6 +535,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu
// notebook providers/kernels/renderers might use `*` as activation event.
await this._extensionService.activateByEvent(`*`);
// this awaits full activation of all matching extensions
await this._extensionService.activateByEvent(`onNotebook:${viewType}`);
// TODO@jrieken deprecated, remove this
await this._extensionService.activateByEvent(`onNotebookEditor:${viewType}`);
}
return this._notebookProviders.has(viewType);
@@ -548,7 +545,6 @@ export class NotebookService extends Disposable implements INotebookService, ICu
registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController) {
this._notebookProviders.set(viewType, { extensionData, controller });
this.notebookProviderInfoStore.get(viewType)!.kernel = controller.kernel;
this._onDidChangeViewTypes.fire();
}
@@ -557,23 +553,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu
this._onDidChangeViewTypes.fire();
}
registerNotebookKernel(notebook: INotebookKernelInfo): void {
this._notebookKernels.set(notebook.id, notebook);
this._onDidChangeKernels.fire();
}
unregisterNotebookKernel(id: string): void {
this._notebookKernels.delete(id);
this._onDidChangeKernels.fire();
}
registerNotebookKernelProvider(provider: INotebookKernelProvider): IDisposable {
const d = this.notebookKernelProviderInfoStore.add(provider);
const kernelChangeEventListener = provider.onDidChangeKernels(() => {
this._onDidChangeKernels.fire();
const kernelChangeEventListener = provider.onDidChangeKernels((e) => {
this._onDidChangeKernels.fire(e);
});
this._onDidChangeKernels.fire();
this._onDidChangeKernels.fire(undefined);
return toDisposable(() => {
kernelChangeEventListener.dispose();
d.dispose();
@@ -593,6 +579,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu
id: dto.id,
label: dto.label,
description: dto.description,
detail: dto.detail,
isPreferred: dto.isPreferred,
preloads: dto.preloads,
providerHandle: dto.providerHandle,
@@ -614,86 +601,41 @@ export class NotebookService extends Disposable implements INotebookService, ICu
return flatten(result);
}
getContributedNotebookKernels(viewType: string, resource: URI): INotebookKernelInfo[] {
let kernelInfos: INotebookKernelInfo[] = [];
this._notebookKernels.forEach(kernel => {
if (this._notebookKernelMatch(resource, kernel!.selectors)) {
kernelInfos.push(kernel!);
}
});
// sort by extensions
const notebookContentProvider = this._notebookProviders.get(viewType);
if (!notebookContentProvider) {
return kernelInfos;
}
kernelInfos = kernelInfos.sort((a, b) => {
if (a.extension.value === notebookContentProvider!.extensionData.id.value) {
return -1;
} else if (b.extension.value === notebookContentProvider!.extensionData.id.value) {
return 1;
} else {
return 0;
}
});
return kernelInfos;
}
private _notebookKernelMatch(resource: URI, selectors: (string | glob.IRelativePattern)[]): boolean {
for (let i = 0; i < selectors.length; i++) {
const pattern = typeof selectors[i] !== 'string' ? selectors[i] : selectors[i].toString();
if (glob.match(pattern, basename(resource.fsPath).toLowerCase())) {
return true;
}
}
return false;
}
getRendererInfo(id: string): INotebookRendererInfo | undefined {
return this.notebookRenderersInfoStore.get(id);
}
async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise<NotebookTextModel | undefined> {
const provider = this._notebookProviders.get(viewType);
if (!provider) {
return undefined;
async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise<NotebookTextModel> {
if (!await this.canResolve(viewType)) {
throw new Error(`CANNOT load notebook, no provider for '${viewType}'`);
}
const modelId = MODEL_ID(uri);
let notebookModel: NotebookTextModel | undefined = undefined;
if (this._models.has(modelId)) {
const provider = this._notebookProviders.get(viewType)!;
let notebookModel: NotebookTextModel;
if (this._models.has(uri)) {
// the model already exists
notebookModel = this._models.get(modelId)!.model;
notebookModel = this._models.get(uri)!.model;
if (forceReload) {
await provider.controller.reloadNotebook(notebookModel);
}
return notebookModel;
} else {
notebookModel = this._instantiationService.createInstance(NotebookTextModel, NotebookService.mainthreadNotebookDocumentHandle++, viewType, provider.controller.supportBackup, uri);
await provider.controller.createNotebook(notebookModel, backupId);
if (!notebookModel) {
return undefined;
}
}
// new notebook model created
const modelData = new ModelData(
notebookModel!,
notebookModel,
(model) => this._onWillDisposeDocument(model),
);
this._models.set(modelId, modelData);
this._onNotebookDocumentAdd.fire([notebookModel!.uri]);
this._models.set(uri, modelData);
this._onNotebookDocumentAdd.fire([notebookModel.uri]);
// after the document is added to the store and sent to ext host, we transform the ouputs
await this.transformTextModelOutputs(notebookModel!);
await this.transformTextModelOutputs(notebookModel);
if (editorId) {
await provider.controller.resolveNotebookEditor(viewType, uri, editorId);
@@ -703,9 +645,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu
}
getNotebookTextModel(uri: URI): NotebookTextModel | undefined {
const modelId = MODEL_ID(uri);
return this._models.get(uri)?.model;
}
return this._models.get(modelId)?.model;
getNotebookTextModels(): Iterable<NotebookTextModel> {
return Iterable.map(this._models.values(), data => data.model);
}
private async transformTextModelOutputs(textModel: NotebookTextModel) {
@@ -727,7 +671,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu
transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]) {
edits.forEach((edit) => {
if (edit.editType === CellEditType.Insert) {
if (edit.editType === CellEditType.Replace) {
edit.cells.forEach((cell) => {
const outputs = cell.outputs;
outputs.map((output) => {
@@ -740,6 +684,16 @@ export class NotebookService extends Disposable implements INotebookService, ICu
}
});
});
} else if (edit.editType === CellEditType.Output) {
edit.outputs.map((output) => {
if (output.outputKind === CellOutputKind.Rich) {
const ret = this._transformMimeTypes(output, output.outputId, textModel.metadata.displayOrder as string[] || []);
const orderedMimeTypes = ret.orderedMimeTypes!;
const pickedMimeTypeIndex = ret.pickedMimeTypeIndex!;
output.pickedMimeTypeIndex = pickedMimeTypeIndex;
output.orderedMimeTypes = orderedMimeTypes;
}
});
}
});
}
@@ -811,54 +765,6 @@ export class NotebookService extends Disposable implements INotebookService, ICu
return this.notebookRenderersInfoStore.getContributedRenderer(mimeType);
}
async executeNotebook(viewType: string, uri: URI): Promise<void> {
const provider = this._notebookProviders.get(viewType);
if (provider) {
return provider.controller.executeNotebookByAttachedKernel(viewType, uri);
}
return;
}
async executeNotebookCell(viewType: string, uri: URI, handle: number): Promise<void> {
const provider = this._notebookProviders.get(viewType);
if (provider) {
await provider.controller.executeNotebookCell(uri, handle);
}
}
async cancelNotebook(viewType: string, uri: URI): Promise<void> {
const provider = this._notebookProviders.get(viewType);
if (provider) {
return provider.controller.cancelNotebookByAttachedKernel(viewType, uri);
}
return;
}
async cancelNotebookCell(viewType: string, uri: URI, handle: number): Promise<void> {
const provider = this._notebookProviders.get(viewType);
if (provider) {
await provider.controller.cancelNotebookCell(uri, handle);
}
}
async executeNotebook2(viewType: string, uri: URI, kernelId: string): Promise<void> {
const kernel = this._notebookKernels.get(kernelId);
if (kernel) {
await kernel.executeNotebook(viewType, uri, undefined);
}
}
async executeNotebookCell2(viewType: string, uri: URI, handle: number, kernelId: string): Promise<void> {
const kernel = this._notebookKernels.get(kernelId);
if (kernel) {
await kernel.executeNotebook(viewType, uri, handle);
}
}
getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[] {
return this.notebookProviderInfoStore.getContributedNotebook(resource);
}
@@ -1000,10 +906,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu
}
private _onWillDisposeDocument(model: INotebookTextModel): void {
const modelId = MODEL_ID(model.uri);
const modelData = this._models.get(modelId);
this._models.delete(modelId);
const modelData = this._models.get(model.uri);
this._models.delete(model.uri);
if (modelData) {
// delete editors and documents

View File

@@ -19,9 +19,9 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellRange, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { clamp } from 'vs/base/common/numbers';
import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants';
@@ -54,16 +54,37 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
private _hiddenRangeIds: string[] = [];
private hiddenRangesPrefixSum: PrefixSumComputer | null = null;
private readonly _onDidChangeVisibleRanges = new Emitter<void>();
onDidChangeVisibleRanges: Event<void> = this._onDidChangeVisibleRanges.event;
private _visibleRanges: ICellRange[] = [];
get visibleRanges() {
return this._visibleRanges;
}
set visibleRanges(ranges: ICellRange[]) {
if (cellRangesEqual(this._visibleRanges, ranges)) {
return;
}
this._visibleRanges = ranges;
this._onDidChangeVisibleRanges.fire();
}
private _isDisposed = false;
get isDisposed() {
return this._isDisposed;
}
private _isInLayout: boolean = false;
private readonly _focusNextPreviousDelegate: IFocusNextPreviousDelegate;
constructor(
private listUser: string,
parentContainer: HTMLElement,
container: HTMLElement,
delegate: IListVirtualDelegate<CellViewModel>,
renderers: IListRenderer<CellViewModel, BaseCellRenderTemplate>[],
@@ -151,6 +172,86 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
focus.focusMode = CellFocusMode.Editor;
}
}));
// update visibleRanges
const updateVisibleRanges = () => {
if (!this.view.length) {
return;
}
const top = this.getViewScrollTop();
const bottom = this.getViewScrollBottom();
const topViewIndex = clamp(this.view.indexAt(top), 0, this.view.length - 1);
const topElement = this.view.element(topViewIndex);
const topModelIndex = this._viewModel!.getCellIndex(topElement);
const bottomViewIndex = clamp(this.view.indexAt(bottom), 0, this.view.length - 1);
const bottomElement = this.view.element(bottomViewIndex);
const bottomModelIndex = this._viewModel!.getCellIndex(bottomElement);
if (bottomModelIndex - topModelIndex === bottomViewIndex - topViewIndex) {
this.visibleRanges = [{ start: topModelIndex, end: bottomModelIndex }];
} else {
let stack: number[] = [];
const ranges: ICellRange[] = [];
// there are hidden ranges
let index = topViewIndex;
let modelIndex = topModelIndex;
while (index <= bottomViewIndex) {
const accu = this.hiddenRangesPrefixSum!.getAccumulatedValue(index);
if (accu === modelIndex + 1) {
// no hidden area after it
if (stack.length) {
if (stack[stack.length - 1] === modelIndex - 1) {
ranges.push({ start: stack[stack.length - 1], end: modelIndex });
} else {
ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] });
}
}
stack.push(modelIndex);
index++;
modelIndex++;
} else {
// there are hidden ranges after it
if (stack.length) {
if (stack[stack.length - 1] === modelIndex - 1) {
ranges.push({ start: stack[stack.length - 1], end: modelIndex });
} else {
ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] });
}
}
stack.push(modelIndex);
index++;
modelIndex = accu;
}
}
if (stack.length) {
ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] });
}
this.visibleRanges = reduceCellRanges(ranges);
}
};
this._localDisposableStore.add(this.view.onDidChangeContentHeight(() => {
if (this._isInLayout) {
DOM.scheduleAtNextAnimationFrame(() => {
updateVisibleRanges();
});
}
updateVisibleRanges();
}));
this._localDisposableStore.add(this.view.onDidScroll(() => {
if (this._isInLayout) {
DOM.scheduleAtNextAnimationFrame(() => {
updateVisibleRanges();
});
}
updateVisibleRanges();
}));
}
elementAt(position: number): ICellViewModel | undefined {
@@ -374,7 +475,11 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
return;
}
const focusInside = DOM.isAncestor(document.activeElement, this.rowsContainer);
super.splice(start, deleteCount, elements);
if (focusInside) {
this.domFocus();
}
const selectionsLeft = [];
this._viewModel!.selectionHandles.forEach(handle => {
@@ -932,6 +1037,12 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
}
}
layout(height?: number, width?: number): void {
this._isInLayout = true;
super.layout(height, width);
this._isInLayout = false;
}
dispose() {
this._isDisposed = true;
this._viewModelStore.dispose();

View File

@@ -8,6 +8,7 @@ import { IProcessedOutput, IRenderOutput, RenderOutputType } from 'vs/workbench/
import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry';
import { onUnexpectedError } from 'vs/base/common/errors';
import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { URI } from 'vs/base/common/uri';
export class OutputRenderer {
protected readonly _contributions: { [key: string]: IOutputTransformContribution; };
@@ -41,11 +42,11 @@ export class OutputRenderer {
return { type: RenderOutputType.None, hasDynamicHeight: false };
}
render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput {
render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput {
const transform = this._mimeTypeMapping[output.outputKind];
if (transform) {
return transform.render(output, container, preferredMimeType);
return transform.render(output, container, preferredMimeType, notebookUri);
} else {
return this.renderNoop(output, container);
}

View File

@@ -17,10 +17,10 @@ import { URI } from 'vs/base/common/uri';
import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { handleANSIOutput } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform';
import { dirname } from 'vs/base/common/resources';
class RichRenderer implements IOutputTransformContribution {
private _mdRenderer: MarkdownRenderer;
private _richMimeTypeRenderers = new Map<string, (output: ITransformedDisplayOutputDto, container: HTMLElement) => IRenderOutput>();
private _richMimeTypeRenderers = new Map<string, (output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement) => IRenderOutput>();
constructor(
public notebookEditor: INotebookEditor,
@@ -29,7 +29,6 @@ class RichRenderer implements IOutputTransformContribution {
@IModeService private readonly modeService: IModeService,
@IThemeService private readonly themeService: IThemeService
) {
this._mdRenderer = instantiationService.createInstance(MarkdownRenderer, undefined);
this._richMimeTypeRenderers.set('application/json', this.renderJSON.bind(this));
this._richMimeTypeRenderers.set('application/javascript', this.renderJavaScript.bind(this));
this._richMimeTypeRenderers.set('text/html', this.renderHTML.bind(this));
@@ -41,7 +40,7 @@ class RichRenderer implements IOutputTransformContribution {
this._richMimeTypeRenderers.set('text/x-javascript', this.renderCode.bind(this));
}
render(output: ITransformedDisplayOutputDto, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput {
render(output: ITransformedDisplayOutputDto, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput {
if (!output.data) {
const contentNode = document.createElement('p');
contentNode.innerText = `No data could be found for output.`;
@@ -69,10 +68,10 @@ class RichRenderer implements IOutputTransformContribution {
}
const renderer = this._richMimeTypeRenderers.get(preferredMimeType);
return renderer!(output, container);
return renderer!(output, notebookUri, container);
}
renderJSON(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput {
renderJSON(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput {
const data = output.data['application/json'];
const str = JSON.stringify(data, null, '\t');
@@ -105,7 +104,7 @@ class RichRenderer implements IOutputTransformContribution {
return { type: RenderOutputType.None, hasDynamicHeight: true };
}
renderCode(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput {
renderCode(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput {
const data = output.data['text/x-javascript'];
const str = (isArray(data) ? data.join('') : data) as string;
@@ -138,7 +137,7 @@ class RichRenderer implements IOutputTransformContribution {
return { type: RenderOutputType.None, hasDynamicHeight: true };
}
renderJavaScript(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput {
renderJavaScript(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput {
const data = output.data['application/javascript'];
const str = isArray(data) ? data.join('') : data;
const scriptVal = `<script type="application/javascript">${str}</script>`;
@@ -150,7 +149,7 @@ class RichRenderer implements IOutputTransformContribution {
};
}
renderHTML(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput {
renderHTML(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput {
const data = output.data['text/html'];
const str = (isArray(data) ? data.join('') : data) as string;
return {
@@ -161,7 +160,7 @@ class RichRenderer implements IOutputTransformContribution {
};
}
renderSVG(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput {
renderSVG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput {
const data = output.data['image/svg+xml'];
const str = (isArray(data) ? data.join('') : data) as string;
return {
@@ -172,17 +171,18 @@ class RichRenderer implements IOutputTransformContribution {
};
}
renderMarkdown(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput {
renderMarkdown(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput {
const data = output.data['text/markdown'];
const str = (isArray(data) ? data.join('') : data) as string;
const mdOutput = document.createElement('div');
mdOutput.appendChild(this._mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }).element);
const mdRenderer = this.instantiationService.createInstance(MarkdownRenderer, dirname(notebookUri));
mdOutput.appendChild(mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }).element);
container.appendChild(mdOutput);
return { type: RenderOutputType.None, hasDynamicHeight: true };
}
renderPNG(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput {
renderPNG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput {
const image = document.createElement('img');
image.src = `data:image/png;base64,${output.data['image/png']}`;
const display = document.createElement('div');
@@ -192,7 +192,7 @@ class RichRenderer implements IOutputTransformContribution {
return { type: RenderOutputType.None, hasDynamicHeight: true };
}
renderJPEG(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput {
renderJPEG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput {
const image = document.createElement('img');
image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`;
const display = document.createElement('div');
@@ -202,7 +202,7 @@ class RichRenderer implements IOutputTransformContribution {
return { type: RenderOutputType.None, hasDynamicHeight: true };
}
renderPlainText(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput {
renderPlainText(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput {
const data = output.data['text/plain'];
const str = (isArray(data) ? data.join('') : data) as string;
const contentNode = DOM.$('.output-plaintext');

View File

@@ -45,7 +45,7 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemActi
const isPrimary = isPrimaryGroup(group);
if (isPrimary) {
const to = Array.isArray<IAction>(target) ? target : target.primary;
const to = Array.isArray(target) ? target : target.primary;
if (to.length > 0) {
to.push(new VerticalSeparator());
@@ -55,7 +55,7 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray<MenuItemActi
}
if (!isPrimary || alwaysFillSecondary) {
const to = Array.isArray<IAction>(target) ? target : target.secondary;
const to = Array.isArray(target) ? target : target.secondary;
if (to.length > 0) {
to.push(new Separator());

View File

@@ -6,7 +6,7 @@
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { INotebookTextModel, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel';
import { NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_HAS_OUTPUTS, CellViewModelStateChangeEvent, CellEditState, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_FOCUSED, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_HAS_OUTPUTS, CellViewModelStateChangeEvent, CellEditState, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_FOCUSED, INotebookEditor, NOTEBOOK_CELL_EDITOR_FOCUSED, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
@@ -18,6 +18,7 @@ export class CellContextKeyManager extends Disposable {
private cellEditable!: IContextKey<boolean>;
private cellRunnable!: IContextKey<boolean>;
private cellFocused!: IContextKey<boolean>;
private cellEditorFocused!: IContextKey<boolean>;
private cellRunState!: IContextKey<string>;
private cellHasOutputs!: IContextKey<boolean>;
private cellContentCollapsed!: IContextKey<boolean>;
@@ -40,6 +41,7 @@ export class CellContextKeyManager extends Disposable {
this.viewType = NOTEBOOK_VIEW_TYPE.bindTo(this.contextKeyService);
this.cellEditable = NOTEBOOK_CELL_EDITABLE.bindTo(this.contextKeyService);
this.cellFocused = NOTEBOOK_CELL_FOCUSED.bindTo(this.contextKeyService);
this.cellEditorFocused = NOTEBOOK_CELL_EDITOR_FOCUSED.bindTo(this.contextKeyService);
this.cellRunnable = NOTEBOOK_CELL_RUNNABLE.bindTo(this.contextKeyService);
this.markdownEditMode = NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.bindTo(this.contextKeyService);
this.cellRunState = NOTEBOOK_CELL_RUN_STATE.bindTo(this.contextKeyService);
@@ -90,6 +92,10 @@ export class CellContextKeyManager extends Disposable {
this.updateForEditState();
}
if (e.focusModeChanged) {
this.updateForFocusState();
}
// if (e.collapseStateChanged) {
// this.updateForCollapseState();
// }
@@ -97,7 +103,15 @@ export class CellContextKeyManager extends Disposable {
}
private updateForFocusState() {
const activeCell = this.notebookEditor.getActiveCell();
this.cellFocused.set(this.notebookEditor.getActiveCell() === this.element);
if (activeCell === this.element) {
this.cellEditorFocused.set(this.element.focusMode === CellFocusMode.Editor);
} else {
this.cellEditorFocused.set(false);
}
}
private updateForMetadata() {

View File

@@ -10,7 +10,7 @@ import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/lis
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IAction } from 'vs/base/common/actions';
import { renderCodicons } from 'vs/base/common/codicons';
import { renderCodiconsAsElement } from 'vs/base/browser/codicons';
import { Color } from 'vs/base/common/color';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
@@ -35,20 +35,21 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { BOTTOM_CELL_TOOLBAR_GAP, CELL_BOTTOM_MARGIN, CELL_TOP_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
import { BOTTOM_CELL_TOOLBAR_GAP, CELL_BOTTOM_MARGIN, CELL_TOP_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
import { CancelCellAction, DeleteCellAction, ExecuteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions';
import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, EXPAND_CELL_CONTENT_COMMAND_ID, ICellViewModel, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys';
import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus';
import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets';
import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell';
import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents';
import { CellDragAndDropController, DRAGGING_CLASS } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd';
import { StatefulMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel';
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
import { CellKind, NotebookCellMetadata, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellKind, NotebookCellMetadata, NotebookCellRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { createAndFillInActionBarActionsWithVerticalSeparators, VerticalSeparator, VerticalSeparatorViewItem } from './cellActionView';
import { CodiconActionViewItem, CellLanguageStatusBarItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents';
import { CellDragAndDropController, DRAGGING_CLASS } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd';
const $ = DOM.$;
@@ -82,10 +83,6 @@ export class NotebookCellListDelegate implements IListVirtualDelegate<CellViewMo
export class CellEditorOptions {
private static fixedEditorOptions: IEditorOptions = {
padding: {
top: EDITOR_TOP_PADDING,
bottom: EDITOR_BOTTOM_PADDING
},
scrollBeyondLastLine: false,
scrollbar: {
verticalScrollbarSize: 14,
@@ -115,17 +112,24 @@ export class CellEditorOptions {
constructor(configurationService: IConfigurationService, language: string) {
this.disposable = configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('editor')) {
if (e.affectsConfiguration('editor') || e.affectsConfiguration(ShowCellStatusBarKey)) {
this._value = computeEditorOptions();
this._onDidChange.fire(this.value);
}
});
const computeEditorOptions = () => {
const showCellStatusBar = configurationService.getValue<boolean>(ShowCellStatusBarKey);
const editorPadding = {
top: EDITOR_TOP_PADDING,
bottom: showCellStatusBar ? EDITOR_BOTTOM_PADDING : EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR
};
const editorOptions = deepClone(configurationService.getValue<IEditorOptions>('editor', { overrideIdentifier: language }));
const computed = {
...editorOptions,
...CellEditorOptions.fixedEditorOptions
...CellEditorOptions.fixedEditorOptions,
...{ padding: editorPadding }
};
if (!computed.folding) {
@@ -329,16 +333,15 @@ abstract class AbstractCellRenderer {
}
if (templateData.currentRenderedCell.metadata?.inputCollapsed) {
this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false });
this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false });
} else if (templateData.currentRenderedCell.metadata?.outputCollapsed) {
this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false });
this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false });
}
}));
}
protected setupCollapsedPart(container: HTMLElement): { collapsedPart: HTMLElement, expandButton: HTMLElement } {
const collapsedPart = DOM.append(container, $('.cell.cell-collapsed-part'));
collapsedPart.innerHTML = renderCodicons('$(unfold)');
const collapsedPart = DOM.append(container, $('.cell.cell-collapsed-part', undefined, ...renderCodiconsAsElement('$(unfold)')));
const expandButton = collapsedPart.querySelector('.codicon') as HTMLElement;
const keybinding = this.keybindingService.lookupKeybinding(EXPAND_CELL_CONTENT_COMMAND_ID);
let title = localize('cellExpandButtonLabel', "Expand");
@@ -379,7 +382,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR
const container = DOM.append(rootContainer, DOM.$('.cell-inner-container'));
const disposables = new DisposableStore();
const contextKeyService = disposables.add(this.contextKeyServiceProvider(container));
const decorationContainer = DOM.append(container, $('.cell-decoration'));
const titleToolbarContainer = DOM.append(container, $('.cell-title-toolbar'));
const toolbar = disposables.add(this.createToolbar(titleToolbarContainer));
const deleteToolbar = disposables.add(this.createToolbar(titleToolbarContainer, 'cell-delete-toolbar'));
@@ -400,7 +403,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR
const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container'));
const betweenCellToolbar = disposables.add(this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService));
const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart);
const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart));
const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService));
const templateData: MarkdownCellRenderTemplate = {
@@ -408,6 +411,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR
expandButton,
contextKeyService,
container,
decorationContainer,
cellContainer: innerContent,
editorPart,
editorContainer,
@@ -419,9 +423,8 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR
deleteToolbar,
betweenCellToolbar,
bottomCellContainer,
statusBarContainer: statusBar.statusBarContainer,
languageStatusBarItem: statusBar.languageStatusBarItem,
titleMenu,
statusBar,
toJSON: () => { return {}; }
};
this.dndController.registerDragHandle(templateData, rootContainer, container, () => this.getDragImage(templateData));
@@ -490,7 +493,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR
elementDisposables.add(this.editorOptions.onDidChange(newValue => markdownCell.updateEditorOptions(newValue)));
elementDisposables.add(markdownCell);
templateData.languageStatusBarItem.update(element, this.notebookEditor);
templateData.statusBar.update(toolbarContext);
}
disposeTemplate(templateData: MarkdownCellRenderTemplate): void {
@@ -602,27 +605,6 @@ class CodeCellDragImageRenderer {
}
}
class CellEditorStatusBar {
readonly cellStatusMessageContainer: HTMLElement;
readonly cellRunStatusContainer: HTMLElement;
readonly statusBarContainer: HTMLElement;
readonly languageStatusBarItem: CellLanguageStatusBarItem;
readonly durationContainer: HTMLElement;
constructor(
container: HTMLElement,
@IInstantiationService instantiationService: IInstantiationService
) {
this.statusBarContainer = DOM.append(container, $('.cell-statusbar-container'));
const leftStatusBarItems = DOM.append(this.statusBarContainer, $('.cell-status-left'));
const rightStatusBarItems = DOM.append(this.statusBarContainer, $('.cell-status-right'));
this.cellRunStatusContainer = DOM.append(leftStatusBarItems, $('.cell-run-status'));
this.durationContainer = DOM.append(leftStatusBarItems, $('.cell-run-duration'));
this.cellStatusMessageContainer = DOM.append(leftStatusBarItems, $('.cell-status-message'));
this.languageStatusBarItem = instantiationService.createInstance(CellLanguageStatusBarItem, rightStatusBarItems);
}
}
export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer<CodeCellViewModel, CodeCellRenderTemplate> {
static readonly TEMPLATE_ID = 'code_cell';
@@ -649,7 +631,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
const container = DOM.append(rootContainer, DOM.$('.cell-inner-container'));
const disposables = new DisposableStore();
const contextKeyService = disposables.add(this.contextKeyServiceProvider(container));
const decorationContainer = DOM.append(container, $('.cell-decoration'));
DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-top'));
const titleToolbarContainer = DOM.append(container, $('.cell-title-toolbar'));
const toolbar = disposables.add(this.createToolbar(titleToolbarContainer));
@@ -678,7 +660,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
width: 0,
height: 0
},
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode()
// overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode()
}, {});
disposables.add(this.editorOptions.onDidChange(newValue => editor.updateOptions(newValue)));
@@ -689,7 +671,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
progressBar.hide();
disposables.add(progressBar);
const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart);
const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart));
const timer = new TimerRenderer(statusBar.durationContainer);
const cellRunState = new RunStateRenderer(statusBar.cellRunStatusContainer, runToolbar, this.instantiationService);
@@ -711,12 +693,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
expandButton,
contextKeyService,
container,
decorationContainer,
cellContainer,
statusBarContainer: statusBar.statusBarContainer,
cellRunState,
cellStatusMessageContainer: statusBar.cellStatusMessageContainer,
languageStatusBarItem: statusBar.languageStatusBarItem,
progressBar,
statusBar,
focusIndicatorLeft: focusIndicator,
focusIndicatorRight,
focusIndicatorBottom,
@@ -761,9 +742,9 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
private updateForMetadata(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void {
const metadata = element.getEvaluatedMetadata(this.notebookEditor.viewModel!.notebookDocument.metadata);
DOM.toggleClass(templateData.cellContainer, 'runnable', !!metadata.runnable);
DOM.toggleClass(templateData.container, 'runnable', !!metadata.runnable);
this.updateExecutionOrder(metadata, templateData);
templateData.cellStatusMessageContainer.textContent = metadata?.statusMessage || '';
templateData.statusBar.cellStatusMessageContainer.textContent = metadata?.statusMessage || '';
templateData.cellRunState.renderState(element.metadata?.runState);
@@ -877,7 +858,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende
this.setBetweenCellToolbarContext(templateData, element, toolbarContext);
templateData.languageStatusBarItem.update(element, this.notebookEditor);
templateData.statusBar.update(toolbarContext);
}
disposeTemplate(templateData: CodeCellRenderTemplate): void {
@@ -967,11 +948,11 @@ export class RunStateRenderer {
}
if (runState === NotebookCellRunState.Success) {
this.element.innerHTML = renderCodicons('$(check)');
DOM.reset(this.element, ...renderCodiconsAsElement('$(check)'));
} else if (runState === NotebookCellRunState.Error) {
this.element.innerHTML = renderCodicons('$(error)');
DOM.reset(this.element, ...renderCodiconsAsElement('$(error)'));
} else if (runState === NotebookCellRunState.Running) {
this.element.innerHTML = renderCodicons('$(sync~spin)');
DOM.reset(this.element, ...renderCodiconsAsElement('$(sync~spin)'));
this.spinnerTimer = setTimeout(() => {
this.spinnerTimer = undefined;

View File

@@ -0,0 +1,217 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel';
import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions';
import { stripCodicons } from 'vs/base/common/codicons';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { extUri } from 'vs/base/common/resources';
import { IModeService } from 'vs/editor/common/services/modeService';
import { localize } from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ChangeCellLanguageAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions';
import { ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService';
import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon';
const $ = DOM.$;
export class CellEditorStatusBar extends Disposable {
readonly cellStatusMessageContainer: HTMLElement;
readonly cellRunStatusContainer: HTMLElement;
readonly statusBarContainer: HTMLElement;
readonly languageStatusBarItem: CellLanguageStatusBarItem;
readonly durationContainer: HTMLElement;
private readonly leftContributedItemsContainer: HTMLElement;
private readonly rightContributedItemsContainer: HTMLElement;
private readonly itemsDisposable: DisposableStore;
private currentContext: INotebookCellActionContext | undefined;
constructor(
container: HTMLElement,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@INotebookCellStatusBarService private readonly notebookCellStatusBarService: INotebookCellStatusBarService
) {
super();
this.statusBarContainer = DOM.append(container, $('.cell-statusbar-container'));
const leftItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-left'));
const rightItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-right'));
this.cellRunStatusContainer = DOM.append(leftItemsContainer, $('.cell-run-status'));
this.durationContainer = DOM.append(leftItemsContainer, $('.cell-run-duration'));
this.cellStatusMessageContainer = DOM.append(leftItemsContainer, $('.cell-status-message'));
this.leftContributedItemsContainer = DOM.append(leftItemsContainer, $('.cell-contributed-items.cell-contributed-items-left'));
this.rightContributedItemsContainer = DOM.append(rightItemsContainer, $('.cell-contributed-items.cell-contributed-items-right'));
this.languageStatusBarItem = instantiationService.createInstance(CellLanguageStatusBarItem, rightItemsContainer);
this.itemsDisposable = this._register(new DisposableStore());
this._register(this.notebookCellStatusBarService.onDidChangeEntriesForCell(e => {
if (this.currentContext && extUri.isEqual(e, this.currentContext.cell.uri)) {
this.updateStatusBarItems();
}
}));
}
update(context: INotebookCellActionContext) {
this.currentContext = context;
this.languageStatusBarItem.update(context.cell, context.notebookEditor);
this.updateStatusBarItems();
}
layout(width: number): void {
this.statusBarContainer.style.width = `${width}px`;
}
private updateStatusBarItems() {
if (!this.currentContext) {
return;
}
this.leftContributedItemsContainer.innerHTML = '';
this.rightContributedItemsContainer.innerHTML = '';
this.itemsDisposable.clear();
const items = this.notebookCellStatusBarService.getEntries(this.currentContext.cell.uri);
items.sort((itemA, itemB) => {
return (itemB.priority ?? 0) - (itemA.priority ?? 0);
});
items.forEach(item => {
const itemView = this.itemsDisposable.add(this.instantiationService.createInstance(CellStatusBarItem, this.currentContext!, item));
if (item.alignment === CellStatusbarAlignment.LEFT) {
this.leftContributedItemsContainer.appendChild(itemView.container);
} else {
this.rightContributedItemsContainer.appendChild(itemView.container);
}
});
}
}
class CellStatusBarItem extends Disposable {
readonly container = $('.cell-status-item');
constructor(
private readonly _context: INotebookCellActionContext,
private readonly _itemModel: INotebookCellStatusBarEntry,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ICommandService private readonly commandService: ICommandService,
@INotificationService private readonly notificationService: INotificationService
) {
super();
new CodiconLabel(this.container).text = this._itemModel.text;
let ariaLabel: string;
let role: string | undefined;
if (this._itemModel.accessibilityInformation) {
ariaLabel = this._itemModel.accessibilityInformation.label;
role = this._itemModel.accessibilityInformation.role;
} else {
ariaLabel = this._itemModel.text ? stripCodicons(this._itemModel.text).trim() : '';
}
if (ariaLabel) {
this.container.setAttribute('aria-label', ariaLabel);
}
if (role) {
this.container.setAttribute('role', role);
}
this.container.title = this._itemModel.tooltip ?? '';
if (this._itemModel.command) {
this.container.classList.add('cell-status-item-has-command');
this.container.tabIndex = 0;
this._register(DOM.addDisposableListener(this.container, DOM.EventType.CLICK, _e => {
this.executeCommand();
}));
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, e => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) {
this.executeCommand();
}
}));
}
}
private async executeCommand(): Promise<void> {
const command = this._itemModel.command;
if (!command) {
return;
}
const id = typeof command === 'string' ? command : command.id;
const args = typeof command === 'string' ? [] : command.arguments ?? [];
args.unshift(this._context);
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id, from: 'cell status bar' });
try {
await this.commandService.executeCommand(id, ...args);
} catch (error) {
this.notificationService.error(toErrorMessage(error));
}
}
}
export class CellLanguageStatusBarItem extends Disposable {
private readonly labelElement: HTMLElement;
private cell: ICellViewModel | undefined;
private editor: INotebookEditor | undefined;
private cellDisposables: DisposableStore;
constructor(
readonly container: HTMLElement,
@IModeService private readonly modeService: IModeService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();
this.labelElement = DOM.append(container, $('.cell-language-picker.cell-status-item'));
this.labelElement.tabIndex = 0;
this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.CLICK, () => {
this.run();
}));
this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.KEY_UP, e => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) {
this.run();
}
}));
this._register(this.cellDisposables = new DisposableStore());
}
private run() {
this.instantiationService.invokeFunction(accessor => {
new ChangeCellLanguageAction().run(accessor, { notebookEditor: this.editor!, cell: this.cell! });
});
}
update(cell: ICellViewModel, editor: INotebookEditor): void {
this.cellDisposables.clear();
this.cell = cell;
this.editor = editor;
this.render();
this.cellDisposables.add(this.cell.model.onDidChangeLanguage(() => this.render()));
}
private render(): void {
const modeId = this.cell?.cellKind === CellKind.Markdown ? 'markdown' : this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language;
this.labelElement.textContent = this.modeService.getLanguageName(modeId) || this.modeService.getLanguageName('plaintext');
this.labelElement.title = localize('notebook.cell.status.language', "Select Cell Language Mode");
}
}

View File

@@ -4,21 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { raceCancellation } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IDimension } from 'vs/editor/common/editorCommon';
import { IModeService } from 'vs/editor/common/services/modeService';
import * as nls from 'vs/nls';
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
import { CellFocusMode, CodeCellRenderTemplate, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { CellOutputKind, IProcessedOutput, IRenderOutput, ITransformedDisplayOutputDto, BUILTIN_RENDERER_ID, RenderOutputType, outputHasDynamicHeight } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IDimension } from 'vs/editor/common/editorCommon';
import { BUILTIN_RENDERER_ID, CellOutputKind, CellUri, IInsetRenderOutput, IProcessedOutput, IRenderOutput, ITransformedDisplayOutputDto, outputHasDynamicHeight, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
interface IMimeTypeRenderer extends IQuickPickItem {
index: number;
@@ -32,6 +33,7 @@ interface IRenderedOutput {
export class CodeCell extends Disposable {
private outputResizeListeners = new Map<IProcessedOutput, DisposableStore>();
private outputElements = new Map<IProcessedOutput, IRenderedOutput>();
constructor(
private notebookEditor: INotebookEditor,
private viewCell: CodeCellViewModel,
@@ -193,7 +195,7 @@ export class CodeCell extends Disposable {
// newly added element
const currIndex = this.viewCell.outputs.indexOf(output);
this.renderOutput(output, currIndex, prevElement);
prevElement = this.outputElements.get(output)!.element;
prevElement = this.outputElements.get(output)?.element;
});
const editorHeight = templateData.editor!.getContentHeight();
@@ -323,13 +325,14 @@ export class CodeCell extends Disposable {
const renderedOutput = this.outputElements.get(currOutput);
if (renderedOutput) {
if (renderedOutput.renderResult.type !== RenderOutputType.None) {
// Show inset in webview, or render output that isn't rendered
// TODO@roblou skipHeightInit flag is a hack - the webview only sends the real height once. Don't wipe it out here.
this.renderOutput(currOutput, index, undefined, true);
this.notebookEditor.createInset(this.viewCell, renderedOutput.renderResult as IInsetRenderOutput, this.viewCell.getOutputOffset(index));
} else {
// Anything else, just update the height
this.viewCell.updateOutputHeight(index, renderedOutput.element.clientHeight);
}
} else {
// Wasn't previously rendered, render it now
this.renderOutput(currOutput, index);
}
}
@@ -338,6 +341,7 @@ export class CodeCell extends Disposable {
private viewUpdateInputCollapsed(): void {
DOM.hide(this.templateData.cellContainer);
DOM.hide(this.templateData.runButtonContainer);
DOM.show(this.templateData.collapsedPart);
DOM.show(this.templateData.outputContainer);
this.templateData.container.classList.toggle('collapsed', true);
@@ -355,6 +359,7 @@ export class CodeCell extends Disposable {
private viewUpdateOutputCollapsed(): void {
DOM.show(this.templateData.cellContainer);
DOM.show(this.templateData.runButtonContainer);
DOM.show(this.templateData.collapsedPart);
DOM.hide(this.templateData.outputContainer);
@@ -368,6 +373,7 @@ export class CodeCell extends Disposable {
private viewUpdateAllCollapsed(): void {
DOM.hide(this.templateData.cellContainer);
DOM.hide(this.templateData.runButtonContainer);
DOM.show(this.templateData.collapsedPart);
DOM.hide(this.templateData.outputContainer);
this.templateData.container.classList.toggle('collapsed', true);
@@ -382,6 +388,7 @@ export class CodeCell extends Disposable {
private viewUpdateExpanded(): void {
DOM.show(this.templateData.cellContainer);
DOM.show(this.templateData.runButtonContainer);
DOM.hide(this.templateData.collapsedPart);
DOM.show(this.templateData.outputContainer);
this.templateData.container.classList.toggle('collapsed', false);
@@ -394,7 +401,7 @@ export class CodeCell extends Disposable {
private layoutEditor(dimension: IDimension): void {
this.templateData.editor?.layout(dimension);
this.templateData.statusBarContainer.style.width = `${dimension.width}px`;
this.templateData.statusBar.layout(dimension.width);
}
private onCellWidthChange(): void {
@@ -430,7 +437,15 @@ export class CodeCell extends Disposable {
);
}
private renderOutput(currOutput: IProcessedOutput, index: number, beforeElement?: HTMLElement, skipHeightInit = false) {
private getNotebookUri(): URI | undefined {
return CellUri.parse(this.viewCell.uri)?.notebook;
}
private renderOutput(currOutput: IProcessedOutput, index: number, beforeElement?: HTMLElement) {
if (this.viewCell.metadata.outputCollapsed) {
return;
}
if (!this.outputResizeListeners.has(currOutput)) {
this.outputResizeListeners.set(currOutput, new DisposableStore());
}
@@ -476,16 +491,16 @@ export class CodeCell extends Disposable {
const renderer = this.notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId);
result = renderer
? { type: RenderOutputType.Extension, renderer, source: currOutput, mimeType: pickedMimeTypeRenderer.mimeType }
: this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType);
: this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType, this.getNotebookUri(),);
} else {
result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType);
result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType, this.getNotebookUri(),);
}
} else {
// for text and error, there is no mimetype
const innerContainer = DOM.$('.output-inner-container');
DOM.append(outputItemDiv, innerContainer);
result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, undefined);
result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, undefined, this.getNotebookUri(),);
}
if (!result) {
@@ -503,49 +518,47 @@ export class CodeCell extends Disposable {
if (result.type !== RenderOutputType.None) {
this.viewCell.selfSizeMonitoring = true;
this.notebookEditor.createInset(this.viewCell, result, this.viewCell.getOutputOffset(index));
this.notebookEditor.createInset(this.viewCell, result as any, this.viewCell.getOutputOffset(index));
} else {
DOM.addClass(outputItemDiv, 'foreground');
DOM.addClass(outputItemDiv, 'output-element');
outputItemDiv.style.position = 'absolute';
}
if (!skipHeightInit) {
if (outputHasDynamicHeight(result)) {
this.viewCell.selfSizeMonitoring = true;
if (outputHasDynamicHeight(result)) {
this.viewCell.selfSizeMonitoring = true;
const clientHeight = outputItemDiv.clientHeight;
const dimension = {
width: this.viewCell.layoutInfo.editorWidth,
height: clientHeight
};
const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => {
if (this.templateData.outputContainer && document.body.contains(this.templateData.outputContainer!)) {
const height = Math.ceil(elementSizeObserver.getHeight());
const clientHeight = outputItemDiv.clientHeight;
const dimension = {
width: this.viewCell.layoutInfo.editorWidth,
height: clientHeight
};
const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => {
if (this.templateData.outputContainer && document.body.contains(this.templateData.outputContainer!)) {
const height = Math.ceil(elementSizeObserver.getHeight());
if (clientHeight === height) {
return;
}
const currIndex = this.viewCell.outputs.indexOf(currOutput);
if (currIndex < 0) {
return;
}
this.viewCell.updateOutputHeight(currIndex, height);
this.relayoutCell();
if (clientHeight === height) {
return;
}
});
elementSizeObserver.startObserving();
this.outputResizeListeners.get(currOutput)!.add(elementSizeObserver);
this.viewCell.updateOutputHeight(index, clientHeight);
} else if (result.type !== RenderOutputType.None) { // no-op if it's a webview
const clientHeight = Math.ceil(outputItemDiv.clientHeight);
this.viewCell.updateOutputHeight(index, clientHeight);
const top = this.viewCell.getOutputOffsetInContainer(index);
outputItemDiv.style.top = `${top}px`;
}
const currIndex = this.viewCell.outputs.indexOf(currOutput);
if (currIndex < 0) {
return;
}
this.viewCell.updateOutputHeight(currIndex, height);
this.relayoutCell();
}
});
elementSizeObserver.startObserving();
this.outputResizeListeners.get(currOutput)!.add(elementSizeObserver);
this.viewCell.updateOutputHeight(index, clientHeight);
} else if (result.type === RenderOutputType.None) { // no-op if it's a webview
const clientHeight = Math.ceil(outputItemDiv.clientHeight);
this.viewCell.updateOutputHeight(index, clientHeight);
const top = this.viewCell.getOutputOffsetInContainer(index);
outputItemDiv.style.top = `${top}px`;
}
}

View File

@@ -3,22 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { MenuItemAction } from 'vs/platform/actions/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { renderCodicons } from 'vs/base/common/codicons';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ChangeCellLanguageAction } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
const $ = DOM.$;
import { renderCodiconsAsElement } from 'vs/base/browser/codicons';
export class CodiconActionViewItem extends MenuEntryActionViewItem {
constructor(
@@ -31,49 +22,7 @@ export class CodiconActionViewItem extends MenuEntryActionViewItem {
}
updateLabel(): void {
if (this.options.label && this.label) {
this.label.innerHTML = renderCodicons(this._commandAction.label ?? '');
DOM.reset(this.label, ...renderCodiconsAsElement(this._commandAction.label ?? ''));
}
}
}
export class CellLanguageStatusBarItem extends Disposable {
private readonly labelElement: HTMLElement;
private cell: ICellViewModel | undefined;
private editor: INotebookEditor | undefined;
private cellDisposables: DisposableStore;
constructor(
readonly container: HTMLElement,
@IModeService private readonly modeService: IModeService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();
this.labelElement = DOM.append(container, $('.cell-language-picker'));
this.labelElement.tabIndex = 0;
this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.CLICK, () => {
this.instantiationService.invokeFunction(accessor => {
new ChangeCellLanguageAction().run(accessor, { notebookEditor: this.editor!, cell: this.cell! });
});
}));
this._register(this.cellDisposables = new DisposableStore());
}
update(cell: ICellViewModel, editor: INotebookEditor): void {
this.cellDisposables.clear();
this.cell = cell;
this.editor = editor;
this.render();
this.cellDisposables.add(this.cell.model.onDidChangeLanguage(() => this.render()));
}
private render(): void {
const modeId = this.cell?.cellKind === CellKind.Markdown ? 'markdown' : this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language;
this.labelElement.textContent = this.modeService.getLanguageName(modeId) || this.modeService.getLanguageName('plaintext');
this.labelElement.title = localize('notebook.cell.status.language', "Select Cell Language Mode");
}
}

View File

@@ -6,7 +6,7 @@
import * as DOM from 'vs/base/browser/dom';
import { raceCancellation } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { renderCodicons } from 'vs/base/common/codicons';
import { renderCodiconsAsElement } from 'vs/base/browser/codicons';
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
@@ -190,7 +190,7 @@ export class StatefulMarkdownCell extends Disposable {
width: width,
height: editorHeight
},
overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode()
// overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode()
}, {});
this.templateData.currentEditor = this.editor;
@@ -282,7 +282,7 @@ export class StatefulMarkdownCell extends Disposable {
private layoutEditor(dimension: DOM.IDimension): void {
this.editor?.layout(dimension);
this.templateData.statusBarContainer.style.width = `${dimension.width}px`;
this.templateData.statusBar.layout(dimension.width);
}
private onCellEditorWidthChange(): void {
@@ -315,10 +315,10 @@ export class StatefulMarkdownCell extends Disposable {
this.templateData.foldingIndicator.innerText = '';
break;
case CellFoldingState.Collapsed:
this.templateData.foldingIndicator.innerHTML = renderCodicons('$(chevron-right)');
DOM.reset(this.templateData.foldingIndicator, ...renderCodiconsAsElement('$(chevron-right)'));
break;
case CellFoldingState.Expanded:
this.templateData.foldingIndicator.innerHTML = renderCodicons('$(chevron-down)');
DOM.reset(this.templateData.foldingIndicator, ...renderCodiconsAsElement('$(chevron-down)'));
break;
default:

View File

@@ -12,12 +12,14 @@ import { IPosition } from 'vs/editor/common/core/position';
import * as editorCommon from 'vs/editor/common/editorCommon';
import * as model from 'vs/editor/common/model';
import { SearchParams } from 'vs/editor/common/model/textModelSearch';
import { EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
import { CELL_STATUSBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
import { CellEditState, CellFocusMode, CursorAtBoundary, CellViewModelStateChangeEvent, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata, INotebookSearchOptions, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export abstract class BaseCellViewModel extends Disposable {
protected readonly _onDidChangeEditorAttachState = new Emitter<void>();
// Do not merge this event with `onDidChangeState` as we are using `Event.once(onDidChangeEditorAttachState)` elsewhere.
readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event;
@@ -106,7 +108,12 @@ export abstract class BaseCellViewModel extends Disposable {
this._dragging = v;
}
constructor(readonly viewType: string, readonly model: NotebookCellTextModel, public id: string) {
constructor(
readonly viewType: string,
readonly model: NotebookCellTextModel,
public id: string,
private readonly _configurationService: IConfigurationService
) {
super();
this._register(model.onDidChangeLanguage(() => {
@@ -116,12 +123,24 @@ export abstract class BaseCellViewModel extends Disposable {
this._register(model.onDidChangeMetadata(() => {
this._onDidChangeState.fire({ metadataChanged: true });
}));
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(ShowCellStatusBarKey)) {
this.layoutChange({});
}
}));
}
protected getEditorStatusbarHeight() {
const showCellStatusBar = this._configurationService.getValue<boolean>(ShowCellStatusBarKey);
return showCellStatusBar ? CELL_STATUSBAR_HEIGHT : 0;
}
// abstract resolveTextModel(): Promise<model.ITextModel>;
abstract hasDynamicHeight(): boolean;
abstract getHeight(lineHeight: number): number;
abstract onDeselect(): void;
abstract layoutChange(change: any): void;
assertTextModelAttached(): boolean {
if (this.textModel && this._textEditor && this._textEditor.getModel() === this.textModel) {

View File

@@ -8,12 +8,13 @@ import * as UUID from 'vs/base/common/uuid';
import * as editorCommon from 'vs/editor/common/editorCommon';
import * as model from 'vs/editor/common/model';
import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer';
import { BOTTOM_CELL_TOOLBAR_GAP, CELL_MARGIN, CELL_RUN_GUTTER, CELL_STATUSBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, CELL_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, BOTTOM_CELL_TOOLBAR_HEIGHT, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants';
import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellViewModel, NotebookLayoutInfo, CodeCellLayoutState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { CellKind, NotebookCellOutputsSplice, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { BaseCellViewModel } from './baseCellViewModel';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, CodeCellLayoutState, ICellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { CellKind, INotebookSearchOptions, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { BaseCellViewModel } from './baseCellViewModel';
export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel {
readonly cellKind = CellKind.Code;
@@ -68,9 +69,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
readonly viewType: string,
readonly model: NotebookCellTextModel,
initialNotebookLayoutInfo: NotebookLayoutInfo | null,
readonly eventDispatcher: NotebookEventDispatcher
readonly eventDispatcher: NotebookEventDispatcher,
@IConfigurationService configurationService: IConfigurationService
) {
super(viewType, model, UUID.generateUuid());
super(viewType, model, UUID.generateUuid(), configurationService);
this._register(this.model.onDidChangeOutputs((splices) => {
this._outputCollection = new Array(this.model.outputs.length);
this._outputsTop = null;
@@ -121,8 +123,9 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
newState = CodeCellLayoutState.Estimated;
}
const indicatorHeight = editorHeight + CELL_STATUSBAR_HEIGHT + outputTotalHeight;
const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + CELL_STATUSBAR_HEIGHT;
const statusbarHeight = this.getEditorStatusbarHeight();
const indicatorHeight = editorHeight + statusbarHeight + outputTotalHeight;
const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + statusbarHeight;
const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2;
const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth;
@@ -209,7 +212,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
}
private computeTotalHeight(editorHeight: number, outputsTotalHeight: number): number {
return EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + CELL_STATUSBAR_HEIGHT + outputsTotalHeight + BOTTOM_CELL_TOOLBAR_GAP + CELL_BOTTOM_MARGIN;
return EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + this.getEditorStatusbarHeight() + outputsTotalHeight + BOTTOM_CELL_TOOLBAR_GAP + CELL_BOTTOM_MARGIN;
}
/**
@@ -221,8 +224,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
this.textModel = ref.object.textEditorModel;
this._register(ref);
this._register(this.textModel.onDidChangeContent(() => {
this.editState = CellEditState.Editing;
this._onDidChangeState.fire({ contentChanged: true });
if (this.editState !== CellEditState.Editing) {
this.editState = CellEditState.Editing;
this._onDidChangeState.fire({ contentChanged: true });
}
}));
}

View File

@@ -70,3 +70,24 @@ export class NotebookEventDispatcher {
}
}
}
export class NotebookDiffEditorEventDispatcher {
protected readonly _onDidChangeLayout = new Emitter<NotebookLayoutChangedEvent>();
readonly onDidChangeLayout = this._onDidChangeLayout.event;
constructor() {
}
emit(events: NotebookViewEvent[]) {
for (let i = 0, len = events.length; i < len; i++) {
const e = events[i];
switch (e.type) {
case NotebookViewEventType.LayoutChanged:
this._onDidChangeLayout.fire(e);
break;
}
}
}
}

View File

@@ -3,19 +3,20 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Emitter, Event } from 'vs/base/common/event';
import * as UUID from 'vs/base/common/uuid';
import * as editorCommon from 'vs/editor/common/editorCommon';
import * as model from 'vs/editor/common/model';
import { BOTTOM_CELL_TOOLBAR_GAP, CELL_MARGIN, CELL_STATUSBAR_HEIGHT, CELL_TOP_MARGIN, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, BOTTOM_CELL_TOOLBAR_HEIGHT, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants';
import * as nls from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants';
import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel';
import { CellFindMatch, ICellViewModel, MarkdownCellLayoutChangeEvent, MarkdownCellLayoutInfo, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer';
import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel';
import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel';
import { NotebookCellStateChangedEvent, NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { CellKind, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookEventDispatcher, NotebookCellStateChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher';
export class MarkdownCellViewModel extends BaseCellViewModel implements ICellViewModel {
readonly cellKind = CellKind.Markdown;
@@ -45,7 +46,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie
set editorHeight(newHeight: number) {
this._editorHeight = newHeight;
this.totalHeight = this._editorHeight + CELL_TOP_MARGIN + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_GAP + CELL_STATUSBAR_HEIGHT;
this.totalHeight = this._editorHeight + CELL_TOP_MARGIN + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_GAP + this.getEditorStatusbarHeight();
}
get editorHeight() {
@@ -65,9 +66,10 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie
initialNotebookLayoutInfo: NotebookLayoutInfo | null,
readonly foldingDelegate: EditorFoldingStateDelegate,
readonly eventDispatcher: NotebookEventDispatcher,
private readonly _mdRenderer: MarkdownRenderer
private readonly _mdRenderer: MarkdownRenderer,
@IConfigurationService configurationService: IConfigurationService
) {
super(viewType, model, UUID.generateUuid());
super(viewType, model, UUID.generateUuid(), configurationService);
this._layoutInfo = {
editorHeight: 0,

View File

@@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import * as strings from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { Range } from 'vs/editor/common/core/range';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, IReadonlyTextBuffer } from 'vs/editor/common/model';
@@ -17,13 +17,13 @@ import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { WorkspaceTextEdit } from 'vs/editor/common/modes';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { CellEditState, CellFindMatch, ICellRange, ICellViewModel, NotebookLayoutInfo, IEditableCellViewModel, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellEditState, CellFindMatch, ICellViewModel, NotebookLayoutInfo, IEditableCellViewModel, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher';
import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel';
import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { CellKind, NotebookCellMetadata, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellKind, NotebookCellMetadata, INotebookSearchOptions, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer';
@@ -174,8 +174,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
return this._notebook.handle;
}
get languages() {
return this._notebook.languages;
get resolvedLanguages() {
return this._notebook.resolvedLanguages;
}
get uri() {
@@ -609,8 +609,9 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
return result;
}
createCell(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true) {
this._notebook.createCell2(index, source, language, type, metadata, synchronous, pushUndoStop, undefined, undefined);
createCell(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true, previouslyFocused: ICellViewModel[] = []) {
const beforeSelections = previouslyFocused.map(e => e.handle);
this._notebook.createCell2(index, source, language, type, metadata, synchronous, pushUndoStop, beforeSelections, undefined);
// TODO, rely on createCell to be sync
return this.viewCells[index];
}
@@ -755,7 +756,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
language,
kind,
{
createCell: (index: number, source: string | string[], language: string, type: CellKind) => {
createCell: (index: number, source: string, language: string, type: CellKind) => {
return this.createCell(index, source, language, type, undefined, true, false) as BaseCellViewModel;
},
deleteCell: (index: number) => {
@@ -1027,7 +1028,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
const viewCell = cell as CellViewModel;
this._lastNotebookEditResource.push(viewCell.uri);
return viewCell.resolveTextModel().then(() => {
this._bulkEditService.apply({ edits: [{ edit: { range: range, text: text }, resource: cell.uri }] }, { quotableLabel: 'Notebook Replace' });
this._bulkEditService.apply(
[new ResourceTextEdit(cell.uri, { range, text })],
{ quotableLabel: 'Notebook Replace' }
);
});
}
@@ -1051,7 +1055,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
return Promise.all(matches.map(match => {
return match.cell.resolveTextModel();
})).then(async () => {
this._bulkEditService.apply({ edits: textEdits }, { quotableLabel: 'Notebook Replace All' });
this._bulkEditService.apply(ResourceEdit.convert({ edits: textEdits }), { quotableLabel: 'Notebook Replace All' });
return;
});
}

View File

@@ -6,6 +6,7 @@
import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
import { URI } from 'vs/base/common/uri';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
/**
* It should not modify Undo/Redo stack
@@ -14,6 +15,7 @@ export interface ITextCellEditingDelegate {
insertCell?(index: number, cell: NotebookCellTextModel): void;
deleteCell?(index: number): void;
moveCell?(fromIndex: number, length: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void;
updateCellMetadata?(index: number, newMetadata: NotebookCellMetadata): void;
emitSelections(selections: number[]): void;
}
@@ -183,3 +185,33 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement {
}
}
}
export class CellMetadataEdit implements IResourceUndoRedoElement {
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
label: string = 'Update Cell Metadata';
constructor(
public resource: URI,
readonly index: number,
readonly oldMetadata: NotebookCellMetadata,
readonly newMetadata: NotebookCellMetadata,
private editingDelegate: ITextCellEditingDelegate,
) {
}
undo(): void {
if (!this.editingDelegate.updateCellMetadata) {
return;
}
this.editingDelegate.updateCellMetadata(this.index, this.oldMetadata);
}
redo(): void | Promise<void> {
if (!this.editingDelegate.updateCellMetadata) {
return;
}
this.editingDelegate.updateCellMetadata(this.index, this.newMetadata);
}
}

Some files were not shown because too many files have changed in this diff Show More