mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-04-05 11:30:29 -04:00
Merge from vscode 7eaf220cafb9d9e901370ffce02229171cbf3ea6
This commit is contained in:
committed by
Anthony Dresser
parent
39d9eed585
commit
a63578e6f7
60
src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts
Normal file
60
src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
167
src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts
Normal file
167
src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts
Normal 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);
|
||||
181
src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts
Normal file
181
src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
244
src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts
Normal file
244
src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts
Normal file
100
src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 += '→';
|
||||
result += '\u2192'; // →
|
||||
break;
|
||||
|
||||
case CharCode.Space:
|
||||
result += '·';
|
||||
break;
|
||||
|
||||
case CharCode.LessThan:
|
||||
result += '<';
|
||||
break;
|
||||
|
||||
case CharCode.GreaterThan:
|
||||
result += '>';
|
||||
break;
|
||||
|
||||
case CharCode.Ampersand:
|
||||
result += '&';
|
||||
result += '\u00B7'; // ·
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */ });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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'))
|
||||
});
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 */) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
: '';
|
||||
}
|
||||
|
||||
@@ -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!)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
167
src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts
Normal file
167
src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts
Normal 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);
|
||||
@@ -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',
|
||||
|
||||
932
src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts
Normal file
932
src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
31
src/vs/workbench/contrib/notebook/browser/diff/common.ts
Normal file
31
src/vs/workbench/contrib/notebook/browser/diff/common.ts
Normal 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;
|
||||
113
src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css
Normal file
113
src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css
Normal 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;
|
||||
}
|
||||
@@ -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]]);
|
||||
}
|
||||
});
|
||||
@@ -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; }`);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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; }`);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user