mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 21:55:38 +00:00
Merge remote-tracking branch 'origin/master' into octref/live-rename
This commit is contained in:
commit
196562bff2
142
.github/commands.yml
vendored
142
.github/commands.yml
vendored
|
@ -1,154 +1,12 @@
|
|||
# {
|
||||
# perform: true,
|
||||
# commands: [
|
||||
# {
|
||||
# type: 'comment',
|
||||
# name: 'findDuplicates',
|
||||
# allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'],
|
||||
# action: 'comment',
|
||||
# comment: "Potential duplicates:\n${potentialDuplicates}"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
|
||||
{
|
||||
perform: true,
|
||||
commands: [
|
||||
{
|
||||
type: 'comment',
|
||||
name: 'question',
|
||||
allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'],
|
||||
action: 'updateLabels',
|
||||
addLabel: '*question'
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '*question',
|
||||
allowTriggerByBot: true,
|
||||
action: 'close',
|
||||
comment: "Please ask your question on [StackOverflow](https://aka.ms/vscodestackoverflow). We have a great community over [there](https://aka.ms/vscodestackoverflow). They have already answered thousands of questions and are happy to answer yours as well. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!"
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '*dev-question',
|
||||
allowTriggerByBot: true,
|
||||
action: 'close',
|
||||
comment: "We have a great developer community [over on slack](https://aka.ms/vscode-dev-community) where extension authors help each other. This is a great place for you to ask questions and find support.\n\nHappy Coding!"
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '*extension-candidate',
|
||||
allowTriggerByBot: true,
|
||||
action: 'close',
|
||||
comment: "We try to keep VS Code lean and we think the functionality you're asking for is great for a VS Code extension. Maybe you can already find one that suits you in the [VS Code Marketplace](https://aka.ms/vscodemarketplace). Just in case, in a few simple steps you can get started [writing your own extension](https://aka.ms/vscodewritingextensions). See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!"
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '*not-reproducible',
|
||||
allowTriggerByBot: true,
|
||||
action: 'close',
|
||||
comment: "We closed this issue because we are unable to reproduce the problem with the steps you describe. Chances are we've already fixed your problem in a recent version of VS Code. If not, please ask us to reopen the issue and provide us with more detail. Our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines might help you with that.\n\nHappy Coding!"
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '*out-of-scope',
|
||||
allowTriggerByBot: true,
|
||||
action: 'close',
|
||||
comment: "We closed this issue because we don't plan to address it in the foreseeable future. You can find more detailed information about our decision-making process [here](https://aka.ms/vscode-out-of-scope). If you disagree and feel that this issue is crucial: We are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/vscoderoadmap) and [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nThanks for your understanding and happy coding!"
|
||||
},
|
||||
{
|
||||
type: 'comment',
|
||||
name: 'causedByExtension',
|
||||
allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'],
|
||||
action: 'updateLabels',
|
||||
addLabel: '*caused-by-extension'
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '*caused-by-extension',
|
||||
allowTriggerByBot: true,
|
||||
action: 'close',
|
||||
comment: "This issue is caused by an extension, please file it with the repository (or contact) the extension has linked in its overview in VS Code or the [marketplace](https://aka.ms/vscodemarketplace) for VS Code. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!"
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '*as-designed',
|
||||
allowTriggerByBot: true,
|
||||
action: 'close',
|
||||
comment: "The described behavior is how it is expected to work. If you disagree, please explain what is expected and what is not in more detail. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!"
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '*english-please',
|
||||
allowTriggerByBot: true,
|
||||
action: 'close',
|
||||
comment: "This issue is being closed because its description is not in English, that makes it hard for us to work on it. Please open a new issue with an English description. You might find [Bing Translator](https://www.bing.com/translator) useful."
|
||||
},
|
||||
{
|
||||
type: 'comment',
|
||||
name: 'duplicate',
|
||||
allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'],
|
||||
action: 'updateLabels',
|
||||
addLabel: '*duplicate'
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '*duplicate',
|
||||
allowTriggerByBot: true,
|
||||
action: 'close',
|
||||
comment: "Thanks for creating this issue! We figured it's covering the same as another one we already have. Thus, we closed this one as a duplicate. You can search for existing issues [here](https://aka.ms/vscodeissuesearch). See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!"
|
||||
},
|
||||
{
|
||||
type: 'comment',
|
||||
name: 'confirm',
|
||||
allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'],
|
||||
action: 'updateLabels',
|
||||
addLabel: 'confirmed',
|
||||
removeLabel: 'confirmation-pending'
|
||||
},
|
||||
{
|
||||
type: 'comment',
|
||||
name: 'confirmationPending',
|
||||
allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'],
|
||||
action: 'updateLabels',
|
||||
addLabel: 'confirmation-pending',
|
||||
removeLabel: 'confirmed'
|
||||
},
|
||||
{
|
||||
type: 'comment',
|
||||
name: 'findDuplicates',
|
||||
allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'],
|
||||
action: 'comment',
|
||||
comment: "Potential duplicates:\n${potentialDuplicates}"
|
||||
},
|
||||
{
|
||||
type: 'comment',
|
||||
name: 'needsMoreInfo',
|
||||
allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'],
|
||||
action: 'updateLabels',
|
||||
addLabel: 'needs more info',
|
||||
comment: "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines. Please take the time to review these and update the issue.\n\nHappy Coding!"
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '~needs more info',
|
||||
action: 'updateLabels',
|
||||
addLabel: 'needs more info',
|
||||
removeLabel: '~needs more info',
|
||||
comment: "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines. Please take the time to review these and update the issue.\n\nHappy Coding!"
|
||||
},
|
||||
{
|
||||
type: 'comment',
|
||||
name: 'a11ymas',
|
||||
allowUsers: ['AccessibilityTestingTeam-TCS', 'dixitsonali95', 'Mohini78', 'ChitrarupaSharma', 'mspatil110', 'umasarath52', 'v-umnaik'],
|
||||
action: 'updateLabels',
|
||||
addLabel: 'a11ymas'
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: '*off-topic',
|
||||
action: 'close',
|
||||
comment: "Thanks for creating this issue. We think this issue is unactionable or unrelated to the goals of this project. Please follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
10
.github/workflows/commands.yml
vendored
10
.github/workflows/commands.yml
vendored
|
@ -14,8 +14,8 @@ jobs:
|
|||
with:
|
||||
repository: 'JacksonKearl/vscode-triage-github-actions'
|
||||
ref: v2
|
||||
# - name: Run Commands
|
||||
# uses: ./commands
|
||||
# with:
|
||||
# token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}}
|
||||
# config-path: commands
|
||||
- name: Run Commands
|
||||
uses: ./commands
|
||||
with:
|
||||
token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}}
|
||||
config-path: commands
|
||||
|
|
24
.github/workflows/copycat.yml
vendored
24
.github/workflows/copycat.yml
vendored
|
@ -12,15 +12,15 @@ jobs:
|
|||
with:
|
||||
repository: 'JacksonKearl/vscode-triage-github-actions'
|
||||
ref: v2
|
||||
# - name: Run CopyCat (JacksonKearl/testissues)
|
||||
# uses: ./copycat
|
||||
# with:
|
||||
# token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}}
|
||||
# owner: JacksonKearl
|
||||
# repo: testissues
|
||||
# - name: Run CopyCat (chrmarti/testissues)
|
||||
# uses: ./copycat
|
||||
# with:
|
||||
# token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}}
|
||||
# owner: chrmarti
|
||||
# repo: testissues
|
||||
- name: Run CopyCat (JacksonKearl/testissues)
|
||||
uses: ./copycat
|
||||
with:
|
||||
token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}}
|
||||
owner: JacksonKearl
|
||||
repo: testissues
|
||||
- name: Run CopyCat (chrmarti/testissues)
|
||||
uses: ./copycat
|
||||
with:
|
||||
token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}}
|
||||
owner: chrmarti
|
||||
repo: testissues
|
||||
|
|
|
@ -122,6 +122,10 @@
|
|||
"name": "vs/workbench/contrib/preferences",
|
||||
"project": "vscode-workbench"
|
||||
},
|
||||
{
|
||||
"name": "vs/workbench/contrib/notebook",
|
||||
"project": "vscode-workbench"
|
||||
},
|
||||
{
|
||||
"name": "vs/workbench/contrib/quickaccess",
|
||||
"project": "vscode-workbench"
|
||||
|
|
|
@ -279,18 +279,32 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
|||
this.scrollableElement.triggerScrollFromMouseWheelEvent(browserEvent);
|
||||
}
|
||||
|
||||
updateElementHeight(index: number, size: number): void {
|
||||
updateElementHeight(index: number, size: number, anchorIndex: number | null): void {
|
||||
if (this.items[index].size === size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
|
||||
|
||||
const heightDiff = index < lastRenderRange.start ? size - this.items[index].size : 0;
|
||||
let heightDiff = 0;
|
||||
|
||||
if (index < lastRenderRange.start) {
|
||||
// do not scroll the viewport if resized element is out of viewport
|
||||
heightDiff = size - this.items[index].size;
|
||||
} else {
|
||||
if (anchorIndex !== null && anchorIndex > index && anchorIndex <= lastRenderRange.end) {
|
||||
// anchor in viewport
|
||||
// resized elemnet in viewport and above the anchor
|
||||
heightDiff = size - this.items[index].size;
|
||||
} else {
|
||||
heightDiff = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.rangeMap.splice(index, 1, [{ size: size }]);
|
||||
this.items[index].size = size;
|
||||
|
||||
this.render(lastRenderRange, this.lastRenderTop + heightDiff, this.lastRenderHeight, undefined, undefined, true);
|
||||
this.render(lastRenderRange, Math.max(0, this.lastRenderTop + heightDiff), this.lastRenderHeight, undefined, undefined, true);
|
||||
|
||||
this.eventuallyUpdateScrollDimensions();
|
||||
|
||||
|
@ -1134,6 +1148,10 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (!!this.virtualDelegate.hasDynamicHeight && !this.virtualDelegate.hasDynamicHeight(item.element)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const size = item.size;
|
||||
|
||||
if (!this.setRowHeight && item.row && item.row.domNode) {
|
||||
|
|
|
@ -1314,7 +1314,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
|||
}
|
||||
|
||||
updateElementHeight(index: number, size: number): void {
|
||||
this.view.updateElementHeight(index, size);
|
||||
this.view.updateElementHeight(index, size, null);
|
||||
}
|
||||
|
||||
rerender(): void {
|
||||
|
|
|
@ -49,7 +49,15 @@ export namespace Iterable {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function* map<T, R>(iterable: Iterable<T>, fn: (t: T) => R): IterableIterator<R> {
|
||||
export function* filter<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): Iterable<T> {
|
||||
for (const element of iterable) {
|
||||
if (predicate(element)) {
|
||||
return yield element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* map<T, R>(iterable: Iterable<T>, fn: (t: T) => R): Iterable<R> {
|
||||
for (const element of iterable) {
|
||||
return yield fn(element);
|
||||
}
|
||||
|
|
|
@ -1013,6 +1013,16 @@ export function isDiffEditor(thing: any): thing is IDiffEditor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*@internal
|
||||
*/
|
||||
export function isCompositeEditor(thing: any): thing is editorCommon.ICompositeCodeEditor {
|
||||
return thing
|
||||
&& typeof thing === 'object'
|
||||
&& typeof (<editorCommon.ICompositeCodeEditor>thing).onDidChangeActiveEditor === 'function';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*@internal
|
||||
*/
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { ConfigurationChangedEvent, IComputedEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { IPosition, Position } from 'vs/editor/common/core/position';
|
||||
|
@ -529,6 +530,24 @@ export interface IDiffEditor extends IEditor {
|
|||
getModifiedEditor(): IEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface ICompositeCodeEditor {
|
||||
|
||||
/**
|
||||
* An event that signals that the active editor has changed
|
||||
*/
|
||||
readonly onDidChangeActiveEditor: Event<ICompositeCodeEditor>;
|
||||
|
||||
/**
|
||||
* The active code editor iff any
|
||||
*/
|
||||
readonly activeCodeEditor: IEditor | undefined;
|
||||
// readonly editors: readonly ICodeEditor[] maybe supported with uris
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An editor contribution that gets created every time a new editor gets created and gets disposed when the editor gets disposed.
|
||||
*/
|
||||
|
|
|
@ -627,6 +627,12 @@ export interface ITextModel {
|
|||
*/
|
||||
equalsTextBuffer(other: ITextBuffer): boolean;
|
||||
|
||||
/**
|
||||
* Get the underling text buffer.
|
||||
* @internal
|
||||
*/
|
||||
getTextBuffer(): ITextBuffer;
|
||||
|
||||
/**
|
||||
* Get the text in a certain range.
|
||||
* @param range The range describing what text to get.
|
||||
|
|
|
@ -384,6 +384,11 @@ export class TextModel extends Disposable implements model.ITextModel {
|
|||
return this._buffer.equals(other);
|
||||
}
|
||||
|
||||
public getTextBuffer(): model.ITextBuffer {
|
||||
this._assertNotDisposed();
|
||||
return this._buffer;
|
||||
}
|
||||
|
||||
private _emitContentChangedEvent(rawChange: ModelRawContentChangedEvent, change: IModelContentChangedEvent): void {
|
||||
if (this._isDisposing) {
|
||||
// Do not confuse listeners by emitting any event after disposing
|
||||
|
|
|
@ -261,7 +261,7 @@ export class FindDecorations implements IDisposable {
|
|||
return result;
|
||||
}
|
||||
|
||||
private static readonly _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({
|
||||
public static readonly _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
zIndex: 13,
|
||||
className: 'currentFindMatch',
|
||||
|
@ -276,7 +276,7 @@ export class FindDecorations implements IDisposable {
|
|||
}
|
||||
});
|
||||
|
||||
private static readonly _FIND_MATCH_DECORATION = ModelDecorationOptions.register({
|
||||
public static readonly _FIND_MATCH_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'findMatch',
|
||||
showIfCollapsed: true,
|
||||
|
@ -290,7 +290,7 @@ export class FindDecorations implements IDisposable {
|
|||
}
|
||||
});
|
||||
|
||||
private static readonly _FIND_MATCH_NO_OVERVIEW_DECORATION = ModelDecorationOptions.register({
|
||||
public static readonly _FIND_MATCH_NO_OVERVIEW_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'findMatch',
|
||||
showIfCollapsed: true
|
||||
|
|
|
@ -437,14 +437,14 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable {
|
|||
KeybindingsRegistry.registerKeybindingRule({
|
||||
...item,
|
||||
id: command.id,
|
||||
when: ContextKeyExpr.and(command.precondition, item.when)
|
||||
when: command.precondition ? ContextKeyExpr.and(command.precondition, item.when) : item.when
|
||||
});
|
||||
}
|
||||
} else if (keybinding) {
|
||||
KeybindingsRegistry.registerKeybindingRule({
|
||||
...keybinding,
|
||||
id: command.id,
|
||||
when: ContextKeyExpr.and(command.precondition, keybinding.when)
|
||||
when: command.precondition ? ContextKeyExpr.and(command.precondition, keybinding.when) : keybinding.when
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ export interface IProgressNotificationOptions extends IProgressOptions {
|
|||
readonly primaryActions?: ReadonlyArray<IAction>;
|
||||
readonly secondaryActions?: ReadonlyArray<IAction>;
|
||||
readonly delay?: number;
|
||||
readonly silent?: boolean;
|
||||
}
|
||||
|
||||
export interface IProgressWindowOptions extends IProgressOptions {
|
||||
|
|
119
src/vs/vscode.proposed.d.ts
vendored
119
src/vs/vscode.proposed.d.ts
vendored
|
@ -1577,6 +1577,125 @@ declare module 'vscode' {
|
|||
|
||||
//#endregion
|
||||
|
||||
//#region Peng: Notebook
|
||||
|
||||
export enum CellKind {
|
||||
Markdown = 1,
|
||||
Code = 2
|
||||
}
|
||||
|
||||
export enum CellOutputKind {
|
||||
Text = 1,
|
||||
Error = 2,
|
||||
Rich = 3
|
||||
}
|
||||
|
||||
export interface CellStreamOutput {
|
||||
outputKind: CellOutputKind.Text;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface CellErrorOutput {
|
||||
outputKind: CellOutputKind.Error;
|
||||
/**
|
||||
* Exception Name
|
||||
*/
|
||||
ename: string;
|
||||
/**
|
||||
* Exception Value
|
||||
*/
|
||||
evalue: string;
|
||||
/**
|
||||
* Exception call stack
|
||||
*/
|
||||
traceback: string[];
|
||||
}
|
||||
|
||||
export interface CellDisplayOutput {
|
||||
outputKind: CellOutputKind.Rich;
|
||||
/**
|
||||
* { mime_type: value }
|
||||
*
|
||||
* Example:
|
||||
* ```json
|
||||
* {
|
||||
* "outputKind": vscode.CellOutputKind.Rich,
|
||||
* "data": {
|
||||
* "text/html": [
|
||||
* "<h1>Hello</h1>"
|
||||
* ],
|
||||
* "text/plain": [
|
||||
* "<IPython.lib.display.IFrame at 0x11dee3e80>"
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
data: { [key: string]: any };
|
||||
}
|
||||
|
||||
export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput;
|
||||
|
||||
export interface NotebookCell {
|
||||
readonly uri: Uri;
|
||||
handle: number;
|
||||
language: string;
|
||||
cellKind: CellKind;
|
||||
outputs: CellOutput[];
|
||||
getContent(): string;
|
||||
}
|
||||
|
||||
export interface NotebookDocument {
|
||||
readonly uri: Uri;
|
||||
readonly fileName: string;
|
||||
readonly isDirty: boolean;
|
||||
languages: string[];
|
||||
cells: NotebookCell[];
|
||||
displayOrder?: GlobPattern[];
|
||||
}
|
||||
|
||||
export interface NotebookEditor {
|
||||
readonly document: NotebookDocument;
|
||||
viewColumn?: ViewColumn;
|
||||
/**
|
||||
* Create a notebook cell. The cell is not inserted into current document when created. Extensions should insert the cell into the document by [TextDocument.cells](#TextDocument.cells)
|
||||
*/
|
||||
createCell(content: string, language: string, type: CellKind, outputs: CellOutput[]): NotebookCell;
|
||||
}
|
||||
|
||||
export interface NotebookProvider {
|
||||
resolveNotebook(editor: NotebookEditor): Promise<void>;
|
||||
executeCell(document: NotebookDocument, cell: NotebookCell | undefined): Promise<void>;
|
||||
save(document: NotebookDocument): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface NotebookOutputSelector {
|
||||
type: string;
|
||||
subTypes?: string[];
|
||||
}
|
||||
|
||||
export interface NotebookOutputRenderer {
|
||||
/**
|
||||
*
|
||||
* @returns HTML fragment. We can probably return `CellOutput` instead of string ?
|
||||
*
|
||||
*/
|
||||
render(document: NotebookDocument, cell: NotebookCell, output: CellOutput, mimeType: string): string;
|
||||
preloads?: Uri[];
|
||||
}
|
||||
|
||||
namespace window {
|
||||
export function registerNotebookProvider(
|
||||
notebookType: string,
|
||||
provider: NotebookProvider
|
||||
): Disposable;
|
||||
|
||||
export function registerNotebookOutputRenderer(type: string, outputSelector: NotebookOutputSelector, renderer: NotebookOutputRenderer): Disposable;
|
||||
|
||||
export let activeNotebookDocument: NotebookDocument | undefined;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region color theme access
|
||||
|
||||
/**
|
||||
|
|
|
@ -56,6 +56,7 @@ import './mainThreadWindow';
|
|||
import './mainThreadWebview';
|
||||
import './mainThreadWorkspace';
|
||||
import './mainThreadComments';
|
||||
import './mainThreadNotebook';
|
||||
import './mainThreadTask';
|
||||
import './mainThreadLabelService';
|
||||
import './mainThreadTunnelService';
|
||||
|
|
255
src/vs/workbench/api/browser/mainThreadNotebook.ts
Normal file
255
src/vs/workbench/api/browser/mainThreadNotebook.ts
Normal file
|
@ -0,0 +1,255 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService';
|
||||
import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
|
||||
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
|
||||
|
||||
export class MainThreadNotebookDocument extends Disposable {
|
||||
private _textModel: NotebookTextModel;
|
||||
|
||||
get textModel() {
|
||||
return this._textModel;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostNotebookShape,
|
||||
public handle: number,
|
||||
public viewType: string,
|
||||
public uri: URI
|
||||
) {
|
||||
super();
|
||||
this._textModel = new NotebookTextModel(handle, viewType, uri);
|
||||
}
|
||||
|
||||
async deleteCell(uri: URI, index: number): Promise<boolean> {
|
||||
let deleteExtHostCell = await this._proxy.$deleteCell(this.viewType, uri, index);
|
||||
if (deleteExtHostCell) {
|
||||
this._textModel.removeCell(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._textModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadNotebook)
|
||||
export class MainThreadNotebooks extends Disposable implements MainThreadNotebookShape {
|
||||
private readonly _notebookProviders = new Map<string, MainThreadNotebookController>();
|
||||
private readonly _proxy: ExtHostNotebookShape;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@INotebookService private _notebookService: INotebookService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
this._register(this._notebookService.onDidChangeActiveEditor(e => {
|
||||
this._proxy.$updateActiveEditor(e.viewType, e.uri);
|
||||
}));
|
||||
|
||||
let userOrder = this.configurationService.getValue<string[]>('notebook.displayOrder');
|
||||
this._proxy.$acceptDisplayOrder({
|
||||
defaultOrder: NOTEBOOK_DISPLAY_ORDER,
|
||||
userOrder: userOrder
|
||||
});
|
||||
|
||||
this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectedKeys.indexOf('notebook.displayOrder') >= 0) {
|
||||
let userOrder = this.configurationService.getValue<string[]>('notebook.displayOrder');
|
||||
|
||||
this._proxy.$acceptDisplayOrder({
|
||||
defaultOrder: NOTEBOOK_DISPLAY_ORDER,
|
||||
userOrder: userOrder
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise<void> {
|
||||
this._notebookService.registerNotebookRenderer(handle, extension, type, selectors, preloads.map(uri => URI.revive(uri)));
|
||||
}
|
||||
|
||||
async $unregisterNotebookRenderer(handle: number): Promise<void> {
|
||||
this._notebookService.unregisterNotebookRenderer(handle);
|
||||
}
|
||||
|
||||
async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise<void> {
|
||||
let controller = new MainThreadNotebookController(this._proxy, this, viewType);
|
||||
this._notebookProviders.set(viewType, controller);
|
||||
this._notebookService.registerNotebookController(viewType, extension, controller);
|
||||
return;
|
||||
}
|
||||
|
||||
async $unregisterNotebookProvider(viewType: string): Promise<void> {
|
||||
this._notebookProviders.delete(viewType);
|
||||
this._notebookService.unregisterNotebookProvider(viewType);
|
||||
return;
|
||||
}
|
||||
|
||||
async $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void> {
|
||||
let controller = this._notebookProviders.get(viewType);
|
||||
|
||||
if (controller) {
|
||||
controller.createNotebookDocument(handle, viewType, resource);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void> {
|
||||
let controller = this._notebookProviders.get(viewType);
|
||||
|
||||
if (controller) {
|
||||
controller.updateLanguages(resource, languages);
|
||||
}
|
||||
}
|
||||
|
||||
async resolveNotebook(viewType: string, uri: URI): Promise<number | undefined> {
|
||||
let handle = await this._proxy.$resolveNotebook(viewType, uri);
|
||||
return handle;
|
||||
}
|
||||
|
||||
async $spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise<void> {
|
||||
let controller = this._notebookProviders.get(viewType);
|
||||
controller?.spliceNotebookCells(resource, splices, renderers);
|
||||
}
|
||||
|
||||
async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise<void> {
|
||||
let controller = this._notebookProviders.get(viewType);
|
||||
controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers);
|
||||
}
|
||||
|
||||
async executeNotebook(viewType: string, uri: URI): Promise<void> {
|
||||
return this._proxy.$executeNotebook(viewType, uri, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export class MainThreadNotebookController implements IMainNotebookController {
|
||||
private _mapping: Map<string, MainThreadNotebookDocument> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostNotebookShape,
|
||||
private _mainThreadNotebook: MainThreadNotebooks,
|
||||
private _viewType: string
|
||||
) {
|
||||
}
|
||||
|
||||
async resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined> {
|
||||
// TODO: resolve notebook should wait for all notebook document destory operations to finish.
|
||||
let mainthreadNotebook = this._mapping.get(URI.from(uri).toString());
|
||||
|
||||
if (mainthreadNotebook) {
|
||||
return mainthreadNotebook.textModel;
|
||||
}
|
||||
|
||||
let notebookHandle = await this._mainThreadNotebook.resolveNotebook(viewType, uri);
|
||||
if (notebookHandle !== undefined) {
|
||||
mainthreadNotebook = this._mapping.get(URI.from(uri).toString());
|
||||
return mainthreadNotebook?.textModel;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
spliceNotebookCells(resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): void {
|
||||
let mainthreadNotebook = this._mapping.get(URI.from(resource).toString());
|
||||
mainthreadNotebook?.textModel.updateRenderers(renderers);
|
||||
mainthreadNotebook?.textModel.$spliceNotebookCells(splices);
|
||||
}
|
||||
|
||||
spliceNotebookCellOutputs(resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): void {
|
||||
let mainthreadNotebook = this._mapping.get(URI.from(resource).toString());
|
||||
mainthreadNotebook?.textModel.updateRenderers(renderers);
|
||||
mainthreadNotebook?.textModel.$spliceNotebookCellOutputs(cellHandle, splices);
|
||||
}
|
||||
|
||||
async executeNotebook(viewType: string, uri: URI): Promise<void> {
|
||||
this._mainThreadNotebook.executeNotebook(viewType, uri);
|
||||
}
|
||||
|
||||
// Methods for ExtHost
|
||||
async createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void> {
|
||||
let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource));
|
||||
this._mapping.set(URI.revive(resource).toString(), document);
|
||||
}
|
||||
|
||||
updateLanguages(resource: UriComponents, languages: string[]) {
|
||||
let document = this._mapping.get(URI.from(resource).toString());
|
||||
document?.textModel.updateLanguages(languages);
|
||||
}
|
||||
|
||||
updateNotebookRenderers(resource: UriComponents, renderers: number[]): void {
|
||||
let document = this._mapping.get(URI.from(resource).toString());
|
||||
document?.textModel.updateRenderers(renderers);
|
||||
}
|
||||
|
||||
updateNotebookActiveCell(uri: URI, cellHandle: number): void {
|
||||
let mainthreadNotebook = this._mapping.get(URI.from(uri).toString());
|
||||
mainthreadNotebook?.textModel.updateActiveCell(cellHandle);
|
||||
}
|
||||
|
||||
async createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise<NotebookCellTextModel | undefined> {
|
||||
let cell = await this._proxy.$createEmptyCell(this._viewType, uri, index, language, type);
|
||||
if (cell) {
|
||||
let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs);
|
||||
return mainCell;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async deleteCell(uri: URI, index: number): Promise<boolean> {
|
||||
let mainthreadNotebook = this._mapping.get(URI.from(uri).toString());
|
||||
|
||||
if (mainthreadNotebook) {
|
||||
return mainthreadNotebook.deleteCell(uri, index);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
executeNotebookActiveCell(uri: URI): void {
|
||||
let mainthreadNotebook = this._mapping.get(URI.from(uri).toString());
|
||||
|
||||
if (mainthreadNotebook && mainthreadNotebook.textModel.activeCell) {
|
||||
this._proxy.$executeNotebook(this._viewType, uri, mainthreadNotebook.textModel.activeCell.handle);
|
||||
}
|
||||
}
|
||||
|
||||
async destoryNotebookDocument(notebook: INotebookTextModel): Promise<void> {
|
||||
let document = this._mapping.get(URI.from(notebook.uri).toString());
|
||||
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
let removeFromExtHost = await this._proxy.$destoryNotebookDocument(this._viewType, notebook.uri);
|
||||
if (removeFromExtHost) {
|
||||
document.dispose();
|
||||
this._mapping.delete(URI.from(notebook.uri).toString());
|
||||
}
|
||||
}
|
||||
|
||||
async save(uri: URI): Promise<boolean> {
|
||||
return this._proxy.$saveNotebook(this._viewType, uri);
|
||||
}
|
||||
}
|
|
@ -67,6 +67,7 @@ import { ILogService } from 'vs/platform/log/common/log';
|
|||
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';
|
||||
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
||||
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
|
||||
import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook';
|
||||
import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming';
|
||||
import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
|
||||
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
|
||||
|
@ -129,7 +130,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, new ExtHostComments(rpcProtocol, extHostCommands, extHostDocuments));
|
||||
const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol));
|
||||
const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress)));
|
||||
const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol));
|
||||
const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol));
|
||||
const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostDocumentsAndEditors));
|
||||
const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol));
|
||||
const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol));
|
||||
const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands));
|
||||
|
@ -598,6 +600,15 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
createInputBox(): vscode.InputBox {
|
||||
return extHostQuickOpen.createInputBox(extension.identifier);
|
||||
},
|
||||
registerNotebookProvider: (viewType: string, provider: vscode.NotebookProvider) => {
|
||||
return extHostNotebook.registerNotebookProvider(extension, viewType, provider);
|
||||
},
|
||||
registerNotebookOutputRenderer: (type: string, outputFilter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer) => {
|
||||
return extHostNotebook.registerNotebookOutputRenderer(type, extension, outputFilter, renderer);
|
||||
},
|
||||
get activeNotebookDocument(): vscode.NotebookDocument | undefined {
|
||||
return extHostNotebook.activeNotebookDocument;
|
||||
},
|
||||
get activeColorTheme(): vscode.ColorTheme {
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostTheming.activeColorTheme;
|
||||
|
@ -1018,7 +1029,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
WebviewContentState: extHostTypes.WebviewContentState,
|
||||
UIKind: UIKind,
|
||||
ColorThemeKind: extHostTypes.ColorThemeKind,
|
||||
TimelineItem: extHostTypes.TimelineItem
|
||||
TimelineItem: extHostTypes.TimelineItem,
|
||||
CellKind: extHostTypes.CellKind,
|
||||
CellOutputKind: extHostTypes.CellOutputKind
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
|
|||
import { TunnelOptions } from 'vs/platform/remote/common/tunnel';
|
||||
import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
|
||||
import { Dto } from 'vs/base/common/types';
|
||||
|
||||
|
@ -577,6 +578,16 @@ export interface WebviewExtensionDescription {
|
|||
readonly location: UriComponents;
|
||||
}
|
||||
|
||||
export interface NotebookExtensionDescription {
|
||||
readonly id: ExtensionIdentifier;
|
||||
readonly location: UriComponents;
|
||||
}
|
||||
|
||||
export enum WebviewEditorCapabilities {
|
||||
Editable,
|
||||
SupportsHotExit,
|
||||
}
|
||||
|
||||
export interface CustomTextEditorCapabilities {
|
||||
readonly supportsMove?: boolean;
|
||||
}
|
||||
|
@ -636,6 +647,49 @@ export interface ExtHostWebviewsShape {
|
|||
$onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise<void>;
|
||||
}
|
||||
|
||||
export enum CellKind {
|
||||
Markdown = 1,
|
||||
Code = 2
|
||||
}
|
||||
|
||||
export enum CellOutputKind {
|
||||
Text = 1,
|
||||
Error = 2,
|
||||
Rich = 3
|
||||
}
|
||||
|
||||
export interface ICellDto {
|
||||
handle: number;
|
||||
uri: UriComponents,
|
||||
source: string[];
|
||||
language: string;
|
||||
cellKind: CellKind;
|
||||
outputs: IOutput[];
|
||||
}
|
||||
|
||||
export type NotebookCellsSplice = [
|
||||
number /* start */,
|
||||
number /* delete count */,
|
||||
ICellDto[]
|
||||
];
|
||||
|
||||
export type NotebookCellOutputsSplice = [
|
||||
number /* start */,
|
||||
number /* delete count */,
|
||||
IOutput[]
|
||||
];
|
||||
|
||||
export interface MainThreadNotebookShape extends IDisposable {
|
||||
$registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise<void>;
|
||||
$unregisterNotebookProvider(viewType: string): Promise<void>;
|
||||
$registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise<void>;
|
||||
$unregisterNotebookRenderer(handle: number): Promise<void>;
|
||||
$createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise<void>;
|
||||
$updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void>;
|
||||
$spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise<void>;
|
||||
$spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise<void>;
|
||||
}
|
||||
|
||||
export interface MainThreadUrlsShape extends IDisposable {
|
||||
$registerUriHandler(handle: number, extensionId: ExtensionIdentifier): Promise<void>;
|
||||
$unregisterUriHandler(handle: number): Promise<void>;
|
||||
|
@ -1464,6 +1518,17 @@ export interface ExtHostCommentsShape {
|
|||
$toggleReaction(commentControllerHandle: number, threadHandle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ExtHostNotebookShape {
|
||||
$resolveNotebook(viewType: string, uri: UriComponents): Promise<number | undefined>;
|
||||
$executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise<void>;
|
||||
$createEmptyCell(viewType: string, uri: UriComponents, index: number, language: string, type: CellKind): Promise<ICellDto | undefined>;
|
||||
$deleteCell(viewType: string, uri: UriComponents, index: number): Promise<boolean>;
|
||||
$saveNotebook(viewType: string, uri: UriComponents): Promise<boolean>;
|
||||
$updateActiveEditor(viewType: string, uri: UriComponents): Promise<void>;
|
||||
$destoryNotebookDocument(viewType: string, uri: UriComponents): Promise<boolean>;
|
||||
$acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void;
|
||||
}
|
||||
|
||||
export interface ExtHostStorageShape {
|
||||
$acceptValue(shared: boolean, key: string, value: object | undefined): void;
|
||||
}
|
||||
|
@ -1529,6 +1594,7 @@ export const MainContext = {
|
|||
MainThreadTask: createMainId<MainThreadTaskShape>('MainThreadTask'),
|
||||
MainThreadWindow: createMainId<MainThreadWindowShape>('MainThreadWindow'),
|
||||
MainThreadLabelService: createMainId<MainThreadLabelServiceShape>('MainThreadLabelService'),
|
||||
MainThreadNotebook: createMainId<MainThreadNotebookShape>('MainThreadNotebook'),
|
||||
MainThreadTheming: createMainId<MainThreadThemingShape>('MainThreadTheming'),
|
||||
MainThreadTunnelService: createMainId<MainThreadTunnelServiceShape>('MainThreadTunnelService'),
|
||||
MainThreadTimeline: createMainId<MainThreadTimelineShape>('MainThreadTimeline')
|
||||
|
@ -1565,7 +1631,8 @@ export const ExtHostContext = {
|
|||
ExtHostStorage: createMainId<ExtHostStorageShape>('ExtHostStorage'),
|
||||
ExtHostUrls: createExtId<ExtHostUrlsShape>('ExtHostUrls'),
|
||||
ExtHostOutputService: createMainId<ExtHostOutputServiceShape>('ExtHostOutputService'),
|
||||
ExtHostLabelService: createMainId<ExtHostLabelServiceShape>('ExtHostLabelService'),
|
||||
ExtHosLabelService: createMainId<ExtHostLabelServiceShape>('ExtHostLabelService'),
|
||||
ExtHostNotebook: createMainId<ExtHostNotebookShape>('ExtHostNotebook'),
|
||||
ExtHostTheming: createMainId<ExtHostThemingShape>('ExtHostTheming'),
|
||||
ExtHostTunnelService: createMainId<ExtHostTunnelServiceShape>('ExtHostTunnelService'),
|
||||
ExtHostAuthentication: createMainId<ExtHostAuthenticationShape>('ExtHostAuthentication'),
|
||||
|
|
|
@ -238,7 +238,7 @@ export class ExtHostDocumentData extends MirrorTextModel {
|
|||
}
|
||||
}
|
||||
|
||||
class ExtHostDocumentLine implements vscode.TextLine {
|
||||
export class ExtHostDocumentLine implements vscode.TextLine {
|
||||
|
||||
private readonly _line: number;
|
||||
private readonly _text: string;
|
||||
|
|
621
src/vs/workbench/api/common/extHostNotebook.ts
Normal file
621
src/vs/workbench/api/common/extHostNotebook.ts
Normal file
|
@ -0,0 +1,621 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { ExtHostNotebookShape, IMainContext, MainThreadNotebookShape, MainContext, ICellDto, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind, CellOutputKind } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { Disposable as VSCodeDisposable } from './extHostTypes';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { DisposableStore, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { readonly } from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
|
||||
import { INotebookDisplayOrder, ITransformedDisplayOutputDto, IOrderedMimeType, IStreamOutput, IErrorOutput, mimeTypeSupportedByCore, IOutput, sortMimeTypes, diff, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { ISplice } from 'vs/base/common/sequence';
|
||||
|
||||
export class ExtHostCell implements vscode.NotebookCell {
|
||||
|
||||
public source: string[];
|
||||
private _outputs: any[];
|
||||
private _onDidChangeOutputs = new Emitter<ISplice<vscode.CellOutput>[]>();
|
||||
onDidChangeOutputs: Event<ISplice<vscode.CellOutput>[]> = this._onDidChangeOutputs.event;
|
||||
private _textDocument: vscode.TextDocument | undefined;
|
||||
private _initalVersion: number = -1;
|
||||
private _outputMapping = new Set<vscode.CellOutput>();
|
||||
|
||||
constructor(
|
||||
readonly handle: number,
|
||||
readonly uri: URI,
|
||||
private _content: string,
|
||||
public cellKind: CellKind,
|
||||
public language: string,
|
||||
outputs: any[]
|
||||
) {
|
||||
this.source = this._content.split(/\r|\n|\r\n/g);
|
||||
this._outputs = outputs;
|
||||
}
|
||||
|
||||
get outputs() {
|
||||
return this._outputs;
|
||||
}
|
||||
|
||||
set outputs(newOutputs: vscode.CellOutput[]) {
|
||||
let diffs = diff<vscode.CellOutput>(this._outputs || [], newOutputs || [], (a) => {
|
||||
return this._outputMapping.has(a);
|
||||
});
|
||||
|
||||
diffs.forEach(diff => {
|
||||
for (let i = diff.start; i < diff.start + diff.deleteCount; i++) {
|
||||
this._outputMapping.delete(this._outputs[i]);
|
||||
}
|
||||
|
||||
diff.toInsert.forEach(output => {
|
||||
this._outputMapping.add(output);
|
||||
});
|
||||
});
|
||||
|
||||
this._outputs = newOutputs;
|
||||
this._onDidChangeOutputs.fire(diffs);
|
||||
}
|
||||
|
||||
getContent(): string {
|
||||
if (this._textDocument && this._initalVersion !== this._textDocument?.version) {
|
||||
return this._textDocument.getText();
|
||||
} else {
|
||||
return this.source.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
attachTextDocument(document: vscode.TextDocument) {
|
||||
this._textDocument = document;
|
||||
this._initalVersion = this._textDocument.version;
|
||||
}
|
||||
|
||||
detachTextDocument(document: vscode.TextDocument) {
|
||||
if (this._textDocument && this._textDocument.version !== this._initalVersion) {
|
||||
this.source = this._textDocument.getText().split(/\r|\n|\r\n/g);
|
||||
}
|
||||
|
||||
this._textDocument = undefined;
|
||||
this._initalVersion = -1;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtHostNotebookDocument extends Disposable implements vscode.NotebookDocument {
|
||||
private static _handlePool: number = 0;
|
||||
readonly handle = ExtHostNotebookDocument._handlePool++;
|
||||
|
||||
private _cells: ExtHostCell[] = [];
|
||||
|
||||
private _cellDisposableMapping = new Map<number, DisposableStore>();
|
||||
|
||||
get cells() {
|
||||
return this._cells;
|
||||
}
|
||||
|
||||
set cells(newCells: ExtHostCell[]) {
|
||||
let diffs = diff<ExtHostCell>(this._cells, newCells, (a) => {
|
||||
return this._cellDisposableMapping.has(a.handle);
|
||||
});
|
||||
|
||||
diffs.forEach(diff => {
|
||||
for (let i = diff.start; i < diff.start + diff.deleteCount; i++) {
|
||||
this._cellDisposableMapping.get(this._cells[i].handle)?.clear();
|
||||
this._cellDisposableMapping.delete(this._cells[i].handle);
|
||||
}
|
||||
|
||||
diff.toInsert.forEach(cell => {
|
||||
this._cellDisposableMapping.set(cell.handle, new DisposableStore());
|
||||
this._cellDisposableMapping.get(cell.handle)?.add(cell.onDidChangeOutputs((outputDiffs) => {
|
||||
this.eventuallyUpdateCellOutputs(cell, outputDiffs);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
this._cells = newCells;
|
||||
this.eventuallyUpdateCells(diffs);
|
||||
}
|
||||
|
||||
private _languages: string[] = [];
|
||||
|
||||
get languages() {
|
||||
return this._languages = [];
|
||||
}
|
||||
|
||||
set languages(newLanguages: string[]) {
|
||||
this._languages = newLanguages;
|
||||
this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages);
|
||||
}
|
||||
|
||||
private _displayOrder: string[] = [];
|
||||
|
||||
get displayOrder() {
|
||||
return this._displayOrder;
|
||||
}
|
||||
|
||||
set displayOrder(newOrder: string[]) {
|
||||
this._displayOrder = newOrder;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: MainThreadNotebookShape,
|
||||
public viewType: string,
|
||||
public uri: URI,
|
||||
public renderingHandler: ExtHostNotebookOutputRenderingHandler
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this._cellDisposableMapping.forEach(cell => cell.dispose());
|
||||
}
|
||||
|
||||
get fileName() { return this.uri.fsPath; }
|
||||
|
||||
get isDirty() { return false; }
|
||||
|
||||
eventuallyUpdateCells(diffs: ISplice<ExtHostCell>[]) {
|
||||
let renderers = new Set<number>();
|
||||
let diffDtos: NotebookCellsSplice[] = [];
|
||||
|
||||
diffDtos = diffs.map(diff => {
|
||||
let inserts = diff.toInsert;
|
||||
|
||||
let cellDtos = inserts.map(cell => {
|
||||
let outputs: IOutput[] = [];
|
||||
if (cell.outputs.length) {
|
||||
outputs = cell.outputs.map(output => {
|
||||
if (output.outputKind === CellOutputKind.Rich) {
|
||||
const ret = this.transformMimeTypes(cell, output);
|
||||
|
||||
if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) {
|
||||
renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!);
|
||||
}
|
||||
return ret;
|
||||
} else {
|
||||
return output as IStreamOutput | IErrorOutput;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
uri: cell.uri,
|
||||
handle: cell.handle,
|
||||
source: cell.source,
|
||||
language: cell.language,
|
||||
cellKind: cell.cellKind,
|
||||
outputs: outputs,
|
||||
isDirty: false
|
||||
};
|
||||
});
|
||||
|
||||
return [diff.start, diff.deleteCount, cellDtos];
|
||||
});
|
||||
|
||||
this._proxy.$spliceNotebookCells(
|
||||
this.viewType,
|
||||
this.uri,
|
||||
diffDtos,
|
||||
Array.from(renderers)
|
||||
);
|
||||
}
|
||||
|
||||
eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice<vscode.CellOutput>[]) {
|
||||
let renderers = new Set<number>();
|
||||
let outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => {
|
||||
let outputs = diff.toInsert;
|
||||
|
||||
let transformedOutputs = outputs.map(output => {
|
||||
if (output.outputKind === CellOutputKind.Rich) {
|
||||
const ret = this.transformMimeTypes(cell, output);
|
||||
|
||||
if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) {
|
||||
renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!);
|
||||
}
|
||||
return ret;
|
||||
} else {
|
||||
return output as IStreamOutput | IErrorOutput;
|
||||
}
|
||||
});
|
||||
|
||||
return [diff.start, diff.deleteCount, transformedOutputs];
|
||||
});
|
||||
|
||||
this._proxy.$spliceNotebookCellOutputs(this.viewType, this.uri, cell.handle, outputDtos, Array.from(renderers));
|
||||
}
|
||||
|
||||
insertCell(index: number, cell: ExtHostCell) {
|
||||
this.cells.splice(index, 0, cell);
|
||||
|
||||
if (!this._cellDisposableMapping.has(cell.handle)) {
|
||||
this._cellDisposableMapping.set(cell.handle, new DisposableStore());
|
||||
}
|
||||
|
||||
let store = this._cellDisposableMapping.get(cell.handle)!;
|
||||
|
||||
store.add(cell.onDidChangeOutputs((diffs) => {
|
||||
this.eventuallyUpdateCellOutputs(cell, diffs);
|
||||
}));
|
||||
}
|
||||
|
||||
deleteCell(index: number): boolean {
|
||||
if (index >= this.cells.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let cell = this.cells[index];
|
||||
this._cellDisposableMapping.get(cell.handle)?.dispose();
|
||||
this._cellDisposableMapping.delete(cell.handle);
|
||||
|
||||
this.cells.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
transformMimeTypes(cell: ExtHostCell, output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto {
|
||||
let mimeTypes = Object.keys(output.data);
|
||||
|
||||
// TODO@rebornix, the document display order might be assigned a bit later. We need to postpone sending the outputs to the core side.
|
||||
let coreDisplayOrder = this.renderingHandler.outputDisplayOrder;
|
||||
const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], this._displayOrder, coreDisplayOrder?.defaultOrder || []);
|
||||
|
||||
let orderMimeTypes: IOrderedMimeType[] = [];
|
||||
|
||||
sorted.forEach(mimeType => {
|
||||
let handlers = this.renderingHandler.findBestMatchedRenderer(mimeType);
|
||||
|
||||
if (handlers.length) {
|
||||
let renderedOutput = handlers[0].render(this, cell, output, mimeType);
|
||||
|
||||
orderMimeTypes.push({
|
||||
mimeType: mimeType,
|
||||
isResolved: true,
|
||||
rendererId: handlers[0].handle,
|
||||
output: renderedOutput
|
||||
});
|
||||
|
||||
for (let i = 1; i < handlers.length; i++) {
|
||||
orderMimeTypes.push({
|
||||
mimeType: mimeType,
|
||||
isResolved: false,
|
||||
rendererId: handlers[i].handle
|
||||
});
|
||||
}
|
||||
|
||||
if (mimeTypeSupportedByCore(mimeType)) {
|
||||
orderMimeTypes.push({
|
||||
mimeType: mimeType,
|
||||
isResolved: false,
|
||||
rendererId: -1
|
||||
});
|
||||
}
|
||||
} else {
|
||||
orderMimeTypes.push({
|
||||
mimeType: mimeType,
|
||||
isResolved: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
outputKind: output.outputKind,
|
||||
data: output.data,
|
||||
orderedMimeTypes: orderMimeTypes,
|
||||
pickedMimeTypeIndex: 0
|
||||
};
|
||||
}
|
||||
|
||||
getCell(cellHandle: number) {
|
||||
return this.cells.find(cell => cell.handle === cellHandle);
|
||||
}
|
||||
|
||||
attachCellTextDocument(textDocument: vscode.TextDocument) {
|
||||
let cell = this.cells.find(cell => cell.uri.toString() === textDocument.uri.toString());
|
||||
if (cell) {
|
||||
cell.attachTextDocument(textDocument);
|
||||
}
|
||||
}
|
||||
|
||||
detachCellTextDocument(textDocument: vscode.TextDocument) {
|
||||
let cell = this.cells.find(cell => cell.uri.toString() === textDocument.uri.toString());
|
||||
if (cell) {
|
||||
cell.detachTextDocument(textDocument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor {
|
||||
private _viewColumn: vscode.ViewColumn | undefined;
|
||||
private static _cellhandlePool: number = 0;
|
||||
|
||||
constructor(
|
||||
viewType: string,
|
||||
readonly id: string,
|
||||
public uri: URI,
|
||||
public document: ExtHostNotebookDocument,
|
||||
private _documentsAndEditors: ExtHostDocumentsAndEditors
|
||||
) {
|
||||
super();
|
||||
this._register(this._documentsAndEditors.onDidAddDocuments(documents => {
|
||||
for (const { document: textDocument } of documents) {
|
||||
let data = CellUri.parse(textDocument.uri);
|
||||
if (data) {
|
||||
if (this.document.uri.toString() === data.notebook.toString()) {
|
||||
document.attachCellTextDocument(textDocument);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._documentsAndEditors.onDidRemoveDocuments(documents => {
|
||||
for (const { document: textDocument } of documents) {
|
||||
let data = CellUri.parse(textDocument.uri);
|
||||
if (data) {
|
||||
if (this.document.uri.toString() === data.notebook.toString()) {
|
||||
document.detachCellTextDocument(textDocument);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
createCell(content: string, language: string, type: CellKind, outputs: vscode.CellOutput[]): vscode.NotebookCell {
|
||||
const handle = ExtHostNotebookEditor._cellhandlePool++;
|
||||
const uri = CellUri.generate(this.document.uri, handle);
|
||||
const cell = new ExtHostCell(handle, uri, content, type, language, outputs);
|
||||
return cell;
|
||||
}
|
||||
|
||||
get viewColumn(): vscode.ViewColumn | undefined {
|
||||
return this._viewColumn;
|
||||
}
|
||||
|
||||
set viewColumn(value) {
|
||||
throw readonly('viewColumn');
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtHostNotebookOutputRenderer {
|
||||
private static _handlePool: number = 0;
|
||||
readonly handle = ExtHostNotebookOutputRenderer._handlePool++;
|
||||
|
||||
constructor(
|
||||
public type: string,
|
||||
public filter: vscode.NotebookOutputSelector,
|
||||
public renderer: vscode.NotebookOutputRenderer
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
matches(mimeType: string): boolean {
|
||||
if (this.filter.subTypes) {
|
||||
if (this.filter.subTypes.indexOf(mimeType) >= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render(document: ExtHostNotebookDocument, cell: ExtHostCell, output: vscode.CellOutput, mimeType: string): string {
|
||||
let html = this.renderer.render(document, cell, output, mimeType);
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExtHostNotebookOutputRenderingHandler {
|
||||
outputDisplayOrder: INotebookDisplayOrder | undefined;
|
||||
findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[];
|
||||
}
|
||||
|
||||
export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostNotebookOutputRenderingHandler {
|
||||
private static _handlePool: number = 0;
|
||||
|
||||
private readonly _proxy: MainThreadNotebookShape;
|
||||
private readonly _notebookProviders = new Map<string, { readonly provider: vscode.NotebookProvider, readonly extension: IExtensionDescription }>();
|
||||
private readonly _documents = new Map<string, ExtHostNotebookDocument>();
|
||||
private readonly _editors = new Map<string, ExtHostNotebookEditor>();
|
||||
private readonly _notebookOutputRenderers = new Map<number, ExtHostNotebookOutputRenderer>();
|
||||
private _outputDisplayOrder: INotebookDisplayOrder | undefined;
|
||||
|
||||
get outputDisplayOrder(): INotebookDisplayOrder | undefined {
|
||||
return this._outputDisplayOrder;
|
||||
}
|
||||
|
||||
private _activeNotebookDocument: ExtHostNotebookDocument | undefined;
|
||||
|
||||
get activeNotebookDocument() {
|
||||
return this._activeNotebookDocument;
|
||||
}
|
||||
|
||||
constructor(mainContext: IMainContext, private _documentsAndEditors: ExtHostDocumentsAndEditors) {
|
||||
this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook);
|
||||
}
|
||||
|
||||
registerNotebookOutputRenderer(
|
||||
type: string,
|
||||
extension: IExtensionDescription,
|
||||
filter: vscode.NotebookOutputSelector,
|
||||
renderer: vscode.NotebookOutputRenderer
|
||||
): vscode.Disposable {
|
||||
let extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer);
|
||||
this._notebookOutputRenderers.set(extHostRenderer.handle, extHostRenderer);
|
||||
this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation }, type, filter, extHostRenderer.handle, renderer.preloads || []);
|
||||
return new VSCodeDisposable(() => {
|
||||
this._notebookOutputRenderers.delete(extHostRenderer.handle);
|
||||
this._proxy.$unregisterNotebookRenderer(extHostRenderer.handle);
|
||||
});
|
||||
}
|
||||
|
||||
findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[] {
|
||||
let matches: ExtHostNotebookOutputRenderer[] = [];
|
||||
for (let renderer of this._notebookOutputRenderers) {
|
||||
if (renderer[1].matches(mimeType)) {
|
||||
matches.push(renderer[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
registerNotebookProvider(
|
||||
extension: IExtensionDescription,
|
||||
viewType: string,
|
||||
provider: vscode.NotebookProvider,
|
||||
): vscode.Disposable {
|
||||
|
||||
if (this._notebookProviders.has(viewType)) {
|
||||
throw new Error(`Notebook provider for '${viewType}' already registered`);
|
||||
}
|
||||
|
||||
this._notebookProviders.set(viewType, { extension, provider });
|
||||
this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType);
|
||||
return new VSCodeDisposable(() => {
|
||||
this._notebookProviders.delete(viewType);
|
||||
this._proxy.$unregisterNotebookProvider(viewType);
|
||||
});
|
||||
}
|
||||
|
||||
async $resolveNotebook(viewType: string, uri: UriComponents): Promise<number | undefined> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (provider) {
|
||||
if (!this._documents.has(URI.revive(uri).toString())) {
|
||||
let document = new ExtHostNotebookDocument(this._proxy, viewType, URI.revive(uri), this);
|
||||
await this._proxy.$createNotebookDocument(
|
||||
document.handle,
|
||||
viewType,
|
||||
uri
|
||||
);
|
||||
|
||||
this._documents.set(URI.revive(uri).toString(), document);
|
||||
}
|
||||
|
||||
let editor = new ExtHostNotebookEditor(
|
||||
viewType,
|
||||
`${ExtHostNotebookController._handlePool++}`,
|
||||
URI.revive(uri),
|
||||
this._documents.get(URI.revive(uri).toString())!,
|
||||
this._documentsAndEditors
|
||||
);
|
||||
|
||||
this._editors.set(URI.revive(uri).toString(), editor);
|
||||
await provider.provider.resolveNotebook(editor);
|
||||
// await editor.document.$updateCells();
|
||||
return editor.document.handle;
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise<void> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
let document = this._documents.get(URI.revive(uri).toString());
|
||||
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined;
|
||||
return provider.provider.executeCell(document!, cell);
|
||||
}
|
||||
|
||||
async $createEmptyCell(viewType: string, uri: URI, index: number, language: string, type: CellKind): Promise<ICellDto | undefined> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (provider) {
|
||||
let editor = this._editors.get(URI.revive(uri).toString());
|
||||
let document = this._documents.get(URI.revive(uri).toString());
|
||||
|
||||
let rawCell = editor?.createCell('', language, type, []) as ExtHostCell;
|
||||
document?.insertCell(index, rawCell!);
|
||||
|
||||
let allDocuments = this._documentsAndEditors.allDocuments();
|
||||
for (let { document: textDocument } of allDocuments) {
|
||||
let data = CellUri.parse(textDocument.uri);
|
||||
if (data) {
|
||||
if (uri.toString() === data.notebook.toString() && textDocument.uri.toString() === rawCell.uri.toString()) {
|
||||
rawCell.attachTextDocument(textDocument);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
uri: rawCell.uri,
|
||||
handle: rawCell.handle,
|
||||
source: rawCell.source,
|
||||
language: rawCell.language,
|
||||
cellKind: rawCell.cellKind,
|
||||
outputs: []
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async $deleteCell(viewType: string, uri: UriComponents, index: number): Promise<boolean> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let document = this._documents.get(URI.revive(uri).toString());
|
||||
|
||||
if (document) {
|
||||
return document.deleteCell(index);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async $saveNotebook(viewType: string, uri: UriComponents): Promise<boolean> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
let document = this._documents.get(URI.revive(uri).toString());
|
||||
|
||||
if (provider && document) {
|
||||
return await provider.provider.save(document);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async $updateActiveEditor(viewType: string, uri: UriComponents): Promise<void> {
|
||||
this._activeNotebookDocument = this._documents.get(URI.revive(uri).toString());
|
||||
}
|
||||
|
||||
async $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise<boolean> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let document = this._documents.get(URI.revive(uri).toString());
|
||||
|
||||
if (document) {
|
||||
document.dispose();
|
||||
this._documents.delete(URI.revive(uri).toString());
|
||||
}
|
||||
|
||||
let editor = this._editors.get(URI.revive(uri).toString());
|
||||
|
||||
if (editor) {
|
||||
editor.dispose();
|
||||
this._editors.delete(URI.revive(uri).toString());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void {
|
||||
this._outputDisplayOrder = displayOrder;
|
||||
}
|
||||
}
|
|
@ -2559,6 +2559,21 @@ export enum ColorThemeKind {
|
|||
|
||||
//#endregion Theming
|
||||
|
||||
//#region Notebook
|
||||
|
||||
export enum CellKind {
|
||||
Markdown = 1,
|
||||
Code = 2
|
||||
}
|
||||
|
||||
export enum CellOutputKind {
|
||||
Text = 1,
|
||||
Error = 2,
|
||||
Rich = 3
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Timeline
|
||||
|
||||
@es5ClassCompat
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .simple-fr-find-part-wrapper {
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: -45px;
|
||||
right: 18px;
|
||||
width: 318px;
|
||||
max-width: calc(100% - 28px - 28px - 8px);
|
||||
pointer-events: none;
|
||||
transition: top 200ms linear;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part {
|
||||
/* visibility: hidden; Use visibility to maintain flex layout while hidden otherwise interferes with transition */
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
align-items: center;
|
||||
pointer-events: all;
|
||||
margin: 0 0 0 17px;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-replace-part {
|
||||
/* visibility: hidden; Use visibility to maintain flex layout while hidden otherwise interferes with transition */
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
align-items: center;
|
||||
pointer-events: all;
|
||||
margin: 0 0 0 17px;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part-wrapper .find-replace-progress {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part-wrapper .find-replace-progress .monaco-progress-container {
|
||||
height: 2px;
|
||||
top: 0px !important;
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part-wrapper .find-replace-progress .monaco-progress-container .progress-bit {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part-wrapper .monaco-findInput {
|
||||
width: 224px;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part-wrapper .button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex: initial;
|
||||
margin-left: 3px;
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part-wrapper.visible .simple-fr-find-part {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part-wrapper .toggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 18px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 0px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part-wrapper.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part-wrapper.visible-transition {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part .monaco-findInput {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part .button {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
flex: initial;
|
||||
margin-left: 3px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part .button.previous {
|
||||
background-image: url('images/chevron-previous-light.svg');
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part .button.next {
|
||||
background-image: url('images/chevron-next-light.svg');
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part .button.close-fw {
|
||||
background-image: url('images/close-light.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .simple-fr-find-part .button.previous,
|
||||
.vs-dark .monaco-workbench .simple-fr-find-part .button.previous {
|
||||
background-image: url('images/chevron-previous-dark.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .simple-fr-find-part .button.next,
|
||||
.vs-dark .monaco-workbench .simple-fr-find-part .button.next {
|
||||
background-image: url('images/chevron-next-dark.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .simple-fr-find-part .button.close-fw,
|
||||
.vs-dark .monaco-workbench .simple-fr-find-part .button.close-fw {
|
||||
background-image: url('images/close-dark.svg');
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-fr-find-part .button.disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
|
@ -0,0 +1,427 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./simpleFindReplaceWidget';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState';
|
||||
import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { SimpleButton } from 'vs/editor/contrib/find/findWidget';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IColorTheme, registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/browser/contextScopedHistoryWidget';
|
||||
import { ReplaceInput, IReplaceInputStyles } from 'vs/base/browser/ui/findinput/replaceInput';
|
||||
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
|
||||
import { attachProgressBarStyler } from 'vs/platform/theme/common/styler';
|
||||
|
||||
const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find");
|
||||
const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find");
|
||||
const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous match");
|
||||
const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next match");
|
||||
const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close");
|
||||
const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace mode");
|
||||
const NLS_REPLACE_INPUT_LABEL = nls.localize('label.replace', "Replace");
|
||||
const NLS_REPLACE_INPUT_PLACEHOLDER = nls.localize('placeholder.replace', "Replace");
|
||||
const NLS_REPLACE_BTN_LABEL = nls.localize('label.replaceButton', "Replace");
|
||||
const NLS_REPLACE_ALL_BTN_LABEL = nls.localize('label.replaceAllButton', "Replace All");
|
||||
|
||||
export abstract class SimpleFindReplaceWidget extends Widget {
|
||||
protected readonly _findInput: FindInput;
|
||||
private readonly _domNode: HTMLElement;
|
||||
private readonly _innerFindDomNode: HTMLElement;
|
||||
private readonly _focusTracker: dom.IFocusTracker;
|
||||
private readonly _findInputFocusTracker: dom.IFocusTracker;
|
||||
private readonly _updateHistoryDelayer: Delayer<void>;
|
||||
private readonly prevBtn: SimpleButton;
|
||||
private readonly nextBtn: SimpleButton;
|
||||
|
||||
private readonly _replaceInput!: ReplaceInput;
|
||||
private readonly _innerReplaceDomNode!: HTMLElement;
|
||||
private _toggleReplaceBtn!: SimpleButton;
|
||||
private readonly _replaceInputFocusTracker!: dom.IFocusTracker;
|
||||
private _replaceBtn!: SimpleButton;
|
||||
private _replaceAllBtn!: SimpleButton;
|
||||
|
||||
|
||||
private _isVisible: boolean = false;
|
||||
private _isReplaceVisible: boolean = false;
|
||||
private foundMatch: boolean = false;
|
||||
|
||||
protected _progressBar!: ProgressBar;
|
||||
|
||||
|
||||
constructor(
|
||||
@IContextViewService private readonly _contextViewService: IContextViewService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IThemeService private readonly _themeService: IThemeService,
|
||||
private readonly _state: FindReplaceState = new FindReplaceState(),
|
||||
showOptionButtons?: boolean
|
||||
) {
|
||||
super();
|
||||
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.classList.add('simple-fr-find-part-wrapper');
|
||||
this._register(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e)));
|
||||
|
||||
let progressContainer = dom.$('.find-replace-progress');
|
||||
this._progressBar = new ProgressBar(progressContainer);
|
||||
this._register(attachProgressBarStyler(this._progressBar, this._themeService));
|
||||
this._domNode.appendChild(progressContainer);
|
||||
|
||||
// Toggle replace button
|
||||
this._toggleReplaceBtn = this._register(new SimpleButton({
|
||||
label: NLS_TOGGLE_REPLACE_MODE_BTN_LABEL,
|
||||
className: 'codicon toggle left',
|
||||
onTrigger: () => {
|
||||
this._isReplaceVisible = !this._isReplaceVisible;
|
||||
this._state.change({ isReplaceRevealed: this._isReplaceVisible }, false);
|
||||
if (this._isReplaceVisible) {
|
||||
this._innerReplaceDomNode.style.display = 'flex';
|
||||
} else {
|
||||
this._innerReplaceDomNode.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}));
|
||||
this._toggleReplaceBtn.toggleClass('codicon-chevron-down', this._isReplaceVisible);
|
||||
this._toggleReplaceBtn.toggleClass('codicon-chevron-right', !this._isReplaceVisible);
|
||||
this._toggleReplaceBtn.setExpanded(this._isReplaceVisible);
|
||||
this._domNode.appendChild(this._toggleReplaceBtn.domNode);
|
||||
|
||||
|
||||
this._innerFindDomNode = document.createElement('div');
|
||||
this._innerFindDomNode.classList.add('simple-fr-find-part');
|
||||
|
||||
this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewService, {
|
||||
label: NLS_FIND_INPUT_LABEL,
|
||||
placeholder: NLS_FIND_INPUT_PLACEHOLDER,
|
||||
validation: (value: string): InputBoxMessage | null => {
|
||||
if (value.length === 0 || !this._findInput.getRegex()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
new RegExp(value);
|
||||
return null;
|
||||
} catch (e) {
|
||||
this.foundMatch = false;
|
||||
this.updateButtons(this.foundMatch);
|
||||
return { content: e.message };
|
||||
}
|
||||
}
|
||||
}, contextKeyService, showOptionButtons));
|
||||
|
||||
// Find History with update delayer
|
||||
this._updateHistoryDelayer = new Delayer<void>(500);
|
||||
|
||||
this.oninput(this._findInput.domNode, (e) => {
|
||||
this.foundMatch = this.onInputChanged();
|
||||
this.updateButtons(this.foundMatch);
|
||||
this._delayedUpdateHistory();
|
||||
});
|
||||
|
||||
this._findInput.setRegex(!!this._state.isRegex);
|
||||
this._findInput.setCaseSensitive(!!this._state.matchCase);
|
||||
this._findInput.setWholeWords(!!this._state.wholeWord);
|
||||
|
||||
this._register(this._findInput.onDidOptionChange(() => {
|
||||
this._state.change({
|
||||
isRegex: this._findInput.getRegex(),
|
||||
wholeWord: this._findInput.getWholeWords(),
|
||||
matchCase: this._findInput.getCaseSensitive()
|
||||
}, true);
|
||||
}));
|
||||
|
||||
this._register(this._state.onFindReplaceStateChange(() => {
|
||||
this._findInput.setRegex(this._state.isRegex);
|
||||
this._findInput.setWholeWords(this._state.wholeWord);
|
||||
this._findInput.setCaseSensitive(this._state.matchCase);
|
||||
this.findFirst();
|
||||
}));
|
||||
|
||||
this.prevBtn = this._register(new SimpleButton({
|
||||
label: NLS_PREVIOUS_MATCH_BTN_LABEL,
|
||||
className: 'previous',
|
||||
onTrigger: () => {
|
||||
this.find(true);
|
||||
}
|
||||
}));
|
||||
|
||||
this.nextBtn = this._register(new SimpleButton({
|
||||
label: NLS_NEXT_MATCH_BTN_LABEL,
|
||||
className: 'next',
|
||||
onTrigger: () => {
|
||||
this.find(false);
|
||||
}
|
||||
}));
|
||||
|
||||
const closeBtn = this._register(new SimpleButton({
|
||||
label: NLS_CLOSE_BTN_LABEL,
|
||||
className: 'close-fw',
|
||||
onTrigger: () => {
|
||||
this.hide();
|
||||
}
|
||||
}));
|
||||
|
||||
this._innerFindDomNode.appendChild(this._findInput.domNode);
|
||||
this._innerFindDomNode.appendChild(this.prevBtn.domNode);
|
||||
this._innerFindDomNode.appendChild(this.nextBtn.domNode);
|
||||
this._innerFindDomNode.appendChild(closeBtn.domNode);
|
||||
|
||||
// _domNode wraps _innerDomNode, ensuring that
|
||||
this._domNode.appendChild(this._innerFindDomNode);
|
||||
|
||||
this.onkeyup(this._innerFindDomNode, e => {
|
||||
if (e.equals(KeyCode.Escape)) {
|
||||
this.hide();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
this._focusTracker = this._register(dom.trackFocus(this._innerFindDomNode));
|
||||
this._register(this._focusTracker.onDidFocus(this.onFocusTrackerFocus.bind(this)));
|
||||
this._register(this._focusTracker.onDidBlur(this.onFocusTrackerBlur.bind(this)));
|
||||
|
||||
this._findInputFocusTracker = this._register(dom.trackFocus(this._findInput.domNode));
|
||||
this._register(this._findInputFocusTracker.onDidFocus(this.onFindInputFocusTrackerFocus.bind(this)));
|
||||
this._register(this._findInputFocusTracker.onDidBlur(this.onFindInputFocusTrackerBlur.bind(this)));
|
||||
|
||||
this._register(dom.addDisposableListener(this._innerFindDomNode, 'click', (event) => {
|
||||
event.stopPropagation();
|
||||
}));
|
||||
|
||||
// Replace
|
||||
this._innerReplaceDomNode = document.createElement('div');
|
||||
this._innerReplaceDomNode.classList.add('simple-fr-replace-part');
|
||||
|
||||
this._replaceInput = this._register(new ContextScopedReplaceInput(null, undefined, {
|
||||
label: NLS_REPLACE_INPUT_LABEL,
|
||||
placeholder: NLS_REPLACE_INPUT_PLACEHOLDER,
|
||||
history: []
|
||||
}, contextKeyService, false));
|
||||
this._innerReplaceDomNode.appendChild(this._replaceInput.domNode);
|
||||
this._replaceInputFocusTracker = this._register(dom.trackFocus(this._replaceInput.domNode));
|
||||
this._register(this._replaceInputFocusTracker.onDidFocus(this.onReplaceInputFocusTrackerFocus.bind(this)));
|
||||
this._register(this._replaceInputFocusTracker.onDidBlur(this.onReplaceInputFocusTrackerBlur.bind(this)));
|
||||
|
||||
this._domNode.appendChild(this._innerReplaceDomNode);
|
||||
|
||||
if (this._isReplaceVisible) {
|
||||
this._innerReplaceDomNode.style.display = 'flex';
|
||||
} else {
|
||||
this._innerReplaceDomNode.style.display = 'none';
|
||||
}
|
||||
|
||||
this._replaceBtn = this._register(new SimpleButton({
|
||||
label: NLS_REPLACE_BTN_LABEL,
|
||||
className: 'codicon codicon-replace',
|
||||
onTrigger: () => {
|
||||
this.replaceOne();
|
||||
}
|
||||
}));
|
||||
|
||||
// Replace all button
|
||||
this._replaceAllBtn = this._register(new SimpleButton({
|
||||
label: NLS_REPLACE_ALL_BTN_LABEL,
|
||||
className: 'codicon codicon-replace-all',
|
||||
onTrigger: () => {
|
||||
this.replaceAll();
|
||||
}
|
||||
}));
|
||||
|
||||
this._innerReplaceDomNode.appendChild(this._replaceBtn.domNode);
|
||||
this._innerReplaceDomNode.appendChild(this._replaceAllBtn.domNode);
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected abstract onInputChanged(): boolean;
|
||||
protected abstract find(previous: boolean): void;
|
||||
protected abstract findFirst(): void;
|
||||
protected abstract replaceOne(): void;
|
||||
protected abstract replaceAll(): void;
|
||||
protected abstract onFocusTrackerFocus(): void;
|
||||
protected abstract onFocusTrackerBlur(): void;
|
||||
protected abstract onFindInputFocusTrackerFocus(): void;
|
||||
protected abstract onFindInputFocusTrackerBlur(): void;
|
||||
protected abstract onReplaceInputFocusTrackerFocus(): void;
|
||||
protected abstract onReplaceInputFocusTrackerBlur(): void;
|
||||
|
||||
protected get inputValue() {
|
||||
return this._findInput.getValue();
|
||||
}
|
||||
|
||||
protected get replaceValue() {
|
||||
return this._replaceInput.getValue();
|
||||
}
|
||||
|
||||
public get focusTracker(): dom.IFocusTracker {
|
||||
return this._focusTracker;
|
||||
}
|
||||
|
||||
public updateTheme(theme: IColorTheme): void {
|
||||
const inputStyles: IFindInputStyles = {
|
||||
inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder),
|
||||
inputActiveOptionBackground: theme.getColor(inputActiveOptionBackground),
|
||||
inputBackground: theme.getColor(inputBackground),
|
||||
inputForeground: theme.getColor(inputForeground),
|
||||
inputBorder: theme.getColor(inputBorder),
|
||||
inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground),
|
||||
inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground),
|
||||
inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder),
|
||||
inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground),
|
||||
inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground),
|
||||
inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder),
|
||||
inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground),
|
||||
inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground),
|
||||
inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder)
|
||||
};
|
||||
this._findInput.style(inputStyles);
|
||||
const replaceStyles: IReplaceInputStyles = {
|
||||
inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder),
|
||||
inputActiveOptionBackground: theme.getColor(inputActiveOptionBackground),
|
||||
inputBackground: theme.getColor(inputBackground),
|
||||
inputForeground: theme.getColor(inputForeground),
|
||||
inputBorder: theme.getColor(inputBorder),
|
||||
inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground),
|
||||
inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground),
|
||||
inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder),
|
||||
inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground),
|
||||
inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground),
|
||||
inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder),
|
||||
inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground),
|
||||
inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground),
|
||||
inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder)
|
||||
};
|
||||
this._replaceInput.style(replaceStyles);
|
||||
}
|
||||
|
||||
private _onStateChanged(e: FindReplaceStateChangedEvent): void {
|
||||
this._updateButtons();
|
||||
}
|
||||
|
||||
private _updateButtons(): void {
|
||||
this._findInput.setEnabled(this._isVisible);
|
||||
this._replaceInput.setEnabled(this._isVisible && this._isReplaceVisible);
|
||||
let findInputIsNonEmpty = (this._state.searchString.length > 0);
|
||||
this._replaceBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty);
|
||||
this._replaceAllBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty);
|
||||
|
||||
dom.toggleClass(this._domNode, 'replaceToggled', this._isReplaceVisible);
|
||||
this._toggleReplaceBtn.toggleClass('codicon-chevron-right', !this._isReplaceVisible);
|
||||
this._toggleReplaceBtn.toggleClass('codicon-chevron-down', this._isReplaceVisible);
|
||||
this._toggleReplaceBtn.setExpanded(this._isReplaceVisible);
|
||||
}
|
||||
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
|
||||
if (this._domNode && this._domNode.parentElement) {
|
||||
this._domNode.parentElement.removeChild(this._domNode);
|
||||
}
|
||||
}
|
||||
|
||||
public getDomNode() {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
public reveal(initialInput?: string): void {
|
||||
if (initialInput) {
|
||||
this._findInput.setValue(initialInput);
|
||||
}
|
||||
|
||||
if (this._isVisible) {
|
||||
this._findInput.select();
|
||||
return;
|
||||
}
|
||||
|
||||
this._isVisible = true;
|
||||
this.updateButtons(this.foundMatch);
|
||||
|
||||
setTimeout(() => {
|
||||
dom.addClass(this._domNode, 'visible');
|
||||
dom.addClass(this._domNode, 'visible-transition');
|
||||
this._domNode.setAttribute('aria-hidden', 'false');
|
||||
this._findInput.select();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public show(initialInput?: string): void {
|
||||
if (initialInput && !this._isVisible) {
|
||||
this._findInput.setValue(initialInput);
|
||||
}
|
||||
|
||||
this._isVisible = true;
|
||||
|
||||
setTimeout(() => {
|
||||
dom.addClass(this._domNode, 'visible');
|
||||
dom.addClass(this._domNode, 'visible-transition');
|
||||
this._domNode.setAttribute('aria-hidden', 'false');
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (this._isVisible) {
|
||||
dom.removeClass(this._domNode, 'visible-transition');
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
// Need to delay toggling visibility until after Transition, then visibility hidden - removes from tabIndex list
|
||||
setTimeout(() => {
|
||||
this._isVisible = false;
|
||||
this.updateButtons(this.foundMatch);
|
||||
dom.removeClass(this._domNode, 'visible');
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
protected _delayedUpdateHistory() {
|
||||
this._updateHistoryDelayer.trigger(this._updateHistory.bind(this));
|
||||
}
|
||||
|
||||
protected _updateHistory() {
|
||||
this._findInput.inputBox.addToHistory();
|
||||
}
|
||||
|
||||
protected _getRegexValue(): boolean {
|
||||
return this._findInput.getRegex();
|
||||
}
|
||||
|
||||
protected _getWholeWordValue(): boolean {
|
||||
return this._findInput.getWholeWords();
|
||||
}
|
||||
|
||||
protected _getCaseSensitiveValue(): boolean {
|
||||
return this._findInput.getCaseSensitive();
|
||||
}
|
||||
|
||||
protected updateButtons(foundMatch: boolean) {
|
||||
const hasInput = this.inputValue.length > 0;
|
||||
this.prevBtn.setEnabled(this._isVisible && hasInput && foundMatch);
|
||||
this.nextBtn.setEnabled(this._isVisible && hasInput && foundMatch);
|
||||
}
|
||||
}
|
||||
|
||||
// theming
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const findWidgetBGColor = theme.getColor(editorWidgetBackground);
|
||||
if (findWidgetBGColor) {
|
||||
collector.addRule(`.monaco-workbench .simple-fr-find-part-wrapper { background-color: ${findWidgetBGColor} !important; }`);
|
||||
}
|
||||
|
||||
const widgetForeground = theme.getColor(editorWidgetForeground);
|
||||
if (widgetForeground) {
|
||||
collector.addRule(`.monaco-workbench .simple-fr-find-part-wrapper { color: ${widgetForeground}; }`);
|
||||
}
|
||||
|
||||
const widgetShadowColor = theme.getColor(widgetShadow);
|
||||
if (widgetShadowColor) {
|
||||
collector.addRule(`.monaco-workbench .simple-fr-find-part-wrapper { box-shadow: 0 2px 8px ${widgetShadowColor}; }`);
|
||||
}
|
||||
});
|
|
@ -54,6 +54,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
|||
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
|
||||
import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess';
|
||||
import { StartDebugQuickAccessProvider } from 'vs/workbench/contrib/debug/browser/debugQuickAccess';
|
||||
import { DebugProgressContribution } from 'vs/workbench/contrib/debug/browser/debugProgress';
|
||||
|
||||
class OpenDebugViewletAction extends ShowViewletAction {
|
||||
public static readonly ID = VIEWLET_ID;
|
||||
|
@ -298,6 +299,7 @@ configurationRegistry.registerConfiguration({
|
|||
|
||||
// Register Debug Status
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugStatusContribution, LifecyclePhase.Eventually);
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugProgressContribution, LifecyclePhase.Eventually);
|
||||
|
||||
// Debug toolbar
|
||||
|
||||
|
|
53
src/vs/workbench/contrib/debug/browser/debugProgress.ts
Normal file
53
src/vs/workbench/contrib/debug/browser/debugProgress.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { IDebugService, VIEWLET_ID, IDebugSession } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class DebugProgressContribution implements IWorkbenchContribution {
|
||||
|
||||
private toDispose: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
@IDebugService private readonly debugService: IDebugService,
|
||||
@IProgressService private readonly progressService: IProgressService
|
||||
) {
|
||||
let progressListener: IDisposable;
|
||||
const onFocusSession = (session: IDebugSession | undefined) => {
|
||||
if (progressListener) {
|
||||
progressListener.dispose();
|
||||
}
|
||||
if (session) {
|
||||
progressListener = session.onDidProgressStart(async progressStartEvent => {
|
||||
const promise = new Promise<void>(r => {
|
||||
// Show progress until a progress end event comes or the session ends
|
||||
const listener = Event.any(Event.filter(session.onDidProgressEnd, e => e.body.progressId === progressStartEvent.body.progressId),
|
||||
session.onDidEndAdapter)(() => {
|
||||
listener.dispose();
|
||||
r();
|
||||
});
|
||||
});
|
||||
|
||||
this.progressService.withProgress({ location: VIEWLET_ID }, () => promise);
|
||||
this.progressService.withProgress({
|
||||
location: ProgressLocation.Notification,
|
||||
title: progressStartEvent.body.title,
|
||||
cancellable: progressStartEvent.body.cancellable,
|
||||
silent: true
|
||||
}, () => promise, () => session.cancel(progressStartEvent.body.progressId));
|
||||
});
|
||||
}
|
||||
};
|
||||
this.toDispose.push(this.debugService.getViewModel().onDidFocusSession(onFocusSession));
|
||||
onFocusSession(this.debugService.getViewModel().focusedSession);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this.toDispose);
|
||||
}
|
||||
}
|
|
@ -5,11 +5,10 @@
|
|||
|
||||
import 'vs/css!./media/debugViewlet';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IAction, Action } from 'vs/base/common/actions';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration, DEBUG_PANEL_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, IDebugSession } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration, DEBUG_PANEL_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { StartAction, ConfigureAction, SelectAndStartAction, FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions';
|
||||
import { StartDebugActionViewItem, FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
@ -44,7 +43,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer {
|
|||
private paneListeners = new Map<string, IDisposable>();
|
||||
private debugToolBarMenu: IMenu | undefined;
|
||||
private disposeOnTitleUpdate: IDisposable | undefined;
|
||||
private progressEvents: { event: DebugProtocol.ProgressStartEvent, session: IDebugSession }[] = [];
|
||||
|
||||
constructor(
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
|
@ -81,38 +79,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer {
|
|||
this.updateTitleArea();
|
||||
}
|
||||
}));
|
||||
|
||||
let progressListener: IDisposable;
|
||||
this._register(this.debugService.getViewModel().onDidFocusSession(session => {
|
||||
if (progressListener) {
|
||||
progressListener.dispose();
|
||||
}
|
||||
if (session) {
|
||||
progressListener = session.onDidProgressStart(async progressStartEvent => {
|
||||
// Update title area to show the cancel progress action
|
||||
this.progressEvents.push({ session: session, event: progressStartEvent });
|
||||
if (progressStartEvent.body.cancellable) {
|
||||
this.cancelAction.tooltip = nls.localize('cancelProgress', "Cancel {0}", progressStartEvent.body.title);
|
||||
this.updateTitleArea();
|
||||
}
|
||||
await this.progressService.withProgress({ location: VIEWLET_ID }, () => {
|
||||
return new Promise(r => {
|
||||
// Show progress until a progress end event comes or the session ends
|
||||
const listener = Event.any(Event.filter(session.onDidProgressEnd, e => e.body.progressId === progressStartEvent.body.progressId),
|
||||
session.onDidEndAdapter)(() => {
|
||||
listener.dispose();
|
||||
r();
|
||||
});
|
||||
});
|
||||
});
|
||||
this.progressEvents = this.progressEvents.filter(pe => pe.event.body.progressId !== progressStartEvent.body.progressId);
|
||||
if (progressStartEvent.body.cancellable) {
|
||||
this.cancelAction.tooltip = nls.localize('cancel', "Cancel");
|
||||
this.updateTitleArea();
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
create(parent: HTMLElement): void {
|
||||
|
@ -145,16 +111,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer {
|
|||
return this._register(this.instantiationService.createInstance(OpenDebugPanelAction, OpenDebugPanelAction.ID, OpenDebugPanelAction.LABEL));
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get cancelAction(): Action {
|
||||
return this._register(new Action('debug.cancelProgress', nls.localize('cancel', "Cancel"), 'debug-action codicon codicon-stop', true, async () => {
|
||||
const progressEvent = this.progressEvents.filter(e => e.event.body.cancellable).pop();
|
||||
if (progressEvent) {
|
||||
await progressEvent.session.cancel(progressEvent.event.body.progressId);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@memoize
|
||||
private get selectAndStartAction(): SelectAndStartAction {
|
||||
return this._register(this.instantiationService.createInstance(SelectAndStartAction, SelectAndStartAction.ID, nls.localize('startAdditionalSession', "Start Additional Session")));
|
||||
|
@ -165,7 +121,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer {
|
|||
return [];
|
||||
}
|
||||
|
||||
let result: IAction[];
|
||||
if (!this.showInitialDebugActions) {
|
||||
|
||||
if (!this.debugToolBarMenu) {
|
||||
|
@ -179,18 +134,14 @@ export class DebugViewPaneContainer extends ViewPaneContainer {
|
|||
}
|
||||
this.disposeOnTitleUpdate = disposable;
|
||||
|
||||
result = actions;
|
||||
} else if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
result = [this.toggleReplAction];
|
||||
} else {
|
||||
result = [this.startAction, this.configureAction, this.toggleReplAction];
|
||||
return actions;
|
||||
}
|
||||
|
||||
if (this.progressEvents.filter(e => e.event.body.cancellable).length) {
|
||||
result.unshift(this.cancelAction);
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
return [this.toggleReplAction];
|
||||
}
|
||||
|
||||
return result;
|
||||
return [this.startAction, this.configureAction, this.toggleReplAction];
|
||||
}
|
||||
|
||||
get showInitialDebugActions(): boolean {
|
||||
|
@ -235,7 +186,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer {
|
|||
}
|
||||
|
||||
if (state === State.Initializing) {
|
||||
this.progressService.withProgress({ location: VIEWLET_ID }, _progress => {
|
||||
this.progressService.withProgress({ location: VIEWLET_ID, }, _progress => {
|
||||
return new Promise(resolve => this.progressResolve = resolve);
|
||||
});
|
||||
}
|
||||
|
|
26
src/vs/workbench/contrib/notebook/browser/constants.ts
Normal file
26
src/vs/workbench/contrib/notebook/browser/constants.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const INSERT_CODE_CELL_ABOVE_COMMAND_ID = 'workbench.notebook.code.insertCellAbove';
|
||||
export const INSERT_CODE_CELL_BELOW_COMMAND_ID = 'workbench.notebook.code.insertCellBelow';
|
||||
export const INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID = 'workbench.notebook.markdown.insertCellAbove';
|
||||
export const INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID = 'workbench.notebook.markdown.insertCellAbove';
|
||||
|
||||
export const EDIT_CELL_COMMAND_ID = 'workbench.notebook.cell.edit';
|
||||
export const SAVE_CELL_COMMAND_ID = 'workbench.notebook.cell.save';
|
||||
export const DELETE_CELL_COMMAND_ID = 'workbench.notebook.cell.delete';
|
||||
|
||||
export const MOVE_CELL_UP_COMMAND_ID = 'workbench.notebook.cell.moveUp';
|
||||
export const MOVE_CELL_DOWN_COMMAND_ID = 'workbench.notebook.cell.moveDown';
|
||||
export const COPY_CELL_UP_COMMAND_ID = 'workbench.notebook.cell.copyUp';
|
||||
export const COPY_CELL_DOWN_COMMAND_ID = 'workbench.notebook.cell.copyDown';
|
||||
|
||||
export const EXECUTE_CELL_COMMAND_ID = 'workbench.notebook.cell.execute';
|
||||
|
||||
// Cell sizing related
|
||||
export const CELL_MARGIN = 24;
|
||||
export const EDITOR_TOP_PADDING = 8;
|
||||
export const EDITOR_BOTTOM_PADDING = 8;
|
||||
export const EDITOR_TOOLBAR_HEIGHT = 22;
|
|
@ -0,0 +1,956 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action2, IAction2Options, MenuId, MenuItemAction, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { InputFocusedContext, InputFocusedContextKey, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { DELETE_CELL_COMMAND_ID, EDIT_CELL_COMMAND_ID, INSERT_CODE_CELL_ABOVE_COMMAND_ID, INSERT_CODE_CELL_BELOW_COMMAND_ID, INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, MOVE_CELL_DOWN_COMMAND_ID, MOVE_CELL_UP_COMMAND_ID, SAVE_CELL_COMMAND_ID, COPY_CELL_UP_COMMAND_ID, COPY_CELL_DOWN_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/constants';
|
||||
import { INotebookEditor, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, ICellViewModel, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
|
||||
import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.executeNotebookCell',
|
||||
title: localize('notebookActions.execute', "Execute Notebook Cell"),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext),
|
||||
primary: KeyMod.WinCtrl | KeyCode.Enter,
|
||||
win: {
|
||||
primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter
|
||||
},
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
runActiveCell(accessor);
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.executeNotebookCellSelectBelow',
|
||||
title: localize('notebookActions.executeAndSelectBelow', "Execute Notebook Cell and Select Below"),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext),
|
||||
primary: KeyMod.Shift | KeyCode.Enter,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const activeCell = runActiveCell(accessor);
|
||||
if (!activeCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = getActiveNotebookEditor(editorService);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = editor.viewModel?.getViewCellIndex(activeCell);
|
||||
if (typeof idx !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to select below, fall back on inserting
|
||||
const nextCell = editor.viewModel?.viewCells[idx + 1];
|
||||
if (nextCell) {
|
||||
editor.focusNotebookCell(nextCell, false);
|
||||
} else {
|
||||
editor.insertNotebookCell(activeCell, CellKind.Code, 'below');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.executeNotebookCellInsertBelow',
|
||||
title: localize('notebookActions.executeAndInsertBelow', "Execute Notebook Cell and Insert Below"),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext),
|
||||
primary: KeyMod.Alt | KeyCode.Enter,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const activeCell = runActiveCell(accessor);
|
||||
if (!activeCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = getActiveNotebookEditor(editorService);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.insertNotebookCell(activeCell, CellKind.Code, 'below');
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.executeNotebook',
|
||||
title: localize('notebookActions.executeNotebook', "Execute Notebook")
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
let editorService = accessor.get(IEditorService);
|
||||
let notebookService = accessor.get(INotebookService);
|
||||
|
||||
let resource = editorService.activeEditor?.resource;
|
||||
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
|
||||
let notebookProviders = notebookService.getContributedNotebookProviders(resource!);
|
||||
|
||||
if (notebookProviders.length > 0) {
|
||||
let viewType = notebookProviders[0].id;
|
||||
notebookService.executeNotebook(viewType, resource);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.quitNotebookEdit',
|
||||
title: localize('notebookActions.quitEditing', "Quit Notebook Cell Editing"),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext),
|
||||
primary: KeyCode.Escape,
|
||||
weight: KeybindingWeight.EditorContrib - 5
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
let editorService = accessor.get(IEditorService);
|
||||
let editor = getActiveNotebookEditor(editorService);
|
||||
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
let activeCell = editor.getActiveCell();
|
||||
if (activeCell) {
|
||||
if (activeCell.cellKind === CellKind.Markdown) {
|
||||
activeCell.state = CellState.Preview;
|
||||
}
|
||||
|
||||
editor.focusNotebookCell(activeCell, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.notebook.hideFind',
|
||||
title: localize('notebookActions.hideFind', "Hide Find in Notebook"),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED),
|
||||
primary: KeyCode.Escape,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
let editorService = accessor.get(IEditorService);
|
||||
let editor = getActiveNotebookEditor(editorService);
|
||||
|
||||
editor?.hideFind();
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.notebook.find',
|
||||
title: localize('notebookActions.findInNotebook', "Find in Notebook"),
|
||||
keybinding: {
|
||||
when: NOTEBOOK_EDITOR_FOCUSED,
|
||||
primary: KeyCode.KEY_F | KeyMod.CtrlCmd,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
let editorService = accessor.get(IEditorService);
|
||||
let editor = getActiveNotebookEditor(editorService);
|
||||
|
||||
editor?.showFind();
|
||||
}
|
||||
});
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
|
||||
command: {
|
||||
id: 'workbench.action.executeNotebook',
|
||||
title: localize('notebookActions.menu.executeNotebook', "Execute Notebook (Run all cells)"),
|
||||
icon: { id: 'codicon/debug-start' }
|
||||
},
|
||||
order: -1,
|
||||
group: 'navigation',
|
||||
when: NOTEBOOK_EDITOR_FOCUSED
|
||||
});
|
||||
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorTitle, {
|
||||
command: {
|
||||
id: 'workbench.action.executeNotebookCell',
|
||||
title: localize('notebookActions.menu.execute', "Execute Notebook Cell"),
|
||||
icon: { id: 'codicon/debug-continue' }
|
||||
},
|
||||
order: -1,
|
||||
group: 'navigation',
|
||||
when: NOTEBOOK_EDITOR_FOCUSED
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.changeCellToCode',
|
||||
title: localize('notebookActions.changeCellToCode', "Change Cell to Code"),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
primary: KeyCode.KEY_Y,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
return changeActiveCellToKind(CellKind.Code, accessor);
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.changeCellToMarkdown',
|
||||
title: localize('notebookActions.changeCellToMarkdown', "Change Cell to Markdown"),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
primary: KeyCode.KEY_M,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
return changeActiveCellToKind(CellKind.Markdown, accessor);
|
||||
}
|
||||
});
|
||||
|
||||
function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor | undefined {
|
||||
// TODO can `isNotebookEditor` be on INotebookEditor to avoid a circular dependency?
|
||||
const activeEditorPane = editorService.activeEditorPane as any | undefined;
|
||||
return activeEditorPane?.isNotebookEditor ? activeEditorPane : undefined;
|
||||
}
|
||||
|
||||
function runActiveCell(accessor: ServicesAccessor): ICellViewModel | undefined {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const notebookService = accessor.get(INotebookService);
|
||||
|
||||
const resource = editorService.activeEditor?.resource;
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = getActiveNotebookEditor(editorService);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notebookProviders = notebookService.getContributedNotebookProviders(resource);
|
||||
if (!notebookProviders.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeCell = editor.getActiveCell();
|
||||
if (!activeCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = editor.viewModel?.getViewCellIndex(activeCell);
|
||||
if (typeof idx !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewType = notebookProviders[0].id;
|
||||
notebookService.executeNotebookActiveCell(viewType, resource);
|
||||
|
||||
return activeCell;
|
||||
}
|
||||
|
||||
function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor): void {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const editor = getActiveNotebookEditor(editorService);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeCell = editor.getActiveCell();
|
||||
if (!activeCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeCell.cellKind === kind) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = activeCell.getText();
|
||||
editor.insertNotebookCell(activeCell, kind, 'below', text);
|
||||
const idx = editor.viewModel?.getViewCellIndex(activeCell);
|
||||
if (typeof idx !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCell = editor.viewModel?.viewCells[idx + 1];
|
||||
if (!newCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.focusNotebookCell(newCell, false);
|
||||
editor.deleteNotebookCell(activeCell);
|
||||
}
|
||||
|
||||
export interface INotebookCellActionContext {
|
||||
cell: ICellViewModel;
|
||||
notebookEditor: INotebookEditor;
|
||||
}
|
||||
|
||||
function getActiveCellContext(accessor: ServicesAccessor): INotebookCellActionContext | undefined {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
|
||||
const editor = getActiveNotebookEditor(editorService);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeCell = editor.getActiveCell();
|
||||
if (!activeCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
cell: activeCell,
|
||||
notebookEditor: editor
|
||||
};
|
||||
}
|
||||
|
||||
abstract class InsertCellCommand extends Action2 {
|
||||
constructor(
|
||||
desc: Readonly<IAction2Options>,
|
||||
private kind: CellKind,
|
||||
private direction: 'above' | 'below'
|
||||
) {
|
||||
super(desc);
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise<void> {
|
||||
if (!context) {
|
||||
context = getActiveCellContext(accessor);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction);
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(class extends InsertCellCommand {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: INSERT_CODE_CELL_ABOVE_COMMAND_ID,
|
||||
title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above")
|
||||
},
|
||||
CellKind.Code,
|
||||
'above');
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends InsertCellCommand {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: INSERT_CODE_CELL_BELOW_COMMAND_ID,
|
||||
title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below")
|
||||
},
|
||||
CellKind.Code,
|
||||
'below');
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends InsertCellCommand {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID,
|
||||
title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"),
|
||||
},
|
||||
CellKind.Markdown,
|
||||
'above');
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends InsertCellCommand {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID,
|
||||
title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"),
|
||||
},
|
||||
CellKind.Code,
|
||||
'below');
|
||||
}
|
||||
});
|
||||
|
||||
export class InsertCodeCellAboveAction extends MenuItemAction {
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService commandService: ICommandService
|
||||
) {
|
||||
super(
|
||||
{
|
||||
id: INSERT_CODE_CELL_ABOVE_COMMAND_ID,
|
||||
title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above"),
|
||||
icon: { id: 'codicon/add' }
|
||||
},
|
||||
undefined,
|
||||
{ shouldForwardArgs: true },
|
||||
contextKeyService,
|
||||
commandService);
|
||||
}
|
||||
}
|
||||
|
||||
export class InsertCodeCellBelowAction extends MenuItemAction {
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService commandService: ICommandService
|
||||
) {
|
||||
super(
|
||||
{
|
||||
id: INSERT_CODE_CELL_BELOW_COMMAND_ID,
|
||||
title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below"),
|
||||
icon: { id: 'codicon/add' }
|
||||
},
|
||||
{
|
||||
id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID,
|
||||
title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"),
|
||||
icon: { id: 'codicon/add' }
|
||||
},
|
||||
{ shouldForwardArgs: true },
|
||||
contextKeyService,
|
||||
commandService);
|
||||
}
|
||||
}
|
||||
|
||||
export class InsertMarkdownCellAboveAction extends MenuItemAction {
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService commandService: ICommandService
|
||||
) {
|
||||
super(
|
||||
{
|
||||
id: INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID,
|
||||
title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"),
|
||||
icon: { id: 'codicon/add' }
|
||||
},
|
||||
undefined,
|
||||
{ shouldForwardArgs: true },
|
||||
contextKeyService,
|
||||
commandService);
|
||||
}
|
||||
}
|
||||
|
||||
export class InsertMarkdownCellBelowAction extends MenuItemAction {
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService commandService: ICommandService
|
||||
) {
|
||||
super(
|
||||
{
|
||||
id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID,
|
||||
title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"),
|
||||
icon: { id: 'codicon/add' }
|
||||
},
|
||||
undefined,
|
||||
{ shouldForwardArgs: true },
|
||||
contextKeyService,
|
||||
commandService);
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: EDIT_CELL_COMMAND_ID,
|
||||
title: localize('notebookActions.editCell', "Edit Cell"),
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
primary: KeyCode.Enter,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(accessor: ServicesAccessor, context?: INotebookCellActionContext) {
|
||||
if (!context) {
|
||||
context = getActiveCellContext(accessor);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return context.notebookEditor.editNotebookCell(context.cell);
|
||||
}
|
||||
});
|
||||
|
||||
export class EditCellAction extends MenuItemAction {
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService commandService: ICommandService
|
||||
) {
|
||||
super(
|
||||
{
|
||||
id: EDIT_CELL_COMMAND_ID,
|
||||
title: localize('notebookActions.editCell', "Edit Cell"),
|
||||
icon: { id: 'codicon/pencil' }
|
||||
},
|
||||
undefined,
|
||||
{ shouldForwardArgs: true },
|
||||
contextKeyService,
|
||||
commandService);
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: SAVE_CELL_COMMAND_ID,
|
||||
title: localize('notebookActions.saveCell', "Save Cell")
|
||||
});
|
||||
}
|
||||
|
||||
run(accessor: ServicesAccessor, context?: INotebookCellActionContext) {
|
||||
if (!context) {
|
||||
context = getActiveCellContext(accessor);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return context.notebookEditor.saveNotebookCell(context.cell);
|
||||
}
|
||||
});
|
||||
|
||||
export class SaveCellAction extends MenuItemAction {
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService commandService: ICommandService
|
||||
) {
|
||||
super(
|
||||
{
|
||||
id: SAVE_CELL_COMMAND_ID,
|
||||
title: localize('notebookActions.saveCell', "Save Cell"),
|
||||
icon: { id: 'codicon/save' }
|
||||
},
|
||||
undefined,
|
||||
{ shouldForwardArgs: true },
|
||||
contextKeyService,
|
||||
commandService);
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: DELETE_CELL_COMMAND_ID,
|
||||
title: localize('notebookActions.deleteCell', "Delete Cell")
|
||||
});
|
||||
}
|
||||
|
||||
run(accessor: ServicesAccessor, context?: INotebookCellActionContext) {
|
||||
if (!context) {
|
||||
context = getActiveCellContext(accessor);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return context.notebookEditor.deleteNotebookCell(context.cell);
|
||||
}
|
||||
});
|
||||
|
||||
export class DeleteCellAction extends MenuItemAction {
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService commandService: ICommandService
|
||||
) {
|
||||
super(
|
||||
{
|
||||
id: DELETE_CELL_COMMAND_ID,
|
||||
title: localize('notebookActions.deleteCell', "Delete Cell"),
|
||||
icon: { id: 'codicon/x' }
|
||||
},
|
||||
undefined,
|
||||
{ shouldForwardArgs: true },
|
||||
contextKeyService,
|
||||
commandService);
|
||||
|
||||
this.class = 'codicon-x';
|
||||
}
|
||||
}
|
||||
|
||||
async function moveCell(context: INotebookCellActionContext, direction: 'up' | 'down'): Promise<void> {
|
||||
direction === 'up' ?
|
||||
context.notebookEditor.moveCellUp(context.cell) :
|
||||
context.notebookEditor.moveCellDown(context.cell);
|
||||
}
|
||||
|
||||
async function copyCell(context: INotebookCellActionContext, direction: 'up' | 'down'): Promise<void> {
|
||||
const text = context.cell.getText();
|
||||
const newCellDirection = direction === 'up' ? 'above' : 'below';
|
||||
return context.notebookEditor.insertNotebookCell(context.cell, context.cell.cellKind, newCellDirection, text);
|
||||
}
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: MOVE_CELL_UP_COMMAND_ID,
|
||||
title: localize('notebookActions.moveCellUp', "Move Cell Up")
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) {
|
||||
if (!context) {
|
||||
context = getActiveCellContext(accessor);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return moveCell(context, 'up');
|
||||
}
|
||||
});
|
||||
|
||||
export class MoveCellUpAction extends MenuItemAction {
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService commandService: ICommandService
|
||||
) {
|
||||
super(
|
||||
{
|
||||
id: MOVE_CELL_UP_COMMAND_ID,
|
||||
title: localize('notebookActions.moveCellUp', "Move Cell Up"),
|
||||
icon: { id: 'codicon/arrow-up' }
|
||||
},
|
||||
{
|
||||
id: COPY_CELL_UP_COMMAND_ID,
|
||||
title: localize('notebookActions.copyCellUp', "Copy Cell Up"),
|
||||
icon: { id: 'codicon/arrow-up' }
|
||||
},
|
||||
{ shouldForwardArgs: true },
|
||||
contextKeyService,
|
||||
commandService);
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: MOVE_CELL_DOWN_COMMAND_ID,
|
||||
title: localize('notebookActions.moveCellDown', "Move Cell Down")
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) {
|
||||
if (!context) {
|
||||
context = getActiveCellContext(accessor);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return moveCell(context, 'down');
|
||||
}
|
||||
});
|
||||
|
||||
export class MoveCellDownAction extends MenuItemAction {
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICommandService commandService: ICommandService
|
||||
) {
|
||||
super(
|
||||
{
|
||||
id: MOVE_CELL_DOWN_COMMAND_ID,
|
||||
title: localize('notebookActions.moveCellDown', "Move Cell Down"),
|
||||
icon: { id: 'codicon/arrow-down' }
|
||||
},
|
||||
{
|
||||
id: COPY_CELL_DOWN_COMMAND_ID,
|
||||
title: localize('notebookActions.copyCellDown', "Copy Cell Down"),
|
||||
icon: { id: 'codicon/arrow-down' }
|
||||
},
|
||||
{ shouldForwardArgs: true },
|
||||
contextKeyService,
|
||||
commandService);
|
||||
|
||||
this.class = 'codicon-arrow-down';
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: COPY_CELL_UP_COMMAND_ID,
|
||||
title: localize('notebookActions.copyCellUp', "Copy Cell Up")
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) {
|
||||
if (!context) {
|
||||
context = getActiveCellContext(accessor);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return copyCell(context, 'up');
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super(
|
||||
{
|
||||
id: COPY_CELL_DOWN_COMMAND_ID,
|
||||
title: localize('notebookActions.copyCellDown', "Copy Cell Down")
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) {
|
||||
if (!context) {
|
||||
context = getActiveCellContext(accessor);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return copyCell(context, 'down');
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.notebook.cursorDown',
|
||||
title: 'Notebook Cursor Move Down',
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.has(InputFocusedContextKey), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('top'), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none')),
|
||||
primary: KeyCode.DownArrow,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise<void> {
|
||||
if (!context) {
|
||||
context = getActiveCellContext(accessor);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const editor = context.notebookEditor;
|
||||
const activeCell = context.cell;
|
||||
|
||||
const idx = editor.viewModel?.getViewCellIndex(activeCell);
|
||||
if (typeof idx !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
const newCell = editor.viewModel?.viewCells[idx + 1];
|
||||
|
||||
if (!newCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.focusNotebookCell(newCell, true);
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.notebook.cursorUp',
|
||||
title: 'Notebook Cursor Move Up',
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.has(InputFocusedContextKey), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('bottom'), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none')),
|
||||
primary: KeyCode.UpArrow,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise<void> {
|
||||
if (!context) {
|
||||
context = getActiveCellContext(accessor);
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const editor = context.notebookEditor;
|
||||
const activeCell = context.cell;
|
||||
|
||||
const idx = editor.viewModel?.getViewCellIndex(activeCell);
|
||||
if (typeof idx !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (idx < 1) {
|
||||
// we don't do loop
|
||||
return;
|
||||
}
|
||||
|
||||
const newCell = editor.viewModel?.viewCells[idx - 1];
|
||||
|
||||
if (!newCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.focusNotebookCell(newCell, true);
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.notebook.undo',
|
||||
title: 'Notebook Undo',
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_Z,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
|
||||
const editor = getActiveNotebookEditor(editorService);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewModel = editor.viewModel;
|
||||
|
||||
if (!viewModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewModel.undo();
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.notebook.redo',
|
||||
title: 'Notebook Redo',
|
||||
keybinding: {
|
||||
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
|
||||
const editor = getActiveNotebookEditor(editorService);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewModel = editor.viewModel;
|
||||
|
||||
if (!viewModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewModel.redo();
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.notebook.testResize',
|
||||
title: 'Notebook Test Cell Resize',
|
||||
keybinding: {
|
||||
when: IsDevelopmentContext,
|
||||
primary: undefined,
|
||||
weight: KeybindingWeight.WorkbenchContrib
|
||||
},
|
||||
f1: true
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const resource = editorService.activeEditor?.resource;
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = getActiveNotebookEditor(editorService);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cells = editor.viewModel?.viewCells;
|
||||
|
||||
if (cells && cells.length) {
|
||||
const firstCell = cells[0];
|
||||
editor.layoutNotebookCell(firstCell, 400);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,234 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { FindDecorations } from 'vs/editor/contrib/find/findDecorations';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { IModelDeltaDecoration } from 'vs/editor/common/model';
|
||||
import { ICellModelDeltaDecorations, ICellModelDecorations } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
|
||||
import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export class NotebookFindWidget extends SimpleFindReplaceWidget {
|
||||
protected _findWidgetFocused: IContextKey<boolean>;
|
||||
private _findMatches: CellFindMatch[] = [];
|
||||
protected _findMatchesStarts: PrefixSumComputer | null = null;
|
||||
private _currentMatch: number = -1;
|
||||
private _allMatchesDecorations: ICellModelDecorations[] = [];
|
||||
private _currentMatchDecorations: ICellModelDecorations[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly _notebookEditor: INotebookEditor,
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
|
||||
) {
|
||||
super(contextViewService, contextKeyService, themeService);
|
||||
this._findWidgetFocused = KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED.bindTo(contextKeyService);
|
||||
this._register(this._findInput.onKeyDown((e) => this._onFindInputKeyDown(e)));
|
||||
}
|
||||
|
||||
private _onFindInputKeyDown(e: IKeyboardEvent): void {
|
||||
if (e.equals(KeyCode.Enter)) {
|
||||
if (this._findMatches.length) {
|
||||
this.set(this._findMatches);
|
||||
|
||||
if (this._currentMatch !== -1) {
|
||||
const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch);
|
||||
this.revealCellRange(nextIndex.index, nextIndex.remainder);
|
||||
}
|
||||
} else {
|
||||
this.set(null);
|
||||
}
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected onInputChanged(): boolean {
|
||||
const val = this.inputValue;
|
||||
if (val) {
|
||||
this._findMatches = this._notebookEditor.viewModel!.find(val).filter(match => match.matches.length > 0);
|
||||
if (this._findMatches.length) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected find(previous: boolean): void {
|
||||
if (!this._findMatches.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._findMatchesStarts) {
|
||||
this.set(this._findMatches);
|
||||
}
|
||||
|
||||
const totalVal = this._findMatchesStarts!.getTotalValue();
|
||||
const nextVal = (this._currentMatch + (previous ? -1 : 1) + totalVal) % totalVal;
|
||||
this._currentMatch = nextVal;
|
||||
|
||||
const nextIndex = this._findMatchesStarts!.getIndexOf(nextVal);
|
||||
this.setCurrentFindMatchDecoration(nextIndex.index, nextIndex.remainder);
|
||||
this.revealCellRange(nextIndex.index, nextIndex.remainder);
|
||||
}
|
||||
|
||||
protected replaceOne() {
|
||||
if (!this._findMatches.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._findMatchesStarts) {
|
||||
this.set(this._findMatches);
|
||||
}
|
||||
|
||||
const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch);
|
||||
const cell = this._findMatches[nextIndex.index].cell;
|
||||
const match = this._findMatches[nextIndex.index].matches[nextIndex.remainder];
|
||||
|
||||
this._progressBar.infinite().show();
|
||||
|
||||
this._notebookEditor.viewModel!.replaceOne(cell, match.range, this.replaceValue).then(() => {
|
||||
this._progressBar.stop();
|
||||
});
|
||||
}
|
||||
|
||||
protected replaceAll() {
|
||||
this._progressBar.infinite().show();
|
||||
|
||||
this._notebookEditor.viewModel!.replaceAll(this._findMatches, this.replaceValue).then(() => {
|
||||
this._progressBar.stop();
|
||||
});
|
||||
}
|
||||
|
||||
private revealCellRange(cellIndex: number, matchIndex: number) {
|
||||
this._findMatches[cellIndex].cell.state = CellState.Editing;
|
||||
this._notebookEditor.selectElement(this._findMatches[cellIndex].cell);
|
||||
this._notebookEditor.setCellSelection(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range);
|
||||
this._notebookEditor.revealRangeInCenterIfOutsideViewport(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range);
|
||||
}
|
||||
|
||||
hide() {
|
||||
super.hide();
|
||||
this.set([]);
|
||||
}
|
||||
|
||||
protected findFirst(): void { }
|
||||
|
||||
protected onFocusTrackerFocus() {
|
||||
this._findWidgetFocused.set(true);
|
||||
}
|
||||
|
||||
protected onFocusTrackerBlur() {
|
||||
this._findWidgetFocused.reset();
|
||||
}
|
||||
|
||||
protected onReplaceInputFocusTrackerFocus(): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
protected onReplaceInputFocusTrackerBlur(): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
protected onFindInputFocusTrackerFocus(): void { }
|
||||
protected onFindInputFocusTrackerBlur(): void { }
|
||||
|
||||
private constructFindMatchesStarts() {
|
||||
if (this._findMatches && this._findMatches.length) {
|
||||
const values = new Uint32Array(this._findMatches.length);
|
||||
for (let i = 0; i < this._findMatches.length; i++) {
|
||||
values[i] = this._findMatches[i].matches.length;
|
||||
}
|
||||
|
||||
this._findMatchesStarts = new PrefixSumComputer(values);
|
||||
} else {
|
||||
this._findMatchesStarts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private set(cellFindMatches: CellFindMatch[] | null): void {
|
||||
if (!cellFindMatches || !cellFindMatches.length) {
|
||||
this._findMatches = [];
|
||||
this.setAllFindMatchesDecorations([]);
|
||||
|
||||
this.constructFindMatchesStarts();
|
||||
this._currentMatch = -1;
|
||||
this.clearCurrentFindMatchDecoration();
|
||||
return;
|
||||
}
|
||||
|
||||
// all matches
|
||||
this._findMatches = cellFindMatches;
|
||||
this.setAllFindMatchesDecorations(cellFindMatches || []);
|
||||
|
||||
// current match
|
||||
this.constructFindMatchesStarts();
|
||||
this._currentMatch = 0;
|
||||
this.setCurrentFindMatchDecoration(0, 0);
|
||||
}
|
||||
|
||||
private setCurrentFindMatchDecoration(cellIndex: number, matchIndex: number) {
|
||||
this._notebookEditor.changeDecorations(accessor => {
|
||||
const findMatchesOptions: ModelDecorationOptions = FindDecorations._CURRENT_FIND_MATCH_DECORATION;
|
||||
|
||||
const cell = this._findMatches[cellIndex].cell;
|
||||
const match = this._findMatches[cellIndex].matches[matchIndex];
|
||||
const decorations: IModelDeltaDecoration[] = [
|
||||
{ range: match.range, options: findMatchesOptions }
|
||||
];
|
||||
const deltaDecoration: ICellModelDeltaDecorations = {
|
||||
ownerId: cell.handle,
|
||||
decorations: decorations
|
||||
};
|
||||
|
||||
this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, [deltaDecoration]);
|
||||
});
|
||||
}
|
||||
|
||||
private clearCurrentFindMatchDecoration() {
|
||||
this._notebookEditor.changeDecorations(accessor => {
|
||||
this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, []);
|
||||
});
|
||||
}
|
||||
|
||||
private setAllFindMatchesDecorations(cellFindMatches: CellFindMatch[]) {
|
||||
this._notebookEditor.changeDecorations((accessor) => {
|
||||
|
||||
let findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION;
|
||||
|
||||
let deltaDecorations: ICellModelDeltaDecorations[] = cellFindMatches.map(cellFindMatch => {
|
||||
const findMatches = cellFindMatch.matches;
|
||||
|
||||
// Find matches
|
||||
let newFindMatchesDecorations: IModelDeltaDecoration[] = new Array<IModelDeltaDecoration>(findMatches.length);
|
||||
for (let i = 0, len = findMatches.length; i < len; i++) {
|
||||
newFindMatchesDecorations[i] = {
|
||||
range: findMatches[i].range,
|
||||
options: findMatchesOptions
|
||||
};
|
||||
}
|
||||
|
||||
return { ownerId: cellFindMatch.cell.handle, decorations: newFindMatchesDecorations };
|
||||
});
|
||||
|
||||
this._allMatchesDecorations = accessor.deltaDecorations(this._allMatchesDecorations, deltaDecorations);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._currentMatch = -1;
|
||||
this._findMatches = [];
|
||||
}
|
||||
}
|
119
src/vs/workbench/contrib/notebook/browser/extensionPoint.ts
Normal file
119
src/vs/workbench/contrib/notebook/browser/extensionPoint.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookProvider';
|
||||
|
||||
namespace NotebookEditorContribution {
|
||||
export const viewType = 'viewType';
|
||||
export const displayName = 'displayName';
|
||||
export const selector = 'selector';
|
||||
}
|
||||
|
||||
interface INotebookEditorContribution {
|
||||
readonly [NotebookEditorContribution.viewType]: string;
|
||||
readonly [NotebookEditorContribution.displayName]: string;
|
||||
readonly [NotebookEditorContribution.selector]?: readonly NotebookSelector[];
|
||||
}
|
||||
|
||||
namespace NotebookRendererContribution {
|
||||
export const viewType = 'viewType';
|
||||
export const displayName = 'displayName';
|
||||
export const mimeTypes = 'mimeTypes';
|
||||
}
|
||||
|
||||
interface INotebookRendererContribution {
|
||||
readonly [NotebookRendererContribution.viewType]: string;
|
||||
readonly [NotebookRendererContribution.displayName]: string;
|
||||
readonly [NotebookRendererContribution.mimeTypes]?: readonly string[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
const notebookProviderContribution: IJSONSchema = {
|
||||
description: nls.localize('contributes.notebook.provider', 'Contributes notebook document provider.'),
|
||||
type: 'array',
|
||||
defaultSnippets: [{ body: [{ viewType: '', displayName: '' }] }],
|
||||
items: {
|
||||
type: 'object',
|
||||
required: [
|
||||
NotebookEditorContribution.viewType,
|
||||
NotebookEditorContribution.displayName,
|
||||
NotebookEditorContribution.selector,
|
||||
],
|
||||
properties: {
|
||||
[NotebookEditorContribution.viewType]: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.notebook.provider.viewType', 'Unique identifier of the notebook.'),
|
||||
},
|
||||
[NotebookEditorContribution.displayName]: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.notebook.provider.displayName', 'Human readable name of the notebook.'),
|
||||
},
|
||||
[NotebookEditorContribution.selector]: {
|
||||
type: 'array',
|
||||
description: nls.localize('contributes.notebook.provider.selector', 'Set of globs that the notebook is for.'),
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
filenamePattern: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.notebook.provider.selector.filenamePattern', 'Glob that the notebook is enabled for.'),
|
||||
},
|
||||
excludeFileNamePattern: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.notebook.selector.provider.excludeFileNamePattern', 'Glob that the notebook is disabled for.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const notebookRendererContribution: IJSONSchema = {
|
||||
description: nls.localize('contributes.notebook.renderer', 'Contributes notebook output renderer provider.'),
|
||||
type: 'array',
|
||||
defaultSnippets: [{ body: [{ viewType: '', displayName: '', mimeTypes: [''] }] }],
|
||||
items: {
|
||||
type: 'object',
|
||||
required: [
|
||||
NotebookRendererContribution.viewType,
|
||||
NotebookRendererContribution.displayName,
|
||||
NotebookRendererContribution.mimeTypes,
|
||||
],
|
||||
properties: {
|
||||
[NotebookRendererContribution.viewType]: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'),
|
||||
},
|
||||
[NotebookRendererContribution.displayName]: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.notebook.renderer.displayName', 'Human readable name of the notebook output renderer.'),
|
||||
},
|
||||
[NotebookRendererContribution.mimeTypes]: {
|
||||
type: 'array',
|
||||
description: nls.localize('contributes.notebook.selector', 'Set of globs that the notebook is for.'),
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const notebookProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint<INotebookEditorContribution[]>(
|
||||
{
|
||||
extensionPoint: 'notebookProvider',
|
||||
jsonSchema: notebookProviderContribution
|
||||
});
|
||||
|
||||
export const notebookRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint<INotebookRendererContribution[]>(
|
||||
{
|
||||
extensionPoint: 'notebookOutputRenderer',
|
||||
jsonSchema: notebookRendererContribution
|
||||
});
|
|
@ -0,0 +1,220 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
|
||||
import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
|
||||
import { IEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor';
|
||||
import { NotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditor';
|
||||
import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
|
||||
import { INotebookService, NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
|
||||
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { assertType } from 'vs/base/common/types';
|
||||
import { parse } from 'vs/base/common/marshalling';
|
||||
import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
|
||||
// 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';
|
||||
|
||||
// Actions
|
||||
import 'vs/workbench/contrib/notebook/browser/contrib/notebookActions';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
|
||||
|
||||
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
|
||||
EditorDescriptor.create(
|
||||
NotebookEditor,
|
||||
NotebookEditor.ID,
|
||||
'Notebook Editor'
|
||||
),
|
||||
[
|
||||
new SyncDescriptor(NotebookEditorInput)
|
||||
]
|
||||
);
|
||||
|
||||
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(
|
||||
NotebookEditorInput.ID,
|
||||
class implements IEditorInputFactory {
|
||||
canSerialize(): boolean {
|
||||
return true;
|
||||
}
|
||||
serialize(input: EditorInput): string {
|
||||
assertType(input instanceof NotebookEditorInput);
|
||||
return JSON.stringify({
|
||||
resource: input.resource,
|
||||
name: input.name,
|
||||
viewType: input.viewType,
|
||||
});
|
||||
}
|
||||
deserialize(instantiationService: IInstantiationService, raw: string) {
|
||||
type Data = { resource: URI, name: string, viewType: string };
|
||||
const data = <Data>parse(raw);
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
const { resource, name, viewType } = data;
|
||||
if (!data || !URI.isUri(resource) || typeof name !== 'string' || typeof viewType !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
// TODO@joh,peng this is disabled because the note-editor isn't fit for being
|
||||
// restorted (as it seems)
|
||||
if ('true') {
|
||||
return undefined;
|
||||
}
|
||||
return instantiationService.createInstance(NotebookEditorInput, resource, name, viewType);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function getFirstNotebookInfo(notebookService: INotebookService, uri: URI): NotebookProviderInfo | undefined {
|
||||
return notebookService.getContributedNotebookProviders(uri)[0];
|
||||
}
|
||||
|
||||
export class NotebookContribution implements IWorkbenchContribution {
|
||||
private _resourceMapping = new ResourceMap<NotebookEditorInput>();
|
||||
|
||||
constructor(
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@INotebookService private readonly notebookService: INotebookService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService
|
||||
|
||||
) {
|
||||
this.editorService.overrideOpenEditor((editor, options, group) => this.onEditorOpening(editor, options, group));
|
||||
|
||||
this.editorService.onDidActiveEditorChange(() => {
|
||||
if (this.editorService.activeEditor && this.editorService.activeEditor! instanceof NotebookEditorInput) {
|
||||
let editorInput = this.editorService.activeEditor! as NotebookEditorInput;
|
||||
this.notebookService.updateActiveNotebookDocument(editorInput.viewType!, editorInput.resource!);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onEditorOpening(originalInput: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined {
|
||||
let resource = originalInput.resource;
|
||||
if (!resource) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let info: NotebookProviderInfo | undefined;
|
||||
const data = CellUri.parse(resource);
|
||||
if (data && (info = getFirstNotebookInfo(this.notebookService, data.notebook))) {
|
||||
// cell-uri -> open (container) notebook
|
||||
const name = basename(data.notebook);
|
||||
const input = this.instantiationService.createInstance(NotebookEditorInput, data.notebook, name, info.id);
|
||||
this._resourceMapping.set(resource, input);
|
||||
return { override: this.editorService.openEditor(input, new NotebookEditorOptions({ ...options, forceReload: true, cellOptions: { resource, options } }), group) };
|
||||
}
|
||||
|
||||
info = getFirstNotebookInfo(this.notebookService, resource);
|
||||
if (!info) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this._resourceMapping.has(resource)) {
|
||||
const input = this._resourceMapping.get(resource);
|
||||
|
||||
if (!input!.isDisposed()) {
|
||||
return { override: this.editorService.openEditor(input!, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group) };
|
||||
}
|
||||
}
|
||||
|
||||
const input = this.instantiationService.createInstance(NotebookEditorInput, resource, originalInput.getName(), info.id);
|
||||
this._resourceMapping.set(resource, input);
|
||||
|
||||
return { override: this.editorService.openEditor(input, options, group) };
|
||||
}
|
||||
}
|
||||
|
||||
class CellContentProvider implements ITextModelContentProvider {
|
||||
|
||||
private readonly _registration: IDisposable;
|
||||
|
||||
constructor(
|
||||
@ITextModelService textModelService: ITextModelService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IModeService private readonly _modeService: IModeService,
|
||||
@INotebookService private readonly _notebookService: INotebookService,
|
||||
) {
|
||||
this._registration = textModelService.registerTextModelContentProvider('vscode-notebook', this);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._registration.dispose();
|
||||
}
|
||||
|
||||
async provideTextContent(resource: URI): Promise<ITextModel | null> {
|
||||
const existing = this._modelService.getModel(resource);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const data = CellUri.parse(resource);
|
||||
// const data = parseCellUri(resource);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const info = getFirstNotebookInfo(this._notebookService, data.notebook);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
const notebook = await this._notebookService.resolveNotebook(info.id, data.notebook);
|
||||
if (!notebook) {
|
||||
return null;
|
||||
}
|
||||
for (let cell of notebook.cells) {
|
||||
if (cell.uri.toString() === resource.toString()) {
|
||||
let bufferFactory = cell.resolveTextBufferFactory();
|
||||
return this._modelService.createModel(
|
||||
bufferFactory,
|
||||
cell.language ? this._modeService.create(cell.language) : this._modeService.createByFilepathOrFirstLine(resource, cell.source[0]),
|
||||
resource
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
|
||||
workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting);
|
||||
workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting);
|
||||
|
||||
registerSingleton(INotebookService, NotebookService);
|
||||
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
id: 'notebook',
|
||||
order: 100,
|
||||
title: nls.localize('notebookConfigurationTitle', "Notebook"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'notebook.displayOrder': {
|
||||
markdownDescription: nls.localize('notebook.displayOrder.description', "Priority list for output mime types"),
|
||||
type: ['array'],
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
default: []
|
||||
}
|
||||
}
|
||||
});
|
294
src/vs/workbench/contrib/notebook/browser/notebook.css
Normal file
294
src/vs/workbench/contrib/notebook/browser/notebook.css
Normal file
|
@ -0,0 +1,294 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor {
|
||||
box-sizing: border-box;
|
||||
line-height: 22px;
|
||||
user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cell.markdown {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
white-space: initial;
|
||||
}
|
||||
|
||||
/* .monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell {
|
||||
transform: translate3d(0, 0, 0);
|
||||
} */
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .notebook-content-widgets {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .output {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
user-select: text;
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
cursor: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .output p {
|
||||
white-space: initial;
|
||||
overflow-x: auto;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .output > div.foreground {
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .output .multi-mimetype-output {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: -24px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .output .error_message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .output pre.traceback {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .output .traceback > span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .output .display img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:focus-within {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
visibility: hidden;
|
||||
width: 16px;
|
||||
margin: auto;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .menu.mouseover,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .menu {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* .monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row:hover {
|
||||
outline: none !important;
|
||||
} */
|
||||
|
||||
/* .monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.focused {
|
||||
outline: none !important;
|
||||
} */
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .menu.mouseover,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .menu:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .monaco-toolbar {
|
||||
visibility: hidden;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.focused .monaco-toolbar,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .monaco-toolbar {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-tree.focused.no-focused-item:focus:before,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .monaco-list:not(.element-focused):focus:before {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.notebook-webview {
|
||||
position: absolute;
|
||||
z-index: 1000000;
|
||||
left: 373px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
/* markdown */
|
||||
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a:focus,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown input:focus,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown select:focus,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown textarea:focus {
|
||||
outline: 1px solid -webkit-focus-ring-color;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr {
|
||||
border: 0;
|
||||
height: 2px;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 {
|
||||
padding-bottom: 0.3em;
|
||||
line-height: 1.2;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h2,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table th,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table td {
|
||||
border: 1px solid ;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > td,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > th,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr + tr > td {
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown blockquote {
|
||||
margin: 0 7px 0 5px;
|
||||
padding: 0 16px 0 10px;
|
||||
border-left-width: 5px;
|
||||
border-left-style: solid;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown code {
|
||||
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
|
||||
font-size: 1em;
|
||||
line-height: 1.357em;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown body.wordWrap pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre:not(.hljs),
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre.hljs code > div {
|
||||
padding: 16px;
|
||||
border-radius: 3px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre code {
|
||||
color: var(--vscode-editor-foreground);
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex img,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block img {
|
||||
filter: brightness(0) invert(0)
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex img,
|
||||
.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block img {
|
||||
filter: brightness(0) invert(1)
|
||||
}
|
||||
|
||||
/** Theming */
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre {
|
||||
background-color: rgba(220, 220, 220, 0.4);
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre {
|
||||
background-color: rgba(10, 10, 10, 0.4);
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre {
|
||||
background-color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 {
|
||||
border-color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th {
|
||||
border-color: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td {
|
||||
border-color: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1,
|
||||
.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr,
|
||||
.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
255
src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts
Normal file
255
src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts
Normal file
|
@ -0,0 +1,255 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
|
||||
import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer';
|
||||
import { IOutput, CellKind, IRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { NotebookViewModel, IModelDecorationsChangeAccessor } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
|
||||
import { FindMatch } from 'vs/editor/common/model';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey<boolean>('notebookFindWidgetFocused', false);
|
||||
|
||||
export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey<boolean>('notebookEditorFocused', false);
|
||||
|
||||
export interface NotebookLayoutInfo {
|
||||
width: number;
|
||||
height: number;
|
||||
fontInfo: BareFontInfo;
|
||||
}
|
||||
|
||||
export interface ICellViewModel {
|
||||
readonly id: string;
|
||||
handle: number;
|
||||
uri: URI;
|
||||
cellKind: CellKind;
|
||||
state: CellState;
|
||||
focusMode: CellFocusMode;
|
||||
getText(): string;
|
||||
}
|
||||
|
||||
export interface INotebookEditor {
|
||||
|
||||
/**
|
||||
* Notebook view model attached to the current editor
|
||||
*/
|
||||
viewModel: NotebookViewModel | undefined;
|
||||
|
||||
/**
|
||||
* Focus the notebook editor cell list
|
||||
*/
|
||||
focus(): void;
|
||||
|
||||
/**
|
||||
* Select & focus cell
|
||||
*/
|
||||
selectElement(cell: ICellViewModel): void;
|
||||
|
||||
/**
|
||||
* Layout info for the notebook editor
|
||||
*/
|
||||
getLayoutInfo(): NotebookLayoutInfo;
|
||||
/**
|
||||
* Fetch the output renderers for notebook outputs.
|
||||
*/
|
||||
getOutputRenderer(): OutputRenderer;
|
||||
|
||||
/**
|
||||
* Insert a new cell around `cell`
|
||||
*/
|
||||
insertNotebookCell(cell: ICellViewModel, type: CellKind, direction: 'above' | 'below', initialText?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Delete a cell from the notebook
|
||||
*/
|
||||
deleteNotebookCell(cell: ICellViewModel): void;
|
||||
|
||||
/**
|
||||
* Move a cell up one spot
|
||||
*/
|
||||
moveCellUp(cell: ICellViewModel): void;
|
||||
|
||||
/**
|
||||
* Move a cell down one spot
|
||||
*/
|
||||
moveCellDown(cell: ICellViewModel): void;
|
||||
|
||||
/**
|
||||
* Switch the cell into editing mode.
|
||||
*
|
||||
* For code cell, the monaco editor will be focused.
|
||||
* For markdown cell, it will switch from preview mode to editing mode, which focuses the monaco editor.
|
||||
*/
|
||||
editNotebookCell(cell: ICellViewModel): void;
|
||||
|
||||
/**
|
||||
* Quit cell editing mode.
|
||||
*/
|
||||
saveNotebookCell(cell: ICellViewModel): void;
|
||||
|
||||
/**
|
||||
* Focus the container of a cell (the monaco editor inside is not focused).
|
||||
*/
|
||||
focusNotebookCell(cell: ICellViewModel, focusEditor: boolean): void;
|
||||
|
||||
/**
|
||||
* Get current active cell
|
||||
*/
|
||||
getActiveCell(): ICellViewModel | undefined;
|
||||
|
||||
/**
|
||||
* Layout the cell with a new height
|
||||
*/
|
||||
layoutNotebookCell(cell: ICellViewModel, height: number): void;
|
||||
|
||||
/**
|
||||
* Render the output in webview layer
|
||||
*/
|
||||
createInset(cell: ICellViewModel, output: IOutput, shadowContent: string, offset: number): void;
|
||||
|
||||
/**
|
||||
* Remove the output from the webview layer
|
||||
*/
|
||||
removeInset(output: IOutput): void;
|
||||
|
||||
/**
|
||||
* Trigger the editor to scroll from scroll event programmatically
|
||||
*/
|
||||
triggerScroll(event: IMouseWheelEvent): void;
|
||||
|
||||
/**
|
||||
* Reveal cell into viewport.
|
||||
*/
|
||||
revealInView(cell: ICellViewModel): void;
|
||||
|
||||
/**
|
||||
* Reveal cell into viewport center.
|
||||
*/
|
||||
revealInCenter(cell: ICellViewModel): void;
|
||||
|
||||
/**
|
||||
* Reveal cell into viewport center if cell is currently out of the viewport.
|
||||
*/
|
||||
revealInCenterIfOutsideViewport(cell: ICellViewModel): void;
|
||||
|
||||
/**
|
||||
* Reveal a line in notebook cell into viewport with minimal scrolling.
|
||||
*/
|
||||
revealLineInView(cell: ICellViewModel, line: number): void;
|
||||
|
||||
/**
|
||||
* Reveal a line in notebook cell into viewport center.
|
||||
*/
|
||||
revealLineInCenter(cell: ICellViewModel, line: number): void;
|
||||
|
||||
/**
|
||||
* Reveal a line in notebook cell into viewport center.
|
||||
*/
|
||||
revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number): void;
|
||||
|
||||
/**
|
||||
* Reveal a range in notebook cell into viewport with minimal scrolling.
|
||||
*/
|
||||
revealRangeInView(cell: ICellViewModel, range: Range): void;
|
||||
|
||||
/**
|
||||
* Reveal a range in notebook cell into viewport center.
|
||||
*/
|
||||
revealRangeInCenter(cell: ICellViewModel, range: Range): void;
|
||||
|
||||
/**
|
||||
* Reveal a range in notebook cell into viewport center.
|
||||
*/
|
||||
revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void;
|
||||
|
||||
setCellSelection(cell: ICellViewModel, selection: Range): void;
|
||||
|
||||
/**
|
||||
* Change the decorations on cells.
|
||||
* The notebook is virtualized and this method should be called to create/delete editor decorations safely.
|
||||
*/
|
||||
changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any;
|
||||
|
||||
/**
|
||||
* Show Find Widget.
|
||||
*
|
||||
* Currently Find is still part of the NotebookEditor core
|
||||
*/
|
||||
showFind(): void;
|
||||
|
||||
/**
|
||||
* Hide Find Widget
|
||||
*/
|
||||
hideFind(): void;
|
||||
}
|
||||
|
||||
export interface CellRenderTemplate {
|
||||
container: HTMLElement;
|
||||
cellContainer: HTMLElement;
|
||||
menuContainer?: HTMLElement;
|
||||
toolbar: ToolBar;
|
||||
editingContainer?: HTMLElement;
|
||||
outputContainer?: HTMLElement;
|
||||
editor?: CodeEditorWidget;
|
||||
disposables: DisposableStore;
|
||||
}
|
||||
|
||||
export interface IOutputTransformContribution {
|
||||
/**
|
||||
* Dispose this contribution.
|
||||
*/
|
||||
dispose(): void;
|
||||
|
||||
render(output: IOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput;
|
||||
}
|
||||
|
||||
export interface CellFindMatch {
|
||||
cell: CellViewModel;
|
||||
matches: FindMatch[];
|
||||
}
|
||||
|
||||
export enum CellRevealType {
|
||||
Line,
|
||||
Range
|
||||
}
|
||||
|
||||
export enum CellRevealPosition {
|
||||
Top,
|
||||
Center
|
||||
}
|
||||
|
||||
export enum CellState {
|
||||
/**
|
||||
* Default state.
|
||||
* For markdown cell, it's Markdown preview.
|
||||
* For code cell, the browser focus should be on the container instead of the editor
|
||||
*/
|
||||
Preview,
|
||||
|
||||
|
||||
/**
|
||||
* Eding mode. Source for markdown or code is rendered in editors and the state will be persistent.
|
||||
*/
|
||||
Editing
|
||||
}
|
||||
|
||||
export enum CellFocusMode {
|
||||
Container,
|
||||
Editor
|
||||
}
|
||||
|
||||
export enum CursorAtBoundary {
|
||||
None,
|
||||
Top,
|
||||
Bottom,
|
||||
Both
|
||||
}
|
745
src/vs/workbench/contrib/notebook/browser/notebookEditor.ts
Normal file
745
src/vs/workbench/contrib/notebook/browser/notebookEditor.ts
Normal file
|
@ -0,0 +1,745 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getZoomLevel } from 'vs/base/browser/browser';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import 'vs/css!./notebook';
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { contrastBorder, editorBackground, focusBorder, foreground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { EditorOptions, IEditorMemento, IEditorCloseEvent } from 'vs/workbench/common/editor';
|
||||
import { INotebookEditor, NotebookLayoutInfo, CellState, NOTEBOOK_EDITOR_FOCUSED, CellFocusMode, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { NotebookEditorInput, NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
|
||||
import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
|
||||
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 } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer';
|
||||
import { IOutput, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils';
|
||||
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IEditor, ICompositeCodeEditor } from 'vs/editor/common/editorCommon';
|
||||
import { IResourceEditorInput } from 'vs/platform/editor/common/editor';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList';
|
||||
import { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget';
|
||||
import { NotebookViewModel, INotebookEditorViewState, IModelDecorationsChangeAccessor } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
|
||||
import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor';
|
||||
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants';
|
||||
|
||||
const $ = DOM.$;
|
||||
const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState';
|
||||
|
||||
export class NotebookEditorOptions extends EditorOptions {
|
||||
|
||||
readonly cellOptions?: IResourceEditorInput;
|
||||
|
||||
constructor(options: Partial<NotebookEditorOptions>) {
|
||||
super();
|
||||
this.overwrite(options);
|
||||
this.cellOptions = options.cellOptions;
|
||||
}
|
||||
|
||||
with(options: Partial<NotebookEditorOptions>): NotebookEditorOptions {
|
||||
return new NotebookEditorOptions({ ...this, ...options });
|
||||
}
|
||||
}
|
||||
|
||||
export class NotebookCodeEditors implements ICompositeCodeEditor {
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private readonly _onDidChangeActiveEditor = new Emitter<this>();
|
||||
readonly onDidChangeActiveEditor: Event<this> = this._onDidChangeActiveEditor.event;
|
||||
|
||||
constructor(
|
||||
private _list: NotebookCellList,
|
||||
private _renderedEditors: Map<ICellViewModel, ICodeEditor | undefined>
|
||||
) {
|
||||
_list.onDidChangeFocus(_e => this._onDidChangeActiveEditor.fire(this), undefined, this._disposables);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onDidChangeActiveEditor.dispose();
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
get activeCodeEditor(): IEditor | undefined {
|
||||
const [focused] = this._list.getFocusedElements();
|
||||
return focused instanceof CellViewModel
|
||||
? this._renderedEditors.get(focused)
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class NotebookEditor extends BaseEditor implements INotebookEditor {
|
||||
static readonly ID: string = 'workbench.editor.notebook';
|
||||
private rootElement!: HTMLElement;
|
||||
private body!: HTMLElement;
|
||||
private webview: BackLayerWebView | null = null;
|
||||
private list: NotebookCellList | undefined;
|
||||
private control: ICompositeCodeEditor | undefined;
|
||||
private renderedEditors: Map<ICellViewModel, ICodeEditor | undefined> = new Map();
|
||||
private notebookViewModel: NotebookViewModel | undefined;
|
||||
private localStore: DisposableStore = this._register(new DisposableStore());
|
||||
private editorMemento: IEditorMemento<INotebookEditorViewState>;
|
||||
private readonly groupListener = this._register(new MutableDisposable());
|
||||
private fontInfo: BareFontInfo | undefined;
|
||||
private dimension: DOM.Dimension | null = null;
|
||||
private editorFocus: IContextKey<boolean> | null = null;
|
||||
private outputRenderer: OutputRenderer;
|
||||
private findWidget: NotebookFindWidget;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IWebviewService private webviewService: IWebviewService,
|
||||
@INotebookService private notebookService: INotebookService,
|
||||
@IEditorGroupsService editorGroupService: IEditorGroupsService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IEnvironmentService private readonly environmentSerice: IEnvironmentService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super(NotebookEditor.ID, telemetryService, themeService, storageService);
|
||||
|
||||
this.editorMemento = this.getEditorMemento<INotebookEditorViewState>(editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY);
|
||||
this.outputRenderer = new OutputRenderer(this, this.instantiationService);
|
||||
this.findWidget = this.instantiationService.createInstance(NotebookFindWidget, this);
|
||||
this.findWidget.updateTheme(this.themeService.getColorTheme());
|
||||
}
|
||||
|
||||
get viewModel() {
|
||||
return this.notebookViewModel;
|
||||
}
|
||||
|
||||
get minimumWidth(): number { return 375; }
|
||||
get maximumWidth(): number { return Number.POSITIVE_INFINITY; }
|
||||
|
||||
// these setters need to exist because this extends from BaseEditor
|
||||
set minimumWidth(value: number) { /*noop*/ }
|
||||
set maximumWidth(value: number) { /*noop*/ }
|
||||
|
||||
|
||||
//#region Editor Core
|
||||
|
||||
|
||||
public get isNotebookEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected createEditor(parent: HTMLElement): void {
|
||||
this.rootElement = DOM.append(parent, $('.notebook-editor'));
|
||||
this.createBody(this.rootElement);
|
||||
this.generateFontInfo();
|
||||
this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService);
|
||||
this._register(this.onDidFocus(() => {
|
||||
this.editorFocus?.set(true);
|
||||
}));
|
||||
|
||||
this._register(this.onDidBlur(() => {
|
||||
this.editorFocus?.set(false);
|
||||
}));
|
||||
}
|
||||
|
||||
private generateFontInfo(): void {
|
||||
const editorOptions = this.configurationService.getValue<IEditorOptions>('editor');
|
||||
this.fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel());
|
||||
}
|
||||
|
||||
private createBody(parent: HTMLElement): void {
|
||||
this.body = document.createElement('div');
|
||||
DOM.addClass(this.body, 'cell-list-container');
|
||||
this.createCellList();
|
||||
DOM.append(parent, this.body);
|
||||
DOM.append(parent, this.findWidget.getDomNode());
|
||||
}
|
||||
|
||||
private createCellList(): void {
|
||||
DOM.addClass(this.body, 'cell-list-container');
|
||||
|
||||
const renders = [
|
||||
this.instantiationService.createInstance(CodeCellRenderer, this, this.renderedEditors),
|
||||
this.instantiationService.createInstance(MarkdownCellRenderer, this),
|
||||
];
|
||||
|
||||
this.list = <NotebookCellList>this.instantiationService.createInstance(
|
||||
NotebookCellList,
|
||||
'NotebookCellList',
|
||||
this.body,
|
||||
this.instantiationService.createInstance(NotebookCellListDelegate),
|
||||
renders,
|
||||
this.contextKeyService,
|
||||
{
|
||||
setRowLineHeight: false,
|
||||
setRowHeight: false,
|
||||
supportDynamicHeights: true,
|
||||
horizontalScrolling: false,
|
||||
keyboardSupport: false,
|
||||
mouseSupport: true,
|
||||
multipleSelectionSupport: false,
|
||||
enableKeyboardNavigation: true,
|
||||
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,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.control = new NotebookCodeEditors(this.list, this.renderedEditors);
|
||||
this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this, this.environmentSerice);
|
||||
this.list.rowsContainer.appendChild(this.webview.element);
|
||||
this._register(this.list);
|
||||
}
|
||||
|
||||
getControl() {
|
||||
return this.control;
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.editorFocus?.set(false);
|
||||
if (this.webview) {
|
||||
this.localStore.clear();
|
||||
this.list?.rowsContainer.removeChild(this.webview?.element);
|
||||
this.webview?.dispose();
|
||||
this.webview = null;
|
||||
}
|
||||
|
||||
this.list?.splice(0, this.list?.length);
|
||||
|
||||
if (this.notebookViewModel && !this.notebookViewModel.isDirty()) {
|
||||
this.notebookService.destoryNotebookDocument(this.notebookViewModel.viewType!, this.notebookViewModel!.notebookDocument);
|
||||
this.notebookViewModel.dispose();
|
||||
this.notebookViewModel = undefined;
|
||||
}
|
||||
|
||||
super.onHide();
|
||||
}
|
||||
|
||||
setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void {
|
||||
super.setEditorVisible(visible, group);
|
||||
this.groupListener.value = ((group as IEditorGroupView).onWillCloseEditor(e => this.onWillCloseEditorInGroup(e)));
|
||||
}
|
||||
|
||||
private onWillCloseEditorInGroup(e: IEditorCloseEvent): void {
|
||||
const editor = e.editor;
|
||||
if (!(editor instanceof NotebookEditorInput)) {
|
||||
return; // only handle files
|
||||
}
|
||||
|
||||
if (editor === this.input) {
|
||||
this.saveTextEditorViewState(editor);
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
super.focus();
|
||||
this.editorFocus?.set(true);
|
||||
}
|
||||
|
||||
async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
|
||||
if (this.input instanceof NotebookEditorInput) {
|
||||
this.saveTextEditorViewState(this.input);
|
||||
}
|
||||
|
||||
await super.setInput(input, options, token);
|
||||
const model = await input.resolve();
|
||||
|
||||
if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(model) || this.webview === null) {
|
||||
this.detachModel();
|
||||
await this.attachModel(input, model);
|
||||
}
|
||||
|
||||
// reveal cell if editor options tell to do so
|
||||
if (options instanceof NotebookEditorOptions && options.cellOptions) {
|
||||
const cellOptions = options.cellOptions;
|
||||
const cell = this.notebookViewModel!.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString());
|
||||
if (cell) {
|
||||
this.revealInCenterIfOutsideViewport(cell);
|
||||
const editor = this.renderedEditors.get(cell)!;
|
||||
if (editor) {
|
||||
if (cellOptions.options?.selection) {
|
||||
const { selection } = cellOptions.options;
|
||||
editor.setSelection({
|
||||
...selection,
|
||||
endLineNumber: selection.endLineNumber || selection.startLineNumber,
|
||||
endColumn: selection.endColumn || selection.startColumn
|
||||
});
|
||||
}
|
||||
if (!cellOptions.options?.preserveFocus) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearInput(): void {
|
||||
if (this.input && this.input instanceof NotebookEditorInput && !this.input.isDisposed()) {
|
||||
this.saveTextEditorViewState(this.input);
|
||||
}
|
||||
|
||||
super.clearInput();
|
||||
}
|
||||
|
||||
private detachModel() {
|
||||
this.localStore.clear();
|
||||
this.notebookViewModel?.dispose();
|
||||
this.notebookViewModel = undefined;
|
||||
this.webview?.clearInsets();
|
||||
this.webview?.clearPreloadsCache();
|
||||
this.findWidget.clear();
|
||||
}
|
||||
|
||||
private async attachModel(input: NotebookEditorInput, model: NotebookEditorModel) {
|
||||
if (!this.webview) {
|
||||
this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this, this.environmentSerice);
|
||||
this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element);
|
||||
}
|
||||
|
||||
this.notebookViewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model);
|
||||
const viewState = this.loadTextEditorViewState(input);
|
||||
this.notebookViewModel.restoreEditorViewState(viewState);
|
||||
|
||||
this.localStore.add(this.notebookViewModel.onDidChangeViewCells((e) => {
|
||||
if (e.synchronous) {
|
||||
e.splices.reverse().forEach((diff) => {
|
||||
this.list?.splice(diff[0], diff[1], diff[2]);
|
||||
});
|
||||
} else {
|
||||
DOM.scheduleAtNextAnimationFrame(() => {
|
||||
e.splices.reverse().forEach((diff) => {
|
||||
this.list?.splice(diff[0], diff[1], diff[2]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
this.webview?.updateRendererPreloads(this.notebookViewModel.renderers);
|
||||
|
||||
this.localStore.add(this.list!.onWillScroll(e => {
|
||||
this.webview!.updateViewScrollTop(-e.scrollTop, []);
|
||||
}));
|
||||
|
||||
this.localStore.add(this.list!.onDidChangeContentHeight(() => {
|
||||
const scrollTop = this.list?.scrollTop || 0;
|
||||
const scrollHeight = this.list?.scrollHeight || 0;
|
||||
this.webview!.element.style.height = `${scrollHeight}px`;
|
||||
let updateItems: { cell: CellViewModel, output: IOutput, cellTop: number }[] = [];
|
||||
|
||||
if (this.webview?.insetMapping) {
|
||||
this.webview?.insetMapping.forEach((value, key) => {
|
||||
let cell = value.cell;
|
||||
let index = this.notebookViewModel!.getViewCellIndex(cell);
|
||||
let cellTop = this.list?.getAbsoluteTop(index) || 0;
|
||||
if (this.webview!.shouldUpdateInset(cell, key, cellTop)) {
|
||||
updateItems.push({
|
||||
cell: cell,
|
||||
output: key,
|
||||
cellTop: cellTop
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (updateItems.length) {
|
||||
this.webview?.updateViewScrollTop(-scrollTop, updateItems);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.localStore.add(this.list!.onDidChangeFocus((e) => {
|
||||
if (e.elements.length > 0) {
|
||||
this.notebookService.updateNotebookActiveCell(input.viewType!, input.resource!, e.elements[0].handle);
|
||||
}
|
||||
}));
|
||||
|
||||
this.list?.splice(0, this.list?.length || 0);
|
||||
this.list?.splice(0, 0, this.notebookViewModel!.viewCells as CellViewModel[]);
|
||||
this.list?.layout();
|
||||
}
|
||||
|
||||
private saveTextEditorViewState(input: NotebookEditorInput): void {
|
||||
if (this.group && this.notebookViewModel) {
|
||||
const state = this.notebookViewModel.saveEditorViewState();
|
||||
this.editorMemento.saveEditorState(this.group, input.resource, state);
|
||||
}
|
||||
}
|
||||
|
||||
private loadTextEditorViewState(input: NotebookEditorInput): INotebookEditorViewState | undefined {
|
||||
if (this.group) {
|
||||
return this.editorMemento.loadEditorState(this.group, input.resource);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
layout(dimension: DOM.Dimension): void {
|
||||
this.dimension = new DOM.Dimension(dimension.width, dimension.height);
|
||||
DOM.toggleClass(this.rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600);
|
||||
DOM.toggleClass(this.rootElement, 'narrow-width', dimension.width < 600);
|
||||
DOM.size(this.body, dimension.width, dimension.height);
|
||||
this.list?.layout(dimension.height, dimension.width);
|
||||
}
|
||||
|
||||
protected saveState(): void {
|
||||
if (this.input instanceof NotebookEditorInput) {
|
||||
this.saveTextEditorViewState(this.input);
|
||||
}
|
||||
|
||||
super.saveState();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Editor Features
|
||||
|
||||
selectElement(cell: ICellViewModel) {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.setSelection([index]);
|
||||
this.list?.setFocus([index]);
|
||||
}
|
||||
}
|
||||
|
||||
revealInView(cell: ICellViewModel) {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.revealInView(index);
|
||||
}
|
||||
}
|
||||
|
||||
revealInCenterIfOutsideViewport(cell: ICellViewModel) {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.revealInCenterIfOutsideViewport(index);
|
||||
}
|
||||
}
|
||||
|
||||
revealInCenter(cell: ICellViewModel) {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.revealInCenter(index);
|
||||
}
|
||||
}
|
||||
|
||||
revealLineInView(cell: ICellViewModel, line: number): void {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.revealLineInView(index, line);
|
||||
}
|
||||
}
|
||||
|
||||
revealLineInCenter(cell: ICellViewModel, line: number) {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.revealLineInCenter(index, line);
|
||||
}
|
||||
}
|
||||
|
||||
revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number) {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.revealLineInCenterIfOutsideViewport(index, line);
|
||||
}
|
||||
}
|
||||
|
||||
revealRangeInView(cell: ICellViewModel, range: Range): void {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.revealRangeInView(index, range);
|
||||
}
|
||||
}
|
||||
|
||||
revealRangeInCenter(cell: ICellViewModel, range: Range): void {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.revealRangeInCenter(index, range);
|
||||
}
|
||||
}
|
||||
|
||||
revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.revealRangeInCenterIfOutsideViewport(index, range);
|
||||
}
|
||||
}
|
||||
|
||||
setCellSelection(cell: ICellViewModel, range: Range): void {
|
||||
const index = this.notebookViewModel?.getViewCellIndex(cell);
|
||||
|
||||
if (index !== undefined) {
|
||||
this.list?.setCellSelection(index, range);
|
||||
}
|
||||
}
|
||||
|
||||
changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any {
|
||||
return this.notebookViewModel?.changeDecorations(callback);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Find Delegate
|
||||
|
||||
public showFind() {
|
||||
this.findWidget.reveal();
|
||||
}
|
||||
|
||||
public hideFind() {
|
||||
this.findWidget.hide();
|
||||
this.focus();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Cell operations
|
||||
layoutNotebookCell(cell: ICellViewModel, height: number) {
|
||||
let relayout = (cell: ICellViewModel, height: number) => {
|
||||
let index = this.notebookViewModel!.getViewCellIndex(cell);
|
||||
if (index >= 0) {
|
||||
this.list?.updateElementHeight(index, height);
|
||||
}
|
||||
};
|
||||
|
||||
DOM.scheduleAtNextAnimationFrame(() => {
|
||||
relayout(cell, height);
|
||||
});
|
||||
}
|
||||
|
||||
async insertNotebookCell(cell: ICellViewModel, type: CellKind, direction: 'above' | 'below', initialText: string = ''): Promise<void> {
|
||||
const newLanguages = this.notebookViewModel!.languages;
|
||||
const language = newLanguages && newLanguages.length ? newLanguages[0] : 'markdown';
|
||||
const index = this.notebookViewModel!.getViewCellIndex(cell);
|
||||
const insertIndex = direction === 'above' ? index : index + 1;
|
||||
const newModeCell = await this.notebookService.createNotebookCell(this.notebookViewModel!.viewType, this.notebookViewModel!.uri, insertIndex, language, type);
|
||||
newModeCell!.source = initialText.split(/\r?\n/g);
|
||||
const newCell = this.notebookViewModel!.insertCell(insertIndex, newModeCell!, true);
|
||||
this.list?.setFocus([insertIndex]);
|
||||
|
||||
if (type === CellKind.Markdown) {
|
||||
newCell.state = CellState.Editing;
|
||||
}
|
||||
|
||||
DOM.scheduleAtNextAnimationFrame(() => {
|
||||
this.list?.revealInCenterIfOutsideViewport(insertIndex);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteNotebookCell(cell: ICellViewModel): Promise<void> {
|
||||
(cell as CellViewModel).save();
|
||||
const index = this.notebookViewModel!.getViewCellIndex(cell);
|
||||
await this.notebookService.deleteNotebookCell(this.notebookViewModel!.viewType, this.notebookViewModel!.uri, index);
|
||||
this.notebookViewModel!.deleteCell(index, true);
|
||||
}
|
||||
|
||||
moveCellDown(cell: ICellViewModel): void {
|
||||
const index = this.notebookViewModel!.getViewCellIndex(cell);
|
||||
const newIdx = index + 1;
|
||||
this.moveCellToIndex(cell, index, newIdx);
|
||||
}
|
||||
|
||||
moveCellUp(cell: ICellViewModel): void {
|
||||
const index = this.notebookViewModel!.getViewCellIndex(cell);
|
||||
const newIdx = index - 1;
|
||||
this.moveCellToIndex(cell, index, newIdx);
|
||||
}
|
||||
|
||||
private moveCellToIndex(cell: ICellViewModel, index: number, newIdx: number): void {
|
||||
if (!this.notebookViewModel!.moveCellToIdx(index, newIdx, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DOM.scheduleAtNextAnimationFrame(() => {
|
||||
this.list?.revealInCenterIfOutsideViewport(index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
editNotebookCell(cell: CellViewModel): void {
|
||||
cell.state = CellState.Editing;
|
||||
|
||||
this.renderedEditors.get(cell)?.focus();
|
||||
}
|
||||
|
||||
saveNotebookCell(cell: ICellViewModel): void {
|
||||
cell.state = CellState.Preview;
|
||||
}
|
||||
|
||||
getActiveCell() {
|
||||
let elements = this.list?.getFocusedElements();
|
||||
|
||||
if (elements && elements.length) {
|
||||
return elements[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
focusNotebookCell(cell: ICellViewModel, focusEditor: boolean) {
|
||||
const index = this.notebookViewModel!.getViewCellIndex(cell);
|
||||
|
||||
if (focusEditor) {
|
||||
this.list?.setFocus([index]);
|
||||
this.list?.setSelection([index]);
|
||||
this.list?.focusView();
|
||||
|
||||
cell.state = CellState.Editing;
|
||||
cell.focusMode = CellFocusMode.Editor;
|
||||
this.revealInCenterIfOutsideViewport(cell);
|
||||
} else {
|
||||
let itemDOM = this.list?.domElementAtIndex(index);
|
||||
if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) {
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
}
|
||||
|
||||
cell.state = CellState.Preview;
|
||||
cell.focusMode = CellFocusMode.Editor;
|
||||
|
||||
this.list?.setFocus([index]);
|
||||
this.list?.setSelection([index]);
|
||||
this.revealInCenterIfOutsideViewport(cell);
|
||||
this.list?.focusView();
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region MISC
|
||||
|
||||
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!
|
||||
};
|
||||
}
|
||||
getFontInfo(): BareFontInfo | undefined {
|
||||
return this.fontInfo;
|
||||
}
|
||||
|
||||
triggerScroll(event: IMouseWheelEvent) {
|
||||
this.list?.triggerScrollFromMouseWheelEvent(event);
|
||||
}
|
||||
|
||||
createInset(cell: CellViewModel, output: IOutput, shadowContent: string, offset: number) {
|
||||
if (!this.webview) {
|
||||
return;
|
||||
}
|
||||
|
||||
let preloads = this.notebookViewModel!.renderers;
|
||||
|
||||
if (!this.webview!.insetMapping.has(output)) {
|
||||
let index = this.notebookViewModel!.getViewCellIndex(cell);
|
||||
let cellTop = this.list?.getAbsoluteTop(index) || 0;
|
||||
|
||||
this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads);
|
||||
} else {
|
||||
let index = this.notebookViewModel!.getViewCellIndex(cell);
|
||||
let cellTop = this.list?.getAbsoluteTop(index) || 0;
|
||||
let scrollTop = this.list?.scrollTop || 0;
|
||||
|
||||
this.webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]);
|
||||
}
|
||||
}
|
||||
|
||||
removeInset(output: IOutput) {
|
||||
if (!this.webview) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.webview!.removeInset(output);
|
||||
}
|
||||
|
||||
getOutputRenderer(): OutputRenderer {
|
||||
return this.outputRenderer;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground';
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null });
|
||||
if (color) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .monaco-editor-background,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell .margin-view-overlays { background: ${color}; }`);
|
||||
}
|
||||
const link = theme.getColor(textLinkForeground);
|
||||
if (link) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell a { color: ${link}; }`);
|
||||
}
|
||||
const activeLink = theme.getColor(textLinkActiveForeground);
|
||||
if (activeLink) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell a:hover,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .cell a:active { color: ${activeLink}; }`);
|
||||
}
|
||||
const shortcut = theme.getColor(textPreformatForeground);
|
||||
if (shortcut) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor code,
|
||||
.monaco-workbench .part.editor > .content .notebook-editor .shortcut { color: ${shortcut}; }`);
|
||||
}
|
||||
const border = theme.getColor(contrastBorder);
|
||||
if (border) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-editor { border-color: ${border}; }`);
|
||||
}
|
||||
const quoteBackground = theme.getColor(textBlockQuoteBackground);
|
||||
if (quoteBackground) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { background: ${quoteBackground}; }`);
|
||||
}
|
||||
const quoteBorder = theme.getColor(textBlockQuoteBorder);
|
||||
if (quoteBorder) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { border-color: ${quoteBorder}; }`);
|
||||
}
|
||||
|
||||
const inactiveListItem = theme.getColor('list.inactiveSelectionBackground');
|
||||
|
||||
if (inactiveListItem) {
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { background-color: ${inactiveListItem}; }`);
|
||||
}
|
||||
|
||||
// Cell Margin
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell { padding: 8px ${CELL_MARGIN}px 8px ${CELL_MARGIN}px; }`);
|
||||
collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { margin: 8px ${CELL_MARGIN}px; }`);
|
||||
});
|
140
src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts
Normal file
140
src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EditorInput, EditorModel, IEditorInput, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
|
||||
import { ICell, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
|
||||
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
|
||||
|
||||
export class NotebookEditorModel extends EditorModel {
|
||||
private _dirty = false;
|
||||
|
||||
protected readonly _onDidChangeDirty = this._register(new Emitter<void>());
|
||||
readonly onDidChangeDirty = this._onDidChangeDirty.event;
|
||||
|
||||
private readonly _onDidChangeCells = new Emitter<NotebookCellsSplice[]>();
|
||||
get onDidChangeCells(): Event<NotebookCellsSplice[]> { return this._onDidChangeCells.event; }
|
||||
|
||||
|
||||
get notebook() {
|
||||
return this._notebook;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _notebook: NotebookTextModel
|
||||
) {
|
||||
super();
|
||||
|
||||
if (_notebook && _notebook.onDidChangeCells) {
|
||||
this._register(_notebook.onDidChangeContent(() => {
|
||||
this._dirty = true;
|
||||
this._onDidChangeDirty.fire();
|
||||
}));
|
||||
this._register(_notebook.onDidChangeCells((e) => {
|
||||
this._onDidChangeCells.fire(e);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
isDirty() {
|
||||
return this._dirty;
|
||||
}
|
||||
|
||||
getNotebook(): NotebookTextModel {
|
||||
return this._notebook;
|
||||
}
|
||||
|
||||
insertCell(cell: ICell, index: number) {
|
||||
let notebook = this.getNotebook();
|
||||
|
||||
if (notebook) {
|
||||
let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs);
|
||||
this.notebook.insertNewCell(index, mainCell);
|
||||
this._dirty = true;
|
||||
this._onDidChangeDirty.fire();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
deleteCell(index: number) {
|
||||
let notebook = this.getNotebook();
|
||||
|
||||
if (notebook) {
|
||||
this.notebook.removeCell(index);
|
||||
}
|
||||
}
|
||||
|
||||
async save(): Promise<boolean> {
|
||||
if (this._notebook) {
|
||||
this._dirty = false;
|
||||
this._onDidChangeDirty.fire();
|
||||
// todo, flush all states
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class NotebookEditorInput extends EditorInput {
|
||||
static readonly ID: string = 'workbench.input.notebook';
|
||||
private promise: Promise<NotebookEditorModel> | null = null;
|
||||
private textModel: NotebookEditorModel | null = null;
|
||||
|
||||
constructor(
|
||||
public resource: URI,
|
||||
public name: string,
|
||||
public readonly viewType: string | undefined,
|
||||
@INotebookService private readonly notebookService: INotebookService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
getTypeId(): string {
|
||||
return NotebookEditorInput.ID;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
isDirty() {
|
||||
return this.textModel?.isDirty() || false;
|
||||
}
|
||||
|
||||
async save(group: GroupIdentifier, options?: ISaveOptions): Promise<IEditorInput | undefined> {
|
||||
if (this.textModel) {
|
||||
await this.notebookService.save(this.textModel.notebook.viewType, this.textModel.notebook.uri);
|
||||
await this.textModel.save();
|
||||
return this;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async revert(group: GroupIdentifier, options?: IRevertOptions): Promise<void> {
|
||||
if (this.textModel) {
|
||||
// TODO@rebornix we need hashing
|
||||
await this.textModel.save();
|
||||
}
|
||||
}
|
||||
|
||||
async resolve(): Promise<NotebookEditorModel> {
|
||||
if (!this.promise) {
|
||||
await this.notebookService.canResolve(this.viewType!);
|
||||
|
||||
this.promise = this.notebookService.resolveNotebook(this.viewType!, this.resource).then(notebook => {
|
||||
this.textModel = new NotebookEditorModel(notebook!);
|
||||
this.textModel.onDidChangeDirty(() => this._onDidChangeDirty.fire());
|
||||
return this.textModel;
|
||||
});
|
||||
}
|
||||
|
||||
return this.promise;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { BrandedService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
|
||||
export type IOutputTransformCtor = IConstructorSignature1<INotebookEditor, IOutputTransformContribution>;
|
||||
|
||||
export interface IOutputTransformDescription {
|
||||
id: string;
|
||||
kind: CellOutputKind;
|
||||
ctor: IOutputTransformCtor;
|
||||
}
|
||||
|
||||
export namespace NotebookRegistry {
|
||||
export function getOutputTransformContributions(): IOutputTransformDescription[] {
|
||||
return NotebookRegistryImpl.INSTANCE.getNotebookOutputTransform();
|
||||
}
|
||||
}
|
||||
|
||||
export function registerOutputTransform<Services extends BrandedService[]>(id: string, kind: CellOutputKind, ctor: { new(editor: INotebookEditor, ...services: Services): IOutputTransformContribution }): void {
|
||||
NotebookRegistryImpl.INSTANCE.registerOutputTransform(id, kind, ctor);
|
||||
}
|
||||
|
||||
class NotebookRegistryImpl {
|
||||
|
||||
static readonly INSTANCE = new NotebookRegistryImpl();
|
||||
|
||||
private readonly outputTransforms: IOutputTransformDescription[];
|
||||
|
||||
constructor() {
|
||||
this.outputTransforms = [];
|
||||
}
|
||||
|
||||
registerOutputTransform<Services extends BrandedService[]>(id: string, kind: CellOutputKind, ctor: { new(editor: INotebookEditor, ...services: Services): IOutputTransformContribution }): void {
|
||||
this.outputTransforms.push({ id: id, kind: kind, ctor: ctor as IOutputTransformCtor });
|
||||
}
|
||||
|
||||
getNotebookOutputTransform(): IOutputTransformDescription[] {
|
||||
return this.outputTransforms.slice(0);
|
||||
}
|
||||
}
|
337
src/vs/workbench/contrib/notebook/browser/notebookService.ts
Normal file
337
src/vs/workbench/contrib/notebook/browser/notebookService.ts
Normal file
|
@ -0,0 +1,337 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint';
|
||||
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
|
||||
import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { INotebookTextModel, ICell, INotebookMimeTypeSelector, INotebookRendererInfo, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
|
||||
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
|
||||
|
||||
function MODEL_ID(resource: URI): string {
|
||||
return resource.toString();
|
||||
}
|
||||
|
||||
export const INotebookService = createDecorator<INotebookService>('notebookService');
|
||||
|
||||
export interface IMainNotebookController {
|
||||
resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined>;
|
||||
executeNotebook(viewType: string, uri: URI): Promise<void>;
|
||||
updateNotebookActiveCell(uri: URI, cellHandle: number): void;
|
||||
createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise<NotebookCellTextModel | undefined>;
|
||||
deleteCell(uri: URI, index: number): Promise<boolean>
|
||||
executeNotebookActiveCell(uri: URI): void;
|
||||
destoryNotebookDocument(notebook: INotebookTextModel): Promise<void>;
|
||||
save(uri: URI): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface INotebookService {
|
||||
_serviceBrand: undefined;
|
||||
canResolve(viewType: string): Promise<void>;
|
||||
onDidChangeActiveEditor: Event<{ viewType: string, uri: URI }>;
|
||||
registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController): void;
|
||||
unregisterNotebookProvider(viewType: string): void;
|
||||
registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]): void;
|
||||
unregisterNotebookRenderer(handle: number): void;
|
||||
getRendererInfo(handle: number): INotebookRendererInfo | undefined;
|
||||
resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined>;
|
||||
executeNotebook(viewType: string, uri: URI): Promise<void>;
|
||||
executeNotebookActiveCell(viewType: string, uri: URI): Promise<void>;
|
||||
getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[];
|
||||
getNotebookProviderResourceRoots(): URI[];
|
||||
updateNotebookActiveCell(viewType: string, resource: URI, cellHandle: number): void;
|
||||
createNotebookCell(viewType: string, resource: URI, index: number, language: string, type: CellKind): Promise<ICell | undefined>;
|
||||
deleteNotebookCell(viewType: string, resource: URI, index: number): Promise<boolean>;
|
||||
destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void;
|
||||
updateActiveNotebookDocument(viewType: string, resource: URI): void;
|
||||
save(viewType: string, resource: URI): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class NotebookProviderInfoStore {
|
||||
private readonly contributedEditors = new Map<string, NotebookProviderInfo>();
|
||||
|
||||
clear() {
|
||||
this.contributedEditors.clear();
|
||||
}
|
||||
|
||||
get(viewType: string): NotebookProviderInfo | undefined {
|
||||
return this.contributedEditors.get(viewType);
|
||||
}
|
||||
|
||||
add(info: NotebookProviderInfo): void {
|
||||
if (this.contributedEditors.has(info.id)) {
|
||||
console.log(`Custom editor with id '${info.id}' already registered`);
|
||||
return;
|
||||
}
|
||||
this.contributedEditors.set(info.id, info);
|
||||
}
|
||||
|
||||
getContributedNotebook(resource: URI): readonly NotebookProviderInfo[] {
|
||||
return [...Iterable.filter(this.contributedEditors.values(), customEditor => customEditor.matches(resource))];
|
||||
}
|
||||
}
|
||||
|
||||
export class NotebookOutputRendererInfoStore {
|
||||
private readonly contributedRenderers = new Map<string, NotebookOutputRendererInfo>();
|
||||
|
||||
clear() {
|
||||
this.contributedRenderers.clear();
|
||||
}
|
||||
|
||||
get(viewType: string): NotebookOutputRendererInfo | undefined {
|
||||
return this.contributedRenderers.get(viewType);
|
||||
}
|
||||
|
||||
add(info: NotebookOutputRendererInfo): void {
|
||||
if (this.contributedRenderers.has(info.id)) {
|
||||
console.log(`Custom notebook output renderer with id '${info.id}' already registered`);
|
||||
return;
|
||||
}
|
||||
this.contributedRenderers.set(info.id, info);
|
||||
}
|
||||
|
||||
getContributedRenderer(mimeType: string): readonly NotebookOutputRendererInfo[] {
|
||||
return Array.from(this.contributedRenderers.values()).filter(customEditor =>
|
||||
customEditor.matches(mimeType));
|
||||
}
|
||||
}
|
||||
|
||||
class ModelData implements IDisposable {
|
||||
private readonly _modelEventListeners = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
public model: NotebookTextModel,
|
||||
onWillDispose: (model: INotebookTextModel) => void
|
||||
) {
|
||||
this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model)));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._modelEventListeners.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class NotebookService extends Disposable implements INotebookService {
|
||||
_serviceBrand: undefined;
|
||||
private readonly _notebookProviders = new Map<string, { controller: IMainNotebookController, extensionData: NotebookExtensionDescription }>();
|
||||
private readonly _notebookRenderers = new Map<number, { extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[] }>();
|
||||
notebookProviderInfoStore: NotebookProviderInfoStore = new NotebookProviderInfoStore();
|
||||
notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore();
|
||||
private readonly _models: { [modelId: string]: ModelData; };
|
||||
private _onDidChangeActiveEditor = new Emitter<{ viewType: string, uri: URI }>();
|
||||
onDidChangeActiveEditor: Event<{ viewType: string, uri: URI }> = this._onDidChangeActiveEditor.event;
|
||||
private _resolvePool = new Map<string, () => void>();
|
||||
|
||||
constructor(
|
||||
@IExtensionService private readonly extensionService: IExtensionService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._models = {};
|
||||
notebookProviderExtensionPoint.setHandler((extensions) => {
|
||||
this.notebookProviderInfoStore.clear();
|
||||
|
||||
for (const extension of extensions) {
|
||||
for (const notebookContribution of extension.value) {
|
||||
this.notebookProviderInfoStore.add(new NotebookProviderInfo({
|
||||
id: notebookContribution.viewType,
|
||||
displayName: notebookContribution.displayName,
|
||||
selector: notebookContribution.selector || [],
|
||||
}));
|
||||
}
|
||||
}
|
||||
// console.log(this._notebookProviderInfoStore);
|
||||
});
|
||||
|
||||
notebookRendererExtensionPoint.setHandler((renderers) => {
|
||||
this.notebookRenderersInfoStore.clear();
|
||||
|
||||
for (const extension of renderers) {
|
||||
for (const notebookContribution of extension.value) {
|
||||
this.notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({
|
||||
id: notebookContribution.viewType,
|
||||
displayName: notebookContribution.displayName,
|
||||
mimeTypes: notebookContribution.mimeTypes || []
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(this.notebookRenderersInfoStore);
|
||||
});
|
||||
}
|
||||
|
||||
async canResolve(viewType: string): Promise<void> {
|
||||
if (this._notebookProviders.has(viewType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.extensionService.activateByEvent(`onNotebookEditor:${viewType}`);
|
||||
|
||||
let resolve: () => void;
|
||||
const promise = new Promise<void>(r => { resolve = r; });
|
||||
this._resolvePool.set(viewType, resolve!);
|
||||
return promise;
|
||||
}
|
||||
|
||||
registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController) {
|
||||
this._notebookProviders.set(viewType, { extensionData, controller });
|
||||
|
||||
let resolve = this._resolvePool.get(viewType);
|
||||
if (resolve) {
|
||||
resolve();
|
||||
this._resolvePool.delete(viewType);
|
||||
}
|
||||
}
|
||||
|
||||
unregisterNotebookProvider(viewType: string): void {
|
||||
this._notebookProviders.delete(viewType);
|
||||
}
|
||||
|
||||
registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]) {
|
||||
this._notebookRenderers.set(handle, { extensionData, type, selectors, preloads });
|
||||
}
|
||||
|
||||
unregisterNotebookRenderer(handle: number) {
|
||||
this._notebookRenderers.delete(handle);
|
||||
}
|
||||
|
||||
getRendererInfo(handle: number): INotebookRendererInfo | undefined {
|
||||
const renderer = this._notebookRenderers.get(handle);
|
||||
|
||||
if (renderer) {
|
||||
return {
|
||||
id: renderer.extensionData.id,
|
||||
extensionLocation: URI.revive(renderer.extensionData.location),
|
||||
preloads: renderer.preloads
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined> {
|
||||
const provider = this._notebookProviders.get(viewType);
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const notebookModel = await provider.controller.resolveNotebook(viewType, uri);
|
||||
if (!notebookModel) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// new notebook model created
|
||||
const modelId = MODEL_ID(uri);
|
||||
const modelData = new ModelData(
|
||||
notebookModel,
|
||||
(model) => this._onWillDispose(model),
|
||||
);
|
||||
this._models[modelId] = modelData;
|
||||
return modelData.model;
|
||||
}
|
||||
|
||||
updateNotebookActiveCell(viewType: string, resource: URI, cellHandle: number): void {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (provider) {
|
||||
provider.controller.updateNotebookActiveCell(resource, cellHandle);
|
||||
}
|
||||
}
|
||||
|
||||
async createNotebookCell(viewType: string, resource: URI, index: number, language: string, type: CellKind): Promise<NotebookCellTextModel | undefined> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (provider) {
|
||||
return provider.controller.createRawCell(resource, index, language, type);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async deleteNotebookCell(viewType: string, resource: URI, index: number): Promise<boolean> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (provider) {
|
||||
return provider.controller.deleteCell(resource, index);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async executeNotebook(viewType: string, uri: URI): Promise<void> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (provider) {
|
||||
return provider.controller.executeNotebook(viewType, uri);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async executeNotebookActiveCell(viewType: string, uri: URI): Promise<void> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (provider) {
|
||||
await provider.controller.executeNotebookActiveCell(uri);
|
||||
}
|
||||
}
|
||||
|
||||
getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[] {
|
||||
return this.notebookProviderInfoStore.getContributedNotebook(resource);
|
||||
}
|
||||
|
||||
getContributedNotebookOutputRenderers(mimeType: string): readonly NotebookOutputRendererInfo[] {
|
||||
return this.notebookRenderersInfoStore.getContributedRenderer(mimeType);
|
||||
}
|
||||
|
||||
getNotebookProviderResourceRoots(): URI[] {
|
||||
let ret: URI[] = [];
|
||||
this._notebookProviders.forEach(val => {
|
||||
ret.push(URI.revive(val.extensionData.location));
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (provider) {
|
||||
provider.controller.destoryNotebookDocument(notebook);
|
||||
}
|
||||
}
|
||||
|
||||
updateActiveNotebookDocument(viewType: string, resource: URI): void {
|
||||
this._onDidChangeActiveEditor.fire({ viewType, uri: resource });
|
||||
}
|
||||
|
||||
async save(viewType: string, resource: URI): Promise<boolean> {
|
||||
let provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (provider) {
|
||||
return provider.controller.save(resource);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _onWillDispose(model: INotebookTextModel): void {
|
||||
let modelId = MODEL_ID(model.uri);
|
||||
let modelData = this._models[modelId];
|
||||
|
||||
delete this._models[modelId];
|
||||
modelData?.dispose();
|
||||
|
||||
// this._onModelRemoved.fire(model);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,362 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IListRenderer, IListVirtualDelegate, ListError } from 'vs/base/browser/ui/list/list';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ScrollEvent } from 'vs/base/common/scrollable';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
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 { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { CellRevealType, CellRevealPosition, CursorAtBoundary } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
|
||||
|
||||
export class NotebookCellList extends WorkbenchList<CellViewModel> implements IDisposable {
|
||||
get onWillScroll(): Event<ScrollEvent> { return this.view.onWillScroll; }
|
||||
|
||||
get rowsContainer(): HTMLElement {
|
||||
return this.view.containerDomNode;
|
||||
}
|
||||
private _previousSelectedElements: CellViewModel[] = [];
|
||||
private _localDisposableStore = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
private listUser: string,
|
||||
container: HTMLElement,
|
||||
delegate: IListVirtualDelegate<CellViewModel>,
|
||||
renderers: IListRenderer<CellViewModel, any>[],
|
||||
contextKeyService: IContextKeyService,
|
||||
options: IWorkbenchListOptions<CellViewModel>,
|
||||
@IListService listService: IListService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IKeybindingService keybindingService: IKeybindingService
|
||||
|
||||
) {
|
||||
super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService);
|
||||
|
||||
this._previousSelectedElements = this.getSelectedElements();
|
||||
this._localDisposableStore.add(this.onDidChangeSelection((e) => {
|
||||
this._previousSelectedElements.forEach(element => {
|
||||
if (e.elements.indexOf(element) < 0) {
|
||||
element.onDeselect();
|
||||
}
|
||||
});
|
||||
this._previousSelectedElements = e.elements;
|
||||
}));
|
||||
|
||||
const notebookEditorCursorAtBoundaryContext = NOTEBOOK_EDITOR_CURSOR_BOUNDARY.bindTo(contextKeyService);
|
||||
notebookEditorCursorAtBoundaryContext.set('none');
|
||||
|
||||
let cursorSelectionListener: IDisposable | null = null;
|
||||
let textEditorAttachListener: IDisposable | null = null;
|
||||
|
||||
const recomputeContext = (element: CellViewModel) => {
|
||||
switch (element.cursorAtBoundary()) {
|
||||
case CursorAtBoundary.Both:
|
||||
notebookEditorCursorAtBoundaryContext.set('both');
|
||||
break;
|
||||
case CursorAtBoundary.Top:
|
||||
notebookEditorCursorAtBoundaryContext.set('top');
|
||||
break;
|
||||
case CursorAtBoundary.Bottom:
|
||||
notebookEditorCursorAtBoundaryContext.set('bottom');
|
||||
break;
|
||||
default:
|
||||
notebookEditorCursorAtBoundaryContext.set('none');
|
||||
break;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
// Cursor Boundary context
|
||||
this._localDisposableStore.add(this.onDidChangeSelection((e) => {
|
||||
if (e.elements.length) {
|
||||
cursorSelectionListener?.dispose();
|
||||
textEditorAttachListener?.dispose();
|
||||
// we only validate the first focused element
|
||||
const focusedElement = e.elements[0];
|
||||
|
||||
cursorSelectionListener = focusedElement.onDidChangeCursorSelection(() => {
|
||||
recomputeContext(focusedElement);
|
||||
});
|
||||
|
||||
textEditorAttachListener = focusedElement.onDidChangeEditorAttachState(() => {
|
||||
if (focusedElement.editorAttached) {
|
||||
recomputeContext(focusedElement);
|
||||
}
|
||||
});
|
||||
|
||||
recomputeContext(focusedElement);
|
||||
return;
|
||||
}
|
||||
|
||||
// reset context
|
||||
notebookEditorCursorAtBoundaryContext.set('none');
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
domElementAtIndex(index: number): HTMLElement | null {
|
||||
return this.view.domElement(index);
|
||||
}
|
||||
|
||||
focusView() {
|
||||
this.view.domNode.focus();
|
||||
}
|
||||
|
||||
getAbsoluteTop(index: number): number {
|
||||
if (index < 0 || index >= this.length) {
|
||||
throw new ListError(this.listUser, `Invalid index ${index}`);
|
||||
}
|
||||
|
||||
return this.view.elementTop(index);
|
||||
}
|
||||
|
||||
triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
|
||||
this.view.triggerScrollFromMouseWheelEvent(browserEvent);
|
||||
}
|
||||
|
||||
updateElementHeight(index: number, size: number): void {
|
||||
const focused = this.getSelection();
|
||||
this.view.updateElementHeight(index, size, focused.length ? focused[0] : null);
|
||||
// this.view.updateElementHeight(index, size, null);
|
||||
}
|
||||
|
||||
// override
|
||||
domFocus() {
|
||||
if (document.activeElement && this.view.domNode.contains(document.activeElement)) {
|
||||
// for example, when focus goes into monaco editor, if we refocus the list view, the editor will lose focus.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMacintosh && document.activeElement && isContextMenuFocused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.domFocus();
|
||||
}
|
||||
|
||||
private _revealRange(index: number, range: Range, revealType: CellRevealType, newlyCreated: boolean, alignToBottom: boolean) {
|
||||
const element = this.view.element(index);
|
||||
const scrollTop = this.view.getScrollTop();
|
||||
const wrapperBottom = scrollTop + this.view.renderHeight;
|
||||
const startLineNumber = range.startLineNumber;
|
||||
const lineOffset = element.getLineScrollTopOffset(startLineNumber);
|
||||
const elementTop = this.view.elementTop(index);
|
||||
const lineTop = elementTop + lineOffset;
|
||||
|
||||
// TODO@rebornix 30 ---> line height * 1.5
|
||||
if (lineTop < scrollTop) {
|
||||
this.view.setScrollTop(lineTop - 30);
|
||||
} else if (lineTop > wrapperBottom) {
|
||||
this.view.setScrollTop(scrollTop + lineTop - wrapperBottom + 30);
|
||||
} else if (newlyCreated) {
|
||||
// newly scrolled into view
|
||||
if (alignToBottom) {
|
||||
// align to the bottom
|
||||
this.view.setScrollTop(scrollTop + lineTop - wrapperBottom + 30);
|
||||
} else {
|
||||
// align to to top
|
||||
this.view.setScrollTop(lineTop - 30);
|
||||
}
|
||||
}
|
||||
|
||||
if (revealType === CellRevealType.Range) {
|
||||
element.revealRangeInCenter(range);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO@rebornix TEST & Fix potential bugs
|
||||
// List items have real dynamic heights, which means after we set `scrollTop` based on the `elementTop(index)`, the element at `index` might still be removed from the view once all relayouting tasks are done.
|
||||
// For example, we scroll item 10 into the view upwards, in the first round, items 7, 8, 9, 10 are all in the viewport. Then item 7 and 8 resize themselves to be larger and finally item 10 is removed from the view.
|
||||
// To ensure that item 10 is always there, we need to scroll item 10 to the top edge of the viewport.
|
||||
private _revealRangeInternal(index: number, range: Range, revealType: CellRevealType) {
|
||||
const scrollTop = this.view.getScrollTop();
|
||||
const wrapperBottom = scrollTop + this.view.renderHeight;
|
||||
const elementTop = this.view.elementTop(index);
|
||||
const element = this.view.element(index);
|
||||
|
||||
if (element.editorAttached) {
|
||||
this._revealRange(index, range, revealType, false, false);
|
||||
} else {
|
||||
const elementHeight = this.view.elementHeight(index);
|
||||
let upwards = false;
|
||||
|
||||
if (elementTop + elementHeight < scrollTop) {
|
||||
// scroll downwards
|
||||
this.view.setScrollTop(elementTop);
|
||||
upwards = false;
|
||||
} else if (elementTop > wrapperBottom) {
|
||||
// scroll upwards
|
||||
this.view.setScrollTop(elementTop - this.view.renderHeight / 2);
|
||||
upwards = true;
|
||||
}
|
||||
|
||||
const editorAttachedPromise = new Promise((resolve, reject) => {
|
||||
element.onDidChangeEditorAttachState(state => state ? resolve() : reject());
|
||||
});
|
||||
|
||||
editorAttachedPromise.then(() => {
|
||||
this._revealRange(index, range, revealType, true, upwards);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
revealLineInView(index: number, line: number) {
|
||||
this._revealRangeInternal(index, new Range(line, 1, line, 1), CellRevealType.Line);
|
||||
}
|
||||
|
||||
revealRangeInView(index: number, range: Range): void {
|
||||
this._revealRangeInternal(index, range, CellRevealType.Range);
|
||||
}
|
||||
|
||||
private _revealRangeInCenterInternal(index: number, range: Range, revealType: CellRevealType) {
|
||||
const reveal = (index: number, range: Range, revealType: CellRevealType) => {
|
||||
const element = this.view.element(index);
|
||||
let lineOffset = element.getLineScrollTopOffset(range.startLineNumber);
|
||||
let lineOffsetInView = this.view.elementTop(index) + lineOffset;
|
||||
this.view.setScrollTop(lineOffsetInView - this.view.renderHeight / 2);
|
||||
|
||||
if (revealType === CellRevealType.Range) {
|
||||
element.revealRangeInCenter(range);
|
||||
}
|
||||
};
|
||||
|
||||
const elementTop = this.view.elementTop(index);
|
||||
const viewItemOffset = elementTop;
|
||||
this.view.setScrollTop(viewItemOffset - this.view.renderHeight / 2);
|
||||
const element = this.view.element(index);
|
||||
|
||||
if (!element.editorAttached) {
|
||||
getEditorAttachedPromise(element).then(() => reveal(index, range, revealType));
|
||||
} else {
|
||||
reveal(index, range, revealType);
|
||||
}
|
||||
}
|
||||
|
||||
revealLineInCenter(index: number, line: number) {
|
||||
this._revealRangeInCenterInternal(index, new Range(line, 1, line, 1), CellRevealType.Line);
|
||||
}
|
||||
|
||||
revealRangeInCenter(index: number, range: Range): void {
|
||||
this._revealRangeInCenterInternal(index, range, CellRevealType.Range);
|
||||
}
|
||||
|
||||
private _revealRangeInCenterIfOutsideViewportInternal(index: number, range: Range, revealType: CellRevealType) {
|
||||
const reveal = (index: number, range: Range, revealType: CellRevealType) => {
|
||||
const element = this.view.element(index);
|
||||
let lineOffset = element.getLineScrollTopOffset(range.startLineNumber);
|
||||
let lineOffsetInView = this.view.elementTop(index) + lineOffset;
|
||||
this.view.setScrollTop(lineOffsetInView - this.view.renderHeight / 2);
|
||||
|
||||
if (revealType === CellRevealType.Range) {
|
||||
setTimeout(() => {
|
||||
element.revealRangeInCenter(range);
|
||||
}, 240);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollTop = this.view.getScrollTop();
|
||||
const wrapperBottom = scrollTop + this.view.renderHeight;
|
||||
const elementTop = this.view.elementTop(index);
|
||||
const viewItemOffset = elementTop;
|
||||
const element = this.view.element(index);
|
||||
|
||||
if (viewItemOffset < scrollTop || viewItemOffset > wrapperBottom) {
|
||||
// let it render
|
||||
this.view.setScrollTop(viewItemOffset - this.view.renderHeight / 2);
|
||||
|
||||
// after rendering, it might be pushed down due to markdown cell dynamic height
|
||||
const elementTop = this.view.elementTop(index);
|
||||
this.view.setScrollTop(elementTop - this.view.renderHeight / 2);
|
||||
|
||||
// reveal editor
|
||||
if (!element.editorAttached) {
|
||||
getEditorAttachedPromise(element).then(() => reveal(index, range, revealType));
|
||||
} else {
|
||||
// for example markdown
|
||||
}
|
||||
} else {
|
||||
if (element.editorAttached) {
|
||||
element.revealRangeInCenter(range);
|
||||
} else {
|
||||
// for example, markdown cell in preview mode
|
||||
getEditorAttachedPromise(element).then(() => reveal(index, range, revealType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
revealLineInCenterIfOutsideViewport(index: number, line: number) {
|
||||
this._revealRangeInCenterIfOutsideViewportInternal(index, new Range(line, 1, line, 1), CellRevealType.Line);
|
||||
}
|
||||
|
||||
revealRangeInCenterIfOutsideViewport(index: number, range: Range): void {
|
||||
this._revealRangeInCenterIfOutsideViewportInternal(index, range, CellRevealType.Range);
|
||||
}
|
||||
|
||||
private _revealInternal(index: number, ignoreIfInsideViewport: boolean, revealPosition: CellRevealPosition) {
|
||||
const scrollTop = this.view.getScrollTop();
|
||||
const wrapperBottom = scrollTop + this.view.renderHeight;
|
||||
const elementTop = this.view.elementTop(index);
|
||||
|
||||
if (ignoreIfInsideViewport && elementTop >= scrollTop && elementTop < wrapperBottom) {
|
||||
// inside the viewport
|
||||
return;
|
||||
}
|
||||
|
||||
// first render
|
||||
const viewItemOffset = revealPosition === CellRevealPosition.Top ? elementTop : (elementTop - this.view.renderHeight / 2);
|
||||
this.view.setScrollTop(viewItemOffset);
|
||||
|
||||
// second scroll as markdown cell is dynamic
|
||||
const newElementTop = this.view.elementTop(index);
|
||||
const newViewItemOffset = revealPosition === CellRevealPosition.Top ? newElementTop : (newElementTop - this.view.renderHeight / 2);
|
||||
this.view.setScrollTop(newViewItemOffset);
|
||||
}
|
||||
|
||||
revealInView(index: number) {
|
||||
this._revealInternal(index, true, CellRevealPosition.Top);
|
||||
}
|
||||
|
||||
revealInCenter(index: number) {
|
||||
this._revealInternal(index, false, CellRevealPosition.Center);
|
||||
}
|
||||
|
||||
revealInCenterIfOutsideViewport(index: number) {
|
||||
this._revealInternal(index, true, CellRevealPosition.Center);
|
||||
}
|
||||
|
||||
setCellSelection(index: number, range: Range) {
|
||||
const element = this.view.element(index);
|
||||
if (element.editorAttached) {
|
||||
element.setSelection(range);
|
||||
} else {
|
||||
getEditorAttachedPromise(element).then(() => { element.setSelection(range); });
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._localDisposableStore.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function getEditorAttachedPromise(element: CellViewModel) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Event.once(element.onDidChangeEditorAttachState)(state => state ? resolve() : reject());
|
||||
});
|
||||
}
|
||||
|
||||
function isContextMenuFocused() {
|
||||
return !!DOM.findParentWithClass(<HTMLElement>document.activeElement, 'context-view');
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IOutput, IRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
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';
|
||||
|
||||
export class OutputRenderer {
|
||||
protected readonly _contributions: { [key: string]: IOutputTransformContribution; };
|
||||
protected readonly _mimeTypeMapping: { [key: number]: IOutputTransformContribution; };
|
||||
|
||||
constructor(
|
||||
notebookEditor: INotebookEditor,
|
||||
private readonly instantiationService: IInstantiationService
|
||||
) {
|
||||
this._contributions = {};
|
||||
this._mimeTypeMapping = {};
|
||||
|
||||
let contributions = NotebookRegistry.getOutputTransformContributions();
|
||||
|
||||
for (const desc of contributions) {
|
||||
try {
|
||||
const contribution = this.instantiationService.createInstance(desc.ctor, notebookEditor);
|
||||
this._contributions[desc.id] = contribution;
|
||||
this._mimeTypeMapping[desc.kind] = contribution;
|
||||
} catch (err) {
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderNoop(output: IOutput, container: HTMLElement): IRenderOutput {
|
||||
const contentNode = document.createElement('p');
|
||||
|
||||
contentNode.innerText = `No renderer could be found for output. It has the following output type: ${output.outputKind}`;
|
||||
container.appendChild(contentNode);
|
||||
return {
|
||||
hasDynamicHeight: false
|
||||
};
|
||||
}
|
||||
|
||||
render(output: IOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput {
|
||||
let transform = this._mimeTypeMapping[output.outputKind];
|
||||
|
||||
if (transform) {
|
||||
return transform.render(output, container, preferredMimeType);
|
||||
} else {
|
||||
return this.renderNoop(output, container);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,386 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { registerOutputTransform } from 'vs/workbench/contrib/notebook/browser/notebookRegistry';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { RGBA, Color } from 'vs/base/common/color';
|
||||
import { ansiColorIdentifiers } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
|
||||
class ErrorTransform implements IOutputTransformContribution {
|
||||
constructor(
|
||||
public editor: INotebookEditor,
|
||||
@IThemeService private readonly themeService: IThemeService
|
||||
) {
|
||||
}
|
||||
|
||||
render(output: any, container: HTMLElement): IRenderOutput {
|
||||
const traceback = document.createElement('pre');
|
||||
DOM.addClasses(traceback, 'traceback');
|
||||
if (output.traceback) {
|
||||
for (let j = 0; j < output.traceback.length; j++) {
|
||||
traceback.appendChild(handleANSIOutput(output.traceback[j], this.themeService));
|
||||
}
|
||||
}
|
||||
container.appendChild(traceback);
|
||||
return {
|
||||
hasDynamicHeight: false
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
}
|
||||
}
|
||||
|
||||
registerOutputTransform('notebook.output.error', CellOutputKind.Error, ErrorTransform);
|
||||
|
||||
/**
|
||||
* @param text The content to stylize.
|
||||
* @returns An {@link HTMLSpanElement} that contains the potentially stylized text.
|
||||
*/
|
||||
export function handleANSIOutput(text: string, themeService: IThemeService): HTMLSpanElement {
|
||||
|
||||
const root: HTMLSpanElement = document.createElement('span');
|
||||
const textLength: number = text.length;
|
||||
|
||||
let styleNames: string[] = [];
|
||||
let customFgColor: RGBA | undefined;
|
||||
let customBgColor: RGBA | undefined;
|
||||
let currentPos: number = 0;
|
||||
let buffer: string = '';
|
||||
|
||||
while (currentPos < textLength) {
|
||||
|
||||
let sequenceFound: boolean = false;
|
||||
|
||||
// Potentially an ANSI escape sequence.
|
||||
// See http://ascii-table.com/ansi-escape-sequences.php & https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
if (text.charCodeAt(currentPos) === 27 && text.charAt(currentPos + 1) === '[') {
|
||||
|
||||
const startPos: number = currentPos;
|
||||
currentPos += 2; // Ignore 'Esc[' as it's in every sequence.
|
||||
|
||||
let ansiSequence: string = '';
|
||||
|
||||
while (currentPos < textLength) {
|
||||
const char: string = text.charAt(currentPos);
|
||||
ansiSequence += char;
|
||||
|
||||
currentPos++;
|
||||
|
||||
// Look for a known sequence terminating character.
|
||||
if (char.match(/^[ABCDHIJKfhmpsu]$/)) {
|
||||
sequenceFound = true;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (sequenceFound) {
|
||||
|
||||
// Flush buffer with previous styles.
|
||||
appendStylizedStringToContainer(root, buffer, styleNames, customFgColor, customBgColor);
|
||||
|
||||
buffer = '';
|
||||
|
||||
/*
|
||||
* Certain ranges that are matched here do not contain real graphics rendition sequences. For
|
||||
* the sake of having a simpler expression, they have been included anyway.
|
||||
*/
|
||||
if (ansiSequence.match(/^(?:[34][0-8]|9[0-7]|10[0-7]|[013]|4|[34]9)(?:;[349][0-7]|10[0-7]|[013]|[245]|[34]9)?(?:;[012]?[0-9]?[0-9])*;?m$/)) {
|
||||
|
||||
const styleCodes: number[] = ansiSequence.slice(0, -1) // Remove final 'm' character.
|
||||
.split(';') // Separate style codes.
|
||||
.filter(elem => elem !== '') // Filter empty elems as '34;m' -> ['34', ''].
|
||||
.map(elem => parseInt(elem, 10)); // Convert to numbers.
|
||||
|
||||
if (styleCodes[0] === 38 || styleCodes[0] === 48) {
|
||||
// Advanced color code - can't be combined with formatting codes like simple colors can
|
||||
// Ignores invalid colors and additional info beyond what is necessary
|
||||
const colorType = (styleCodes[0] === 38) ? 'foreground' : 'background';
|
||||
|
||||
if (styleCodes[1] === 5) {
|
||||
set8BitColor(styleCodes, colorType);
|
||||
} else if (styleCodes[1] === 2) {
|
||||
set24BitColor(styleCodes, colorType);
|
||||
}
|
||||
} else {
|
||||
setBasicFormatters(styleCodes);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Unsupported sequence so simply hide it.
|
||||
}
|
||||
|
||||
} else {
|
||||
currentPos = startPos;
|
||||
}
|
||||
}
|
||||
|
||||
if (sequenceFound === false) {
|
||||
buffer += text.charAt(currentPos);
|
||||
currentPos++;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining text buffer if not empty.
|
||||
if (buffer) {
|
||||
appendStylizedStringToContainer(root, buffer, styleNames, customFgColor, customBgColor);
|
||||
}
|
||||
|
||||
return root;
|
||||
|
||||
/**
|
||||
* Change the foreground or background color by clearing the current color
|
||||
* and adding the new one.
|
||||
* @param colorType If `'foreground'`, will change the foreground color, if
|
||||
* `'background'`, will change the background color.
|
||||
* @param color Color to change to. If `undefined` or not provided,
|
||||
* will clear current color without adding a new one.
|
||||
*/
|
||||
function changeColor(colorType: 'foreground' | 'background', color?: RGBA | undefined): void {
|
||||
if (colorType === 'foreground') {
|
||||
customFgColor = color;
|
||||
} else if (colorType === 'background') {
|
||||
customBgColor = color;
|
||||
}
|
||||
styleNames = styleNames.filter(style => style !== `code-${colorType}-colored`);
|
||||
if (color !== undefined) {
|
||||
styleNames.push(`code-${colorType}-colored`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and set basic ANSI formatting. Supports bold, italic, underline,
|
||||
* normal foreground and background colors, and bright foreground and
|
||||
* background colors. Not to be used for codes containing advanced colors.
|
||||
* Will ignore invalid codes.
|
||||
* @param styleCodes Array of ANSI basic styling numbers, which will be
|
||||
* applied in order. New colors and backgrounds clear old ones; new formatting
|
||||
* does not.
|
||||
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code }
|
||||
*/
|
||||
function setBasicFormatters(styleCodes: number[]): void {
|
||||
for (let code of styleCodes) {
|
||||
switch (code) {
|
||||
case 0: {
|
||||
styleNames = [];
|
||||
customFgColor = undefined;
|
||||
customBgColor = undefined;
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
styleNames.push('code-bold');
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
styleNames.push('code-italic');
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
styleNames.push('code-underline');
|
||||
break;
|
||||
}
|
||||
case 39: {
|
||||
changeColor('foreground', undefined);
|
||||
break;
|
||||
}
|
||||
case 49: {
|
||||
changeColor('background', undefined);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
setBasicColor(code);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and set styling for complicated 24-bit ANSI color codes.
|
||||
* @param styleCodes Full list of integer codes that make up the full ANSI
|
||||
* sequence, including the two defining codes and the three RGB codes.
|
||||
* @param colorType If `'foreground'`, will set foreground color, if
|
||||
* `'background'`, will set background color.
|
||||
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit }
|
||||
*/
|
||||
function set24BitColor(styleCodes: number[], colorType: 'foreground' | 'background'): void {
|
||||
if (styleCodes.length >= 5 &&
|
||||
styleCodes[2] >= 0 && styleCodes[2] <= 255 &&
|
||||
styleCodes[3] >= 0 && styleCodes[3] <= 255 &&
|
||||
styleCodes[4] >= 0 && styleCodes[4] <= 255) {
|
||||
const customColor = new RGBA(styleCodes[2], styleCodes[3], styleCodes[4]);
|
||||
changeColor(colorType, customColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and set styling for advanced 8-bit ANSI color codes.
|
||||
* @param styleCodes Full list of integer codes that make up the ANSI
|
||||
* sequence, including the two defining codes and the one color code.
|
||||
* @param colorType If `'foreground'`, will set foreground color, if
|
||||
* `'background'`, will set background color.
|
||||
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit }
|
||||
*/
|
||||
function set8BitColor(styleCodes: number[], colorType: 'foreground' | 'background'): void {
|
||||
let colorNumber = styleCodes[2];
|
||||
const color = calcANSI8bitColor(colorNumber);
|
||||
|
||||
if (color) {
|
||||
changeColor(colorType, color);
|
||||
} else if (colorNumber >= 0 && colorNumber <= 15) {
|
||||
// Need to map to one of the four basic color ranges (30-37, 90-97, 40-47, 100-107)
|
||||
colorNumber += 30;
|
||||
if (colorNumber >= 38) {
|
||||
// Bright colors
|
||||
colorNumber += 52;
|
||||
}
|
||||
if (colorType === 'background') {
|
||||
colorNumber += 10;
|
||||
}
|
||||
setBasicColor(colorNumber);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and set styling for basic bright and dark ANSI color codes. Uses
|
||||
* theme colors if available. Automatically distinguishes between foreground
|
||||
* and background colors; does not support color-clearing codes 39 and 49.
|
||||
* @param styleCode Integer color code on one of the following ranges:
|
||||
* [30-37, 90-97, 40-47, 100-107]. If not on one of these ranges, will do
|
||||
* nothing.
|
||||
*/
|
||||
function setBasicColor(styleCode: number): void {
|
||||
const theme = themeService.getColorTheme();
|
||||
let colorType: 'foreground' | 'background' | undefined;
|
||||
let colorIndex: number | undefined;
|
||||
|
||||
if (styleCode >= 30 && styleCode <= 37) {
|
||||
colorIndex = styleCode - 30;
|
||||
colorType = 'foreground';
|
||||
} else if (styleCode >= 90 && styleCode <= 97) {
|
||||
colorIndex = (styleCode - 90) + 8; // High-intensity (bright)
|
||||
colorType = 'foreground';
|
||||
} else if (styleCode >= 40 && styleCode <= 47) {
|
||||
colorIndex = styleCode - 40;
|
||||
colorType = 'background';
|
||||
} else if (styleCode >= 100 && styleCode <= 107) {
|
||||
colorIndex = (styleCode - 100) + 8; // High-intensity (bright)
|
||||
colorType = 'background';
|
||||
}
|
||||
|
||||
if (colorIndex !== undefined && colorType) {
|
||||
const colorName = ansiColorIdentifiers[colorIndex];
|
||||
const color = theme.getColor(colorName);
|
||||
if (color) {
|
||||
changeColor(colorType, color.rgba);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param root The {@link HTMLElement} to append the content to.
|
||||
* @param stringContent The text content to be appended.
|
||||
* @param cssClasses The list of CSS styles to apply to the text content.
|
||||
* @param linkDetector The {@link LinkDetector} responsible for generating links from {@param stringContent}.
|
||||
* @param customTextColor If provided, will apply custom color with inline style.
|
||||
* @param customBackgroundColor If provided, will apply custom color with inline style.
|
||||
*/
|
||||
export function appendStylizedStringToContainer(
|
||||
root: HTMLElement,
|
||||
stringContent: string,
|
||||
cssClasses: string[],
|
||||
customTextColor?: RGBA,
|
||||
customBackgroundColor?: RGBA
|
||||
): void {
|
||||
if (!root || !stringContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = linkify(stringContent, true);
|
||||
container.className = cssClasses.join(' ');
|
||||
if (customTextColor) {
|
||||
container.style.color =
|
||||
Color.Format.CSS.formatRGB(new Color(customTextColor));
|
||||
}
|
||||
if (customBackgroundColor) {
|
||||
container.style.backgroundColor =
|
||||
Color.Format.CSS.formatRGB(new Color(customBackgroundColor));
|
||||
}
|
||||
|
||||
root.appendChild(container);
|
||||
}
|
||||
|
||||
function linkify(text: string, splitLines?: boolean): HTMLElement {
|
||||
if (splitLines) {
|
||||
const lines = text.split('\n');
|
||||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
lines[i] = lines[i] + '\n';
|
||||
}
|
||||
if (!lines[lines.length - 1]) {
|
||||
// Remove the last element ('') that split added.
|
||||
lines.pop();
|
||||
}
|
||||
const elements = lines.map(line => linkify(line));
|
||||
if (elements.length === 1) {
|
||||
// Do not wrap single line with extra span.
|
||||
return elements[0];
|
||||
}
|
||||
const container = document.createElement('span');
|
||||
elements.forEach(e => container.appendChild(e));
|
||||
return container;
|
||||
}
|
||||
|
||||
const container = document.createElement('span');
|
||||
container.appendChild(document.createTextNode(text));
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the color from the color set defined in the ANSI 8-bit standard.
|
||||
* Standard and high intensity colors are not defined in the standard as specific
|
||||
* colors, so these and invalid colors return `undefined`.
|
||||
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit } for info.
|
||||
* @param colorNumber The number (ranging from 16 to 255) referring to the color
|
||||
* desired.
|
||||
*/
|
||||
export function calcANSI8bitColor(colorNumber: number): RGBA | undefined {
|
||||
if (colorNumber % 1 !== 0) {
|
||||
// Should be integer
|
||||
// {{SQL CARBON EDIT}} @todo anthonydresser 4/12/19 this is necessary because we don't use strict null checks
|
||||
return undefined;
|
||||
} if (colorNumber >= 16 && colorNumber <= 231) {
|
||||
// Converts to one of 216 RGB colors
|
||||
colorNumber -= 16;
|
||||
|
||||
let blue: number = colorNumber % 6;
|
||||
colorNumber = (colorNumber - blue) / 6;
|
||||
let green: number = colorNumber % 6;
|
||||
colorNumber = (colorNumber - green) / 6;
|
||||
let red: number = colorNumber;
|
||||
|
||||
// red, green, blue now range on [0, 5], need to map to [0,255]
|
||||
const convFactor: number = 255 / 5;
|
||||
blue = Math.round(blue * convFactor);
|
||||
green = Math.round(green * convFactor);
|
||||
red = Math.round(red * convFactor);
|
||||
|
||||
return new RGBA(red, green, blue);
|
||||
} else if (colorNumber >= 232 && colorNumber <= 255) {
|
||||
// Converts to a grayscale value
|
||||
colorNumber -= 232;
|
||||
const colorLevel: number = Math.round(colorNumber / 23 * 255);
|
||||
return new RGBA(colorLevel, colorLevel, colorLevel);
|
||||
} else {
|
||||
// {{SQL CARBON EDIT}} @todo anthonydresser 4/12/19 this is necessary because we don't use strict null checks
|
||||
return undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { registerOutputTransform } from 'vs/workbench/contrib/notebook/browser/notebookRegistry';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { isArray } from 'vs/base/common/types';
|
||||
import * as marked from 'vs/base/common/marked/marked';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
class RichRenderer implements IOutputTransformContribution {
|
||||
private _mdRenderer: marked.Renderer = new marked.Renderer({ gfm: true });;
|
||||
private _richMimeTypeRenderers = new Map<string, (output: any, container: HTMLElement) => IRenderOutput>();
|
||||
|
||||
constructor(
|
||||
public notebookEditor: INotebookEditor,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IModeService private readonly modeService: IModeService
|
||||
) {
|
||||
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));
|
||||
this._richMimeTypeRenderers.set('image/svg+xml', this.renderSVG.bind(this));
|
||||
this._richMimeTypeRenderers.set('text/markdown', this.renderMarkdown.bind(this));
|
||||
this._richMimeTypeRenderers.set('image/png', this.renderPNG.bind(this));
|
||||
this._richMimeTypeRenderers.set('image/jpeg', this.renderJavaScript.bind(this));
|
||||
this._richMimeTypeRenderers.set('text/plain', this.renderPlainText.bind(this));
|
||||
this._richMimeTypeRenderers.set('text/x-javascript', this.renderCode.bind(this));
|
||||
}
|
||||
|
||||
render(output: any, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput {
|
||||
if (!output.data) {
|
||||
const contentNode = document.createElement('p');
|
||||
contentNode.innerText = `No data could be found for output.`;
|
||||
container.appendChild(contentNode);
|
||||
|
||||
return {
|
||||
hasDynamicHeight: false
|
||||
};
|
||||
}
|
||||
|
||||
if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) {
|
||||
const contentNode = document.createElement('p');
|
||||
let mimeTypes = [];
|
||||
for (const property in output.data) {
|
||||
mimeTypes.push(property);
|
||||
}
|
||||
|
||||
let mimeTypesMessage = mimeTypes.join(', ');
|
||||
|
||||
contentNode.innerText = `No renderer could be found for output. It has the following MIME types: ${mimeTypesMessage}`;
|
||||
container.appendChild(contentNode);
|
||||
|
||||
return {
|
||||
hasDynamicHeight: false
|
||||
};
|
||||
}
|
||||
|
||||
let renderer = this._richMimeTypeRenderers.get(preferredMimeType);
|
||||
return renderer!(output, container);
|
||||
}
|
||||
|
||||
renderJSON(output: any, container: HTMLElement) {
|
||||
let data = output.data['application/json'];
|
||||
let str = JSON.stringify(data, null, '\t');
|
||||
|
||||
const editor = this.instantiationService.createInstance(CodeEditorWidget, container, {
|
||||
...getOutputSimpleEditorOptions(),
|
||||
dimension: {
|
||||
width: 0,
|
||||
height: 0
|
||||
}
|
||||
}, {
|
||||
isSimpleWidget: true
|
||||
});
|
||||
|
||||
let mode = this.modeService.create('json');
|
||||
let resource = URI.parse(`notebook-output-${Date.now()}.json`);
|
||||
const textModel = this.modelService.createModel(str, mode, resource, false);
|
||||
editor.setModel(textModel);
|
||||
|
||||
let width = this.notebookEditor.getLayoutInfo().width;
|
||||
let fontInfo = this.notebookEditor.getLayoutInfo().fontInfo;
|
||||
let height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18);
|
||||
|
||||
editor.layout({
|
||||
height,
|
||||
width
|
||||
});
|
||||
|
||||
container.style.height = `${height + 16}px`;
|
||||
|
||||
return {
|
||||
hasDynamicHeight: true
|
||||
};
|
||||
}
|
||||
|
||||
renderCode(output: any, container: HTMLElement) {
|
||||
let data = output.data['text/x-javascript'];
|
||||
let str = isArray(data) ? data.join('') : data;
|
||||
|
||||
const editor = this.instantiationService.createInstance(CodeEditorWidget, container, {
|
||||
...getOutputSimpleEditorOptions(),
|
||||
dimension: {
|
||||
width: 0,
|
||||
height: 0
|
||||
}
|
||||
}, {
|
||||
isSimpleWidget: true
|
||||
});
|
||||
|
||||
let mode = this.modeService.create('javascript');
|
||||
let resource = URI.parse(`notebook-output-${Date.now()}.js`);
|
||||
const textModel = this.modelService.createModel(str, mode, resource, false);
|
||||
editor.setModel(textModel);
|
||||
|
||||
let width = this.notebookEditor.getLayoutInfo().width;
|
||||
let fontInfo = this.notebookEditor.getLayoutInfo().fontInfo;
|
||||
let height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18);
|
||||
|
||||
editor.layout({
|
||||
height,
|
||||
width
|
||||
});
|
||||
|
||||
container.style.height = `${height + 16}px`;
|
||||
|
||||
return {
|
||||
hasDynamicHeight: true
|
||||
};
|
||||
}
|
||||
|
||||
renderJavaScript(output: any, container: HTMLElement) {
|
||||
let data = output.data['application/javascript'];
|
||||
let str = isArray(data) ? data.join('') : data;
|
||||
let scriptVal = `<script type="application/javascript">${str}</script>`;
|
||||
return {
|
||||
shadowContent: scriptVal,
|
||||
hasDynamicHeight: false
|
||||
};
|
||||
}
|
||||
|
||||
renderHTML(output: any, container: HTMLElement) {
|
||||
let data = output.data['text/html'];
|
||||
let str = isArray(data) ? data.join('') : data;
|
||||
return {
|
||||
shadowContent: str,
|
||||
hasDynamicHeight: false
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
renderSVG(output: any, container: HTMLElement) {
|
||||
let data = output.data['image/svg+xml'];
|
||||
let str = isArray(data) ? data.join('') : data;
|
||||
return {
|
||||
shadowContent: str,
|
||||
hasDynamicHeight: false
|
||||
};
|
||||
}
|
||||
|
||||
renderMarkdown(output: any, container: HTMLElement) {
|
||||
let data = output.data['text/markdown'];
|
||||
const str = isArray(data) ? data.join('') : data;
|
||||
const mdOutput = document.createElement('div');
|
||||
mdOutput.innerHTML = marked(str, { renderer: this._mdRenderer });
|
||||
container.appendChild(mdOutput);
|
||||
|
||||
return {
|
||||
hasDynamicHeight: true
|
||||
};
|
||||
}
|
||||
|
||||
renderPNG(output: any, container: HTMLElement) {
|
||||
const image = document.createElement('img');
|
||||
image.src = `data:image/png;base64,${output.data['image/png']}`;
|
||||
const display = document.createElement('div');
|
||||
DOM.addClasses(display, 'display');
|
||||
display.appendChild(image);
|
||||
container.appendChild(display);
|
||||
return {
|
||||
hasDynamicHeight: true
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
renderJPEG(output: any, container: HTMLElement) {
|
||||
const image = document.createElement('img');
|
||||
image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`;
|
||||
const display = document.createElement('div');
|
||||
DOM.addClasses(display, 'display');
|
||||
display.appendChild(image);
|
||||
container.appendChild(display);
|
||||
return {
|
||||
hasDynamicHeight: true
|
||||
};
|
||||
}
|
||||
|
||||
renderPlainText(output: any, container: HTMLElement) {
|
||||
let data = output.data['text/plain'];
|
||||
let str = isArray(data) ? data.join('') : data;
|
||||
const contentNode = document.createElement('p');
|
||||
contentNode.innerText = str;
|
||||
container.appendChild(contentNode);
|
||||
|
||||
return {
|
||||
hasDynamicHeight: false
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
}
|
||||
}
|
||||
|
||||
registerOutputTransform('notebook.output.rich', CellOutputKind.Rich, RichRenderer);
|
||||
|
||||
|
||||
export function getOutputSimpleEditorOptions(): IEditorOptions {
|
||||
return {
|
||||
readOnly: true,
|
||||
wordWrap: 'on',
|
||||
overviewRulerLanes: 0,
|
||||
glyphMargin: false,
|
||||
selectOnLineNumbers: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
selectionHighlight: false,
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerBorder: false,
|
||||
scrollBeyondLastLine: false,
|
||||
renderLineHighlight: 'none',
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
lineNumbers: 'off',
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { registerOutputTransform } from 'vs/workbench/contrib/notebook/browser/notebookRegistry';
|
||||
import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
|
||||
class StreamRenderer implements IOutputTransformContribution {
|
||||
constructor(
|
||||
editor: INotebookEditor
|
||||
) {
|
||||
}
|
||||
|
||||
render(output: any, container: HTMLElement): IRenderOutput {
|
||||
const contentNode = document.createElement('p');
|
||||
contentNode.innerText = output.text;
|
||||
container.appendChild(contentNode);
|
||||
return {
|
||||
hasDynamicHeight: false
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
}
|
||||
}
|
||||
|
||||
registerOutputTransform('notebook.output.stream', CellOutputKind.Text, StreamRenderer);
|
|
@ -0,0 +1,417 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as UUID from 'vs/base/common/uuid';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
|
||||
import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader';
|
||||
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
|
||||
import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants';
|
||||
|
||||
export interface IDimentionMessage {
|
||||
type: 'dimension';
|
||||
id: string;
|
||||
data: DOM.Dimension;
|
||||
}
|
||||
|
||||
|
||||
export interface IScrollAckMessage {
|
||||
type: 'scroll-ack';
|
||||
data: { top: number };
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface IClearMessage {
|
||||
type: 'clear';
|
||||
}
|
||||
|
||||
export interface ICreationRequestMessage {
|
||||
type: 'html';
|
||||
content: string;
|
||||
id: string;
|
||||
outputId: string;
|
||||
top: number;
|
||||
}
|
||||
|
||||
export interface IContentWidgetTopRequest {
|
||||
id: string;
|
||||
top: number;
|
||||
}
|
||||
|
||||
export interface IViewScrollTopRequestMessage {
|
||||
type: 'view-scroll';
|
||||
top?: number;
|
||||
widgets: IContentWidgetTopRequest[];
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface IScrollRequestMessage {
|
||||
type: 'scroll';
|
||||
id: string;
|
||||
top: number;
|
||||
widgetTop?: number;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface IUpdatePreloadResourceMessage {
|
||||
type: 'preload';
|
||||
resources: string[];
|
||||
}
|
||||
|
||||
type IMessage = IDimentionMessage | IScrollAckMessage;
|
||||
|
||||
let version = 0;
|
||||
export class BackLayerWebView extends Disposable {
|
||||
element: HTMLElement;
|
||||
webview: WebviewElement;
|
||||
insetMapping: Map<IOutput, { outputId: string, cell: CellViewModel, cacheOffset: number | undefined }> = new Map();
|
||||
reversedInsetMapping: Map<string, IOutput> = new Map();
|
||||
preloadsCache: Map<string, boolean> = new Map();
|
||||
localResourceRootsCache: URI[] | undefined = undefined;
|
||||
rendererRootsCache: URI[] = [];
|
||||
|
||||
constructor(public webviewService: IWebviewService, public notebookService: INotebookService, public notebookEditor: INotebookEditor, public environmentSerice: IEnvironmentService) {
|
||||
super();
|
||||
this.element = document.createElement('div');
|
||||
|
||||
this.element.style.width = `calc(100% - ${CELL_MARGIN * 2}px)`;
|
||||
this.element.style.height = '1400px';
|
||||
this.element.style.position = 'absolute';
|
||||
this.element.style.margin = '0px 0 0px 24px';
|
||||
|
||||
const loader = URI.file(path.join(environmentSerice.appRoot, '/out/vs/loader.js')).with({ scheme: WebviewResourceScheme });
|
||||
|
||||
let content = /* html */`
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
#container > div > div {
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
margin: 8px 0;
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
body {
|
||||
padding: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="overflow: hidden;">
|
||||
<script>
|
||||
self.require = {};
|
||||
</script>
|
||||
<script src="${loader}"></script>
|
||||
<div id="__vscode_preloads"></div>
|
||||
<div id='container' class="widgetarea" style="position: absolute;width:100%;top: 0px"></div>
|
||||
<script>
|
||||
(function () {
|
||||
// eslint-disable-next-line no-undef
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
const preservedScriptAttributes = {
|
||||
type: true,
|
||||
src: true,
|
||||
nonce: true,
|
||||
noModule: true,
|
||||
async: true
|
||||
};
|
||||
|
||||
// derived from https://github.com/jquery/jquery/blob/d0ce00cdfa680f1f0c38460bc51ea14079ae8b07/src/core/DOMEval.js
|
||||
const domEval = (container) => {
|
||||
var arr = Array.from(container.getElementsByTagName('script'));
|
||||
for (let n = 0; n < arr.length; n++) {
|
||||
let node = arr[n];
|
||||
let scriptTag = document.createElement('script');
|
||||
scriptTag.text = node.innerText;
|
||||
for (let key in preservedScriptAttributes ) {
|
||||
const val = node[key] || node.getAttribute && node.getAttribute(key);
|
||||
if (val) {
|
||||
scriptTag.setAttribute(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: should script with src not be removed?
|
||||
container.appendChild(scriptTag).parentNode.removeChild(scriptTag);
|
||||
}
|
||||
};
|
||||
|
||||
let observers = [];
|
||||
|
||||
const resizeObserve = (container, id) => {
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
for (let entry of entries) {
|
||||
if (entry.target.id === id && entry.contentRect) {
|
||||
vscode.postMessage({
|
||||
type: 'dimension',
|
||||
id: id,
|
||||
data: {
|
||||
height: entry.contentRect.height
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(container);
|
||||
observers.push(resizeObserver);
|
||||
}
|
||||
|
||||
window.addEventListener('message', event => {
|
||||
let id = event.data.id;
|
||||
|
||||
switch (event.data.type) {
|
||||
case 'html':
|
||||
{
|
||||
let cellOutputContainer = document.getElementById(id);
|
||||
let outputId = event.data.outputId;
|
||||
if (!cellOutputContainer) {
|
||||
let newElement = document.createElement('div');
|
||||
|
||||
newElement.id = id;
|
||||
document.getElementById('container').appendChild(newElement);
|
||||
cellOutputContainer = newElement;
|
||||
}
|
||||
|
||||
let outputNode = document.createElement('div');
|
||||
outputNode.style.position = 'absolute';
|
||||
outputNode.style.top = event.data.top + 'px';
|
||||
|
||||
outputNode.id = outputId;
|
||||
let content = event.data.content;
|
||||
outputNode.innerHTML = content;
|
||||
cellOutputContainer.appendChild(outputNode);
|
||||
|
||||
// eval
|
||||
domEval(outputNode);
|
||||
resizeObserve(outputNode, outputId);
|
||||
|
||||
vscode.postMessage({
|
||||
type: 'dimension',
|
||||
id: outputId,
|
||||
data: {
|
||||
height: outputNode.clientHeight
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'view-scroll':
|
||||
{
|
||||
// const date = new Date();
|
||||
// console.log('----- will scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
|
||||
|
||||
for (let i = 0; i < event.data.widgets.length; i++) {
|
||||
let widget = document.getElementById(event.data.widgets[i].id);
|
||||
widget.style.top = event.data.widgets[i].top + 'px';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'clear':
|
||||
document.getElementById('container').innerHTML = '';
|
||||
for (let i = 0; i < observers.length; i++) {
|
||||
observers[i].disconnect();
|
||||
}
|
||||
|
||||
observers = [];
|
||||
break;
|
||||
case 'clearOutput':
|
||||
let output = document.getElementById(id);
|
||||
output.parentNode.removeChild(output);
|
||||
// @TODO remove observer
|
||||
break;
|
||||
case 'preload':
|
||||
let resources = event.data.resources;
|
||||
let preloadsContainer = document.getElementById('__vscode_preloads');
|
||||
for (let i = 0; i < resources.length; i++) {
|
||||
let scriptTag = document.createElement('script');
|
||||
scriptTag.setAttribute('src', resources[i]);
|
||||
preloadsContainer.appendChild(scriptTag)
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}());
|
||||
|
||||
</script>
|
||||
</body>
|
||||
`;
|
||||
|
||||
this.webview = this._createInset(webviewService, content);
|
||||
this.webview.mountTo(this.element);
|
||||
|
||||
this._register(this.webview.onDidWheel(e => {
|
||||
this.notebookEditor.triggerScroll(e);
|
||||
}));
|
||||
|
||||
this._register(this.webview.onMessage((data: IMessage) => {
|
||||
if (data.type === 'dimension') {
|
||||
let output = this.reversedInsetMapping.get(data.id);
|
||||
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cell = this.insetMapping.get(output)!.cell;
|
||||
let height = data.data.height;
|
||||
let outputHeight = height === 0 ? 0 : height + 16;
|
||||
|
||||
if (cell) {
|
||||
let outputIndex = cell.outputs.indexOf(output);
|
||||
cell.updateOutputHeight(outputIndex, outputHeight);
|
||||
this.notebookEditor.layoutNotebookCell(cell, cell.getCellTotalHeight());
|
||||
}
|
||||
} else if (data.type === 'scroll-ack') {
|
||||
// const date = new Date();
|
||||
// const top = data.data.top;
|
||||
// console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _createInset(webviewService: IWebviewService, content: string) {
|
||||
this.localResourceRootsCache = [...this.notebookService.getNotebookProviderResourceRoots(), URI.file(this.environmentSerice.appRoot)];
|
||||
const webview = webviewService.createWebviewElement('' + UUID.generateUuid(), {
|
||||
enableFindWidget: false,
|
||||
}, {
|
||||
allowScripts: true,
|
||||
localResourceRoots: this.localResourceRootsCache
|
||||
});
|
||||
webview.html = content;
|
||||
return webview;
|
||||
}
|
||||
|
||||
shouldUpdateInset(cell: CellViewModel, output: IOutput, cellTop: number) {
|
||||
let outputCache = this.insetMapping.get(output)!;
|
||||
let outputIndex = cell.outputs.indexOf(output);
|
||||
|
||||
let outputOffsetInOutputContainer = cell.getOutputOffset(outputIndex);
|
||||
let outputOffset = cellTop + cell.editorHeight + 16 /* editor padding */ + 8 + outputOffsetInOutputContainer;
|
||||
|
||||
if (outputOffset === outputCache.cacheOffset) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
updateViewScrollTop(top: number, items: { cell: CellViewModel, output: IOutput, cellTop: number }[]) {
|
||||
let widgets: IContentWidgetTopRequest[] = items.map(item => {
|
||||
let outputCache = this.insetMapping.get(item.output)!;
|
||||
let id = outputCache.outputId;
|
||||
let outputIndex = item.cell.outputs.indexOf(item.output);
|
||||
|
||||
let outputOffsetInOutputContainer = item.cell.getOutputOffset(outputIndex);
|
||||
let outputOffset = item.cellTop + item.cell.editorHeight + 16 /* editor padding */ + 8 + outputOffsetInOutputContainer;
|
||||
outputCache.cacheOffset = outputOffset;
|
||||
|
||||
// console.log('trigger output offset change', outputOffset);
|
||||
|
||||
return {
|
||||
id: id,
|
||||
top: outputOffset
|
||||
};
|
||||
});
|
||||
|
||||
let message: IViewScrollTopRequestMessage = {
|
||||
top,
|
||||
type: 'view-scroll',
|
||||
version: version++,
|
||||
widgets: widgets
|
||||
};
|
||||
|
||||
this.webview.sendMessage(message);
|
||||
}
|
||||
|
||||
createInset(cell: CellViewModel, output: IOutput, cellTop: number, offset: number, shadowContent: string, preloads: Set<number>) {
|
||||
this.updateRendererPreloads(preloads);
|
||||
let initialTop = cellTop + offset;
|
||||
let outputId = UUID.generateUuid();
|
||||
|
||||
let message: ICreationRequestMessage = {
|
||||
type: 'html',
|
||||
content: shadowContent,
|
||||
id: cell.id,
|
||||
outputId: outputId,
|
||||
top: initialTop
|
||||
};
|
||||
|
||||
this.webview.sendMessage(message);
|
||||
this.insetMapping.set(output, { outputId: outputId, cell: cell, cacheOffset: initialTop });
|
||||
this.reversedInsetMapping.set(outputId, output);
|
||||
}
|
||||
|
||||
removeInset(output: IOutput) {
|
||||
let outputCache = this.insetMapping.get(output);
|
||||
if (!outputCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = outputCache.outputId;
|
||||
|
||||
this.webview.sendMessage({
|
||||
type: 'clearOutput',
|
||||
id: id
|
||||
});
|
||||
this.insetMapping.delete(output);
|
||||
this.reversedInsetMapping.delete(id);
|
||||
}
|
||||
|
||||
clearInsets() {
|
||||
this.webview.sendMessage({
|
||||
type: 'clear'
|
||||
});
|
||||
|
||||
this.insetMapping = new Map();
|
||||
this.reversedInsetMapping = new Map();
|
||||
}
|
||||
|
||||
updateRendererPreloads(preloads: Set<number>) {
|
||||
let resources: string[] = [];
|
||||
let extensionLocations: URI[] = [];
|
||||
preloads.forEach(preload => {
|
||||
let rendererInfo = this.notebookService.getRendererInfo(preload);
|
||||
|
||||
if (rendererInfo) {
|
||||
let preloadResources = rendererInfo.preloads.map(preloadResource => preloadResource.with({ scheme: WebviewResourceScheme }));
|
||||
extensionLocations.push(rendererInfo.extensionLocation);
|
||||
preloadResources.forEach(e => {
|
||||
if (!this.preloadsCache.has(e.toString())) {
|
||||
resources.push(e.toString());
|
||||
this.preloadsCache.set(e.toString(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.rendererRootsCache = extensionLocations;
|
||||
const mixedResourceRoots = [...(this.localResourceRootsCache || []), ...this.rendererRootsCache];
|
||||
|
||||
this.webview.contentOptions = {
|
||||
allowScripts: true,
|
||||
enableCommandUris: true,
|
||||
localResourceRoots: mixedResourceRoots
|
||||
};
|
||||
|
||||
let message: IUpdatePreloadResourceMessage = {
|
||||
type: 'preload',
|
||||
resources: resources
|
||||
};
|
||||
|
||||
this.webview.sendMessage(message);
|
||||
}
|
||||
|
||||
clearPreloadsCache() {
|
||||
this.preloadsCache.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getZoomLevel } from 'vs/base/browser/browser';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
import 'vs/css!vs/workbench/contrib/notebook/browser/notebook';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { InsertCodeCellAboveAction, INotebookCellActionContext, InsertCodeCellBelowAction, InsertMarkdownCellAboveAction, InsertMarkdownCellBelowAction, EditCellAction, SaveCellAction, DeleteCellAction, MoveCellUpAction, MoveCellDownAction } from 'vs/workbench/contrib/notebook/browser/contrib/notebookActions';
|
||||
import { CellRenderTemplate, INotebookEditor, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell';
|
||||
import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell';
|
||||
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { CellViewModel } from '../../viewModel/notebookCellViewModel';
|
||||
import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { MenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
|
||||
|
||||
export class NotebookCellListDelegate implements IListVirtualDelegate<ICellViewModel> {
|
||||
private _lineHeight: number;
|
||||
private _toolbarHeight = EDITOR_TOOLBAR_HEIGHT;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
const editorOptions = this.configurationService.getValue<IEditorOptions>('editor');
|
||||
this._lineHeight = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight;
|
||||
}
|
||||
|
||||
getHeight(element: CellViewModel): number {
|
||||
return element.getHeight(this._lineHeight) + this._toolbarHeight;
|
||||
}
|
||||
|
||||
hasDynamicHeight(element: CellViewModel): boolean {
|
||||
return element.hasDynamicHeight();
|
||||
}
|
||||
|
||||
getTemplateId(element: CellViewModel): string {
|
||||
if (element.cellKind === CellKind.Markdown) {
|
||||
return MarkdownCellRenderer.TEMPLATE_ID;
|
||||
} else {
|
||||
return CodeCellRenderer.TEMPLATE_ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AbstractCellRenderer {
|
||||
protected editorOptions: IEditorOptions;
|
||||
|
||||
constructor(
|
||||
protected readonly instantiationService: IInstantiationService,
|
||||
protected readonly notebookEditor: INotebookEditor,
|
||||
protected readonly contextMenuService: IContextMenuService,
|
||||
private readonly configurationService: IConfigurationService,
|
||||
private readonly keybindingService: IKeybindingService,
|
||||
private readonly notificationService: INotificationService,
|
||||
language: string,
|
||||
) {
|
||||
const editorOptions = deepClone(this.configurationService.getValue<IEditorOptions>('editor', { overrideIdentifier: language }));
|
||||
this.editorOptions = {
|
||||
...editorOptions,
|
||||
padding: {
|
||||
top: EDITOR_TOP_PADDING,
|
||||
bottom: EDITOR_BOTTOM_PADDING
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
scrollbar: {
|
||||
verticalScrollbarSize: 14,
|
||||
horizontal: 'auto',
|
||||
useShadows: true,
|
||||
verticalHasArrows: false,
|
||||
horizontalHasArrows: false,
|
||||
alwaysConsumeMouseWheel: false
|
||||
},
|
||||
overviewRulerLanes: 3,
|
||||
fixedOverflowWidgets: false,
|
||||
lineNumbersMinChars: 1,
|
||||
minimap: { enabled: false },
|
||||
};
|
||||
}
|
||||
|
||||
protected createToolbar(container: HTMLElement): ToolBar {
|
||||
const toolbar = new ToolBar(container, this.contextMenuService, {
|
||||
actionViewItemProvider: action => {
|
||||
if (action instanceof MenuItemAction) {
|
||||
const item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
|
||||
return item;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
showContextMenu(listIndex: number | undefined, element: CellViewModel, x: number, y: number) {
|
||||
const actions: IAction[] = [
|
||||
this.instantiationService.createInstance(InsertCodeCellAboveAction),
|
||||
this.instantiationService.createInstance(InsertCodeCellBelowAction),
|
||||
this.instantiationService.createInstance(InsertMarkdownCellAboveAction),
|
||||
this.instantiationService.createInstance(InsertMarkdownCellBelowAction),
|
||||
];
|
||||
actions.push(...this.getAdditionalContextMenuActions());
|
||||
actions.push(...[
|
||||
this.instantiationService.createInstance(DeleteCellAction)
|
||||
]);
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => {
|
||||
return {
|
||||
x,
|
||||
y
|
||||
};
|
||||
},
|
||||
getActions: () => actions,
|
||||
getActionsContext: () => <INotebookCellActionContext>{
|
||||
cell: element,
|
||||
notebookEditor: this.notebookEditor
|
||||
},
|
||||
autoSelectFirstItem: false
|
||||
});
|
||||
}
|
||||
|
||||
abstract getAdditionalContextMenuActions(): IAction[];
|
||||
}
|
||||
|
||||
export class MarkdownCellRenderer extends AbstractCellRenderer implements IListRenderer<ICellViewModel, CellRenderTemplate> {
|
||||
static readonly TEMPLATE_ID = 'markdown_cell';
|
||||
private disposables: Map<ICellViewModel, DisposableStore> = new Map();
|
||||
|
||||
constructor(
|
||||
notehookEditor: INotebookEditor,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
) {
|
||||
super(instantiationService, notehookEditor, contextMenuService, configurationService, keybindingService, notificationService, 'markdown');
|
||||
}
|
||||
|
||||
get templateId() {
|
||||
return MarkdownCellRenderer.TEMPLATE_ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): CellRenderTemplate {
|
||||
const codeInnerContent = document.createElement('div');
|
||||
DOM.addClasses(codeInnerContent, 'cell', 'code');
|
||||
codeInnerContent.style.display = 'none';
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
const toolbar = this.createToolbar(container);
|
||||
toolbar.setActions([
|
||||
this.instantiationService.createInstance(MoveCellUpAction),
|
||||
this.instantiationService.createInstance(MoveCellDownAction),
|
||||
this.instantiationService.createInstance(InsertCodeCellBelowAction),
|
||||
this.instantiationService.createInstance(EditCellAction),
|
||||
this.instantiationService.createInstance(SaveCellAction),
|
||||
this.instantiationService.createInstance(DeleteCellAction)
|
||||
])();
|
||||
disposables.add(toolbar);
|
||||
|
||||
container.appendChild(codeInnerContent);
|
||||
|
||||
const innerContent = document.createElement('div');
|
||||
DOM.addClasses(innerContent, 'cell', 'markdown');
|
||||
container.appendChild(innerContent);
|
||||
|
||||
const action = document.createElement('div');
|
||||
DOM.addClasses(action, 'menu', 'codicon-settings-gear', 'codicon');
|
||||
container.appendChild(action);
|
||||
|
||||
return {
|
||||
container: container,
|
||||
cellContainer: innerContent,
|
||||
menuContainer: action,
|
||||
editingContainer: codeInnerContent,
|
||||
disposables,
|
||||
toolbar
|
||||
};
|
||||
}
|
||||
|
||||
renderElement(element: CellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void {
|
||||
templateData.editingContainer!.style.display = 'none';
|
||||
templateData.cellContainer.innerHTML = '';
|
||||
let renderedHTML = element.getHTML();
|
||||
if (renderedHTML) {
|
||||
templateData.cellContainer.appendChild(renderedHTML);
|
||||
}
|
||||
|
||||
if (height) {
|
||||
this.disposables.get(element)?.clear();
|
||||
if (!this.disposables.has(element)) {
|
||||
this.disposables.set(element, new DisposableStore());
|
||||
}
|
||||
let elementDisposable = this.disposables.get(element);
|
||||
|
||||
elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, 'mousedown', e => {
|
||||
const { top, height } = DOM.getDomNodePagePosition(templateData.menuContainer!);
|
||||
e.preventDefault();
|
||||
|
||||
const listIndexAttr = templateData.menuContainer?.parentElement?.getAttribute('data-index');
|
||||
const listIndex = listIndexAttr ? Number(listIndexAttr) : undefined;
|
||||
this.showContextMenu(listIndex, element, e.posx, top + height);
|
||||
}));
|
||||
|
||||
elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_LEAVE, e => {
|
||||
templateData.menuContainer?.classList.remove('mouseover');
|
||||
}));
|
||||
|
||||
elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_ENTER, e => {
|
||||
templateData.menuContainer?.classList.add('mouseover');
|
||||
}));
|
||||
|
||||
elementDisposable!.add(new StatefullMarkdownCell(this.notebookEditor, element, templateData, this.editorOptions, this.instantiationService));
|
||||
}
|
||||
|
||||
templateData.toolbar!.context = <INotebookCellActionContext>{
|
||||
cell: element,
|
||||
notebookEditor: this.notebookEditor
|
||||
};
|
||||
}
|
||||
|
||||
getAdditionalContextMenuActions(): IAction[] {
|
||||
return [
|
||||
this.instantiationService.createInstance(EditCellAction),
|
||||
this.instantiationService.createInstance(SaveCellAction),
|
||||
];
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: CellRenderTemplate): void {
|
||||
// throw nerendererw Error('Method not implemented.');
|
||||
|
||||
}
|
||||
|
||||
disposeElement(element: ICellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void {
|
||||
if (height) {
|
||||
this.disposables.get(element)?.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer<ICellViewModel, CellRenderTemplate> {
|
||||
static readonly TEMPLATE_ID = 'code_cell';
|
||||
private disposables: Map<ICellViewModel, DisposableStore> = new Map();
|
||||
|
||||
constructor(
|
||||
protected notebookEditor: INotebookEditor,
|
||||
private renderedEditors: Map<ICellViewModel, ICodeEditor | undefined>,
|
||||
@IContextMenuService contextMenuService: IContextMenuService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
) {
|
||||
super(instantiationService, notebookEditor, contextMenuService, configurationService, keybindingService, notificationService, 'python');
|
||||
}
|
||||
|
||||
get templateId() {
|
||||
return CodeCellRenderer.TEMPLATE_ID;
|
||||
}
|
||||
|
||||
renderTemplate(container: HTMLElement): CellRenderTemplate {
|
||||
const disposables = new DisposableStore();
|
||||
const toolbarContainer = document.createElement('div');
|
||||
container.appendChild(toolbarContainer);
|
||||
DOM.addClasses(toolbarContainer, 'menu', 'codicon-settings-gear', 'codicon');
|
||||
const toolbar = this.createToolbar(container);
|
||||
toolbar.setActions([
|
||||
this.instantiationService.createInstance(MoveCellUpAction),
|
||||
this.instantiationService.createInstance(MoveCellDownAction),
|
||||
this.instantiationService.createInstance(InsertCodeCellBelowAction),
|
||||
this.instantiationService.createInstance(DeleteCellAction)
|
||||
])();
|
||||
disposables.add(toolbar);
|
||||
|
||||
const cellContainer = document.createElement('div');
|
||||
DOM.addClasses(cellContainer, 'cell', 'code');
|
||||
container.appendChild(cellContainer);
|
||||
const editor = this.instantiationService.createInstance(CodeEditorWidget, cellContainer, {
|
||||
...this.editorOptions,
|
||||
dimension: {
|
||||
width: 0,
|
||||
height: 0
|
||||
}
|
||||
}, {});
|
||||
const menuContainer = document.createElement('div');
|
||||
DOM.addClasses(menuContainer, 'menu', 'codicon-settings-gear', 'codicon');
|
||||
container.appendChild(menuContainer);
|
||||
|
||||
const outputContainer = document.createElement('div');
|
||||
DOM.addClasses(outputContainer, 'output');
|
||||
container.appendChild(outputContainer);
|
||||
|
||||
return {
|
||||
container,
|
||||
cellContainer,
|
||||
menuContainer,
|
||||
toolbar,
|
||||
outputContainer,
|
||||
editor,
|
||||
disposables
|
||||
};
|
||||
}
|
||||
|
||||
renderElement(element: CellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void {
|
||||
if (height === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (templateData.outputContainer) {
|
||||
templateData.outputContainer!.innerHTML = '';
|
||||
}
|
||||
|
||||
this.disposables.get(element)?.clear();
|
||||
if (!this.disposables.has(element)) {
|
||||
this.disposables.set(element, new DisposableStore());
|
||||
}
|
||||
|
||||
const elementDisposable = this.disposables.get(element);
|
||||
|
||||
elementDisposable?.add(DOM.addStandardDisposableListener(templateData.menuContainer!, 'mousedown', e => {
|
||||
let { top, height } = DOM.getDomNodePagePosition(templateData.menuContainer!);
|
||||
e.preventDefault();
|
||||
|
||||
const listIndexAttr = templateData.menuContainer?.parentElement?.getAttribute('data-index');
|
||||
const listIndex = listIndexAttr ? Number(listIndexAttr) : undefined;
|
||||
|
||||
this.showContextMenu(listIndex, element, e.posx, top + height);
|
||||
}));
|
||||
|
||||
elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_LEAVE, e => {
|
||||
templateData.menuContainer?.classList.remove('mouseover');
|
||||
}));
|
||||
|
||||
elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_ENTER, e => {
|
||||
templateData.menuContainer?.classList.add('mouseover');
|
||||
}));
|
||||
|
||||
elementDisposable?.add(this.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData));
|
||||
this.renderedEditors.set(element, templateData.editor);
|
||||
|
||||
templateData.toolbar!.context = <INotebookCellActionContext>{
|
||||
cell: element,
|
||||
notebookEditor: this.notebookEditor
|
||||
};
|
||||
}
|
||||
|
||||
getAdditionalContextMenuActions(): IAction[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: CellRenderTemplate): void {
|
||||
templateData.disposables.clear();
|
||||
}
|
||||
|
||||
disposeElement(element: ICellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void {
|
||||
this.disposables.get(element)?.clear();
|
||||
this.renderedEditors.delete(element);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,389 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. 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 { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver';
|
||||
import { IOutput, ITransformedDisplayOutputDto, IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { CellRenderTemplate, INotebookEditor, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { raceCancellation } from 'vs/base/common/async';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
|
||||
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
|
||||
import { CELL_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
|
||||
|
||||
interface IMimeTypeRenderer extends IQuickPickItem {
|
||||
index: number;
|
||||
}
|
||||
|
||||
export class CodeCell extends Disposable {
|
||||
private outputResizeListeners = new Map<IOutput, DisposableStore>();
|
||||
private outputElements = new Map<IOutput, HTMLElement>();
|
||||
constructor(
|
||||
private notebookEditor: INotebookEditor,
|
||||
private viewCell: CellViewModel,
|
||||
private templateData: CellRenderTemplate,
|
||||
@INotebookService private notebookService: INotebookService,
|
||||
@IQuickInputService private readonly quickInputService: IQuickInputService
|
||||
) {
|
||||
super();
|
||||
|
||||
let width: number;
|
||||
const listDimension = notebookEditor.getLayoutInfo();
|
||||
width = listDimension.width - CELL_MARGIN * 2;
|
||||
// if (listDimension) {
|
||||
// } else {
|
||||
// width = templateData.container.clientWidth - 24 /** for scrollbar and margin right */;
|
||||
// }
|
||||
|
||||
const lineNum = viewCell.lineCount;
|
||||
const lineHeight = notebookEditor.getLayoutInfo().fontInfo.lineHeight;
|
||||
const totalHeight = lineNum * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING;
|
||||
templateData.editor?.layout(
|
||||
{
|
||||
width: width,
|
||||
height: totalHeight
|
||||
}
|
||||
);
|
||||
viewCell.editorHeight = totalHeight;
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
this._register({ dispose() { cts.dispose(true); } });
|
||||
raceCancellation(viewCell.resolveTextModel(), cts.token).then(model => {
|
||||
if (model && templateData.editor) {
|
||||
templateData.editor.setModel(model);
|
||||
viewCell.attachTextEditor(templateData.editor);
|
||||
if (notebookEditor.getActiveCell() === viewCell && viewCell.focusMode === CellFocusMode.Editor) {
|
||||
templateData.editor?.focus();
|
||||
}
|
||||
|
||||
let realContentHeight = templateData.editor?.getContentHeight();
|
||||
let width: number;
|
||||
const listDimension = notebookEditor.getLayoutInfo();
|
||||
width = listDimension.width - CELL_MARGIN * 2;
|
||||
// if (listDimension) {
|
||||
// } else {
|
||||
// width = templateData.container.clientWidth - 24 /** for scrollbar and margin right */;
|
||||
// }
|
||||
|
||||
if (realContentHeight !== undefined && realContentHeight !== totalHeight) {
|
||||
templateData.editor?.layout(
|
||||
{
|
||||
width: width,
|
||||
height: realContentHeight
|
||||
}
|
||||
);
|
||||
|
||||
viewCell.editorHeight = realContentHeight;
|
||||
}
|
||||
|
||||
if (this.notebookEditor.getActiveCell() === this.viewCell && viewCell.focusMode === CellFocusMode.Editor) {
|
||||
templateData.editor?.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._register(viewCell.onDidChangeFocusMode(() => {
|
||||
if (viewCell.focusMode === CellFocusMode.Editor) {
|
||||
templateData.editor?.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
let cellWidthResizeObserver = getResizesObserver(templateData.cellContainer, {
|
||||
width: width,
|
||||
height: totalHeight
|
||||
}, () => {
|
||||
let newWidth = cellWidthResizeObserver.getWidth();
|
||||
let realContentHeight = templateData.editor!.getContentHeight();
|
||||
templateData.editor?.layout(
|
||||
{
|
||||
width: newWidth,
|
||||
height: realContentHeight
|
||||
}
|
||||
);
|
||||
|
||||
viewCell.editorHeight = realContentHeight;
|
||||
});
|
||||
|
||||
cellWidthResizeObserver.startObserving();
|
||||
this._register(cellWidthResizeObserver);
|
||||
|
||||
this._register(templateData.editor!.onDidContentSizeChange((e) => {
|
||||
if (e.contentHeightChanged) {
|
||||
if (this.viewCell.editorHeight !== e.contentHeight) {
|
||||
let viewLayout = templateData.editor!.getLayoutInfo();
|
||||
|
||||
templateData.editor?.layout(
|
||||
{
|
||||
width: viewLayout.width,
|
||||
height: e.contentHeight
|
||||
}
|
||||
);
|
||||
|
||||
this.viewCell.editorHeight = e.contentHeight;
|
||||
|
||||
notebookEditor.layoutNotebookCell(this.viewCell, viewCell.getCellTotalHeight());
|
||||
}
|
||||
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(templateData.editor!.onDidChangeCursorSelection(() => {
|
||||
const primarySelection = templateData.editor!.getSelection();
|
||||
|
||||
if (primarySelection) {
|
||||
this.notebookEditor.revealLineInView(viewCell, primarySelection!.positionLineNumber);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(viewCell.onDidChangeOutputs((splices) => {
|
||||
if (!splices.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.viewCell.outputs.length) {
|
||||
this.templateData.outputContainer!.style.display = 'block';
|
||||
} else {
|
||||
this.templateData.outputContainer!.style.display = 'none';
|
||||
}
|
||||
|
||||
let reversedSplices = splices.reverse();
|
||||
|
||||
reversedSplices.forEach(splice => {
|
||||
viewCell.spliceOutputHeights(splice[0], splice[1], splice[2].map(_ => 0));
|
||||
});
|
||||
|
||||
let removedKeys: IOutput[] = [];
|
||||
|
||||
this.outputElements.forEach((value, key) => {
|
||||
if (viewCell.outputs.indexOf(key) < 0) {
|
||||
// already removed
|
||||
removedKeys.push(key);
|
||||
// remove element from DOM
|
||||
this.templateData?.outputContainer?.removeChild(value);
|
||||
this.notebookEditor.removeInset(key);
|
||||
}
|
||||
});
|
||||
|
||||
removedKeys.forEach(key => {
|
||||
// remove element cache
|
||||
this.outputElements.delete(key);
|
||||
// remove elment resize listener if there is one
|
||||
this.outputResizeListeners.delete(key);
|
||||
});
|
||||
|
||||
let prevElement: HTMLElement | undefined = undefined;
|
||||
|
||||
this.viewCell.outputs.reverse().forEach(output => {
|
||||
if (this.outputElements.has(output)) {
|
||||
// already exist
|
||||
prevElement = this.outputElements.get(output);
|
||||
return;
|
||||
}
|
||||
|
||||
// newly added element
|
||||
let currIndex = this.viewCell.outputs.indexOf(output);
|
||||
this.renderOutput(output, currIndex, prevElement);
|
||||
prevElement = this.outputElements.get(output);
|
||||
});
|
||||
|
||||
let editorHeight = templateData.editor!.getContentHeight();
|
||||
viewCell.editorHeight = editorHeight;
|
||||
notebookEditor.layoutNotebookCell(viewCell, viewCell.getCellTotalHeight());
|
||||
}));
|
||||
|
||||
if (viewCell.outputs.length > 0) {
|
||||
this.templateData.outputContainer!.style.display = 'block';
|
||||
// there are outputs, we need to calcualte their sizes and trigger relayout
|
||||
// @todo, if there is no resizable output, we should not check their height individually, which hurts the performance
|
||||
for (let index = 0; index < this.viewCell.outputs.length; index++) {
|
||||
const currOutput = this.viewCell.outputs[index];
|
||||
|
||||
// always add to the end
|
||||
this.renderOutput(currOutput, index, undefined);
|
||||
}
|
||||
|
||||
viewCell.editorHeight = totalHeight;
|
||||
this.notebookEditor.layoutNotebookCell(viewCell, viewCell.getCellTotalHeight());
|
||||
} else {
|
||||
// noop
|
||||
this.templateData.outputContainer!.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
renderOutput(currOutput: IOutput, index: number, beforeElement?: HTMLElement) {
|
||||
if (!this.outputResizeListeners.has(currOutput)) {
|
||||
this.outputResizeListeners.set(currOutput, new DisposableStore());
|
||||
}
|
||||
|
||||
let outputItemDiv = document.createElement('div');
|
||||
let result: IRenderOutput | undefined = undefined;
|
||||
|
||||
if (currOutput.outputKind === CellOutputKind.Rich) {
|
||||
let transformedDisplayOutput = currOutput as ITransformedDisplayOutputDto;
|
||||
|
||||
if (transformedDisplayOutput.orderedMimeTypes.length > 1) {
|
||||
outputItemDiv.style.position = 'relative';
|
||||
const mimeTypePicker = DOM.$('.multi-mimetype-output');
|
||||
DOM.addClasses(mimeTypePicker, 'codicon', 'codicon-list-selection');
|
||||
outputItemDiv.appendChild(mimeTypePicker);
|
||||
this.outputResizeListeners.get(currOutput)!.add(DOM.addStandardDisposableListener(mimeTypePicker, 'mousedown', async e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await this.pickActiveMimeTypeRenderer(transformedDisplayOutput);
|
||||
}));
|
||||
}
|
||||
let pickedMimeTypeRenderer = currOutput.orderedMimeTypes[currOutput.pickedMimeTypeIndex];
|
||||
|
||||
if (pickedMimeTypeRenderer.isResolved) {
|
||||
// html
|
||||
result = this.notebookEditor.getOutputRenderer().render({ outputKind: CellOutputKind.Rich, data: { 'text/html': pickedMimeTypeRenderer.output! } } as any, outputItemDiv, 'text/html');
|
||||
} else {
|
||||
result = this.notebookEditor.getOutputRenderer().render(currOutput, outputItemDiv, pickedMimeTypeRenderer.mimeType);
|
||||
}
|
||||
} else {
|
||||
// for text and error, there is no mimetype
|
||||
result = this.notebookEditor.getOutputRenderer().render(currOutput, outputItemDiv, undefined);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
this.viewCell.updateOutputHeight(index, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
this.outputElements.set(currOutput, outputItemDiv);
|
||||
|
||||
if (beforeElement) {
|
||||
this.templateData.outputContainer?.insertBefore(outputItemDiv, beforeElement);
|
||||
} else {
|
||||
this.templateData.outputContainer?.appendChild(outputItemDiv);
|
||||
}
|
||||
|
||||
if (result.shadowContent) {
|
||||
this.viewCell.selfSizeMonitoring = true;
|
||||
let editorHeight = this.viewCell.editorHeight;
|
||||
this.notebookEditor.createInset(this.viewCell, currOutput, result.shadowContent, editorHeight + 8 + this.viewCell.getOutputOffset(index));
|
||||
} else {
|
||||
DOM.addClass(outputItemDiv, 'foreground');
|
||||
}
|
||||
|
||||
let hasDynamicHeight = result.hasDynamicHeight;
|
||||
|
||||
if (hasDynamicHeight) {
|
||||
let clientHeight = outputItemDiv.clientHeight;
|
||||
let listDimension = this.notebookEditor.getLayoutInfo();
|
||||
let dimension = listDimension ? {
|
||||
width: listDimension.width - CELL_MARGIN * 2,
|
||||
height: clientHeight
|
||||
} : undefined;
|
||||
const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => {
|
||||
if (this.templateData.outputContainer && document.body.contains(this.templateData.outputContainer!)) {
|
||||
let height = elementSizeObserver.getHeight() + 8 * 2; // include padding
|
||||
|
||||
if (clientHeight === height) {
|
||||
// console.log(this.viewCell.outputs);
|
||||
return;
|
||||
}
|
||||
|
||||
const currIndex = this.viewCell.outputs.indexOf(currOutput);
|
||||
if (currIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewCell.updateOutputHeight(currIndex, height);
|
||||
this.notebookEditor.layoutNotebookCell(this.viewCell, this.viewCell.getCellTotalHeight());
|
||||
}
|
||||
});
|
||||
elementSizeObserver.startObserving();
|
||||
this.outputResizeListeners.get(currOutput)!.add(elementSizeObserver);
|
||||
this.viewCell.updateOutputHeight(index, clientHeight);
|
||||
} else {
|
||||
if (result.shadowContent) {
|
||||
// webview
|
||||
// noop
|
||||
// let cachedHeight = this.viewCell.getOutputHeight(currOutput);
|
||||
} else {
|
||||
// static output
|
||||
|
||||
// @TODO, if we stop checking output height, we need to evaluate it later when checking the height of output container
|
||||
let clientHeight = outputItemDiv.clientHeight;
|
||||
this.viewCell.updateOutputHeight(index, clientHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generateRendererInfo(renderId: number | undefined): string {
|
||||
if (renderId === undefined || renderId === -1) {
|
||||
return 'builtin';
|
||||
}
|
||||
|
||||
let renderInfo = this.notebookService.getRendererInfo(renderId);
|
||||
|
||||
if (renderInfo) {
|
||||
return renderInfo.id.value;
|
||||
}
|
||||
|
||||
return 'builtin';
|
||||
}
|
||||
|
||||
async pickActiveMimeTypeRenderer(output: ITransformedDisplayOutputDto) {
|
||||
let currIndex = output.pickedMimeTypeIndex;
|
||||
const items = output.orderedMimeTypes.map((mimeType, index): IMimeTypeRenderer => ({
|
||||
label: mimeType.mimeType,
|
||||
id: mimeType.mimeType,
|
||||
index: index,
|
||||
picked: index === currIndex,
|
||||
description: this.generateRendererInfo(mimeType.rendererId) + (index === currIndex
|
||||
? nls.localize('curruentActiveMimeType', " (Currently Active)")
|
||||
: ''),
|
||||
}));
|
||||
|
||||
const picker = this.quickInputService.createQuickPick();
|
||||
picker.items = items;
|
||||
picker.activeItems = items.filter(item => !!item.picked);
|
||||
picker.placeholder = nls.localize('promptChooseMimeType.placeHolder', "Select output mimetype to render for current output");
|
||||
|
||||
const pick = await new Promise<number | undefined>(resolve => {
|
||||
picker.onDidAccept(() => {
|
||||
resolve(picker.selectedItems.length === 1 ? (picker.selectedItems[0] as IMimeTypeRenderer).index : undefined);
|
||||
picker.dispose();
|
||||
});
|
||||
picker.show();
|
||||
});
|
||||
|
||||
if (pick === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pick !== currIndex) {
|
||||
// user chooses another mimetype
|
||||
let index = this.viewCell.outputs.indexOf(output);
|
||||
let nextElement = index + 1 < this.viewCell.outputs.length ? this.outputElements.get(this.viewCell.outputs[index + 1]) : undefined;
|
||||
this.outputResizeListeners.get(output)?.clear();
|
||||
let element = this.outputElements.get(output);
|
||||
if (element) {
|
||||
this.templateData?.outputContainer?.removeChild(element);
|
||||
this.notebookEditor.removeInset(output);
|
||||
}
|
||||
|
||||
output.pickedMimeTypeIndex = pick;
|
||||
|
||||
this.renderOutput(output, index, nextElement);
|
||||
this.notebookEditor.layoutNotebookCell(this.viewCell, this.viewCell.getCellTotalHeight());
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.viewCell.detachTextEditor();
|
||||
this.outputResizeListeners.forEach((value) => {
|
||||
value.dispose();
|
||||
});
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver';
|
||||
import { INotebookEditor, CellRenderTemplate, CellFocusMode, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { raceCancellation } from 'vs/base/common/async';
|
||||
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
|
||||
import { CELL_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
|
||||
|
||||
export class StatefullMarkdownCell extends Disposable {
|
||||
private editor: CodeEditorWidget | null = null;
|
||||
private cellContainer: HTMLElement;
|
||||
private editingContainer?: HTMLElement;
|
||||
|
||||
private localDisposables: DisposableStore;
|
||||
|
||||
constructor(
|
||||
notebookEditor: INotebookEditor,
|
||||
public viewCell: CellViewModel,
|
||||
templateData: CellRenderTemplate,
|
||||
editorOptions: IEditorOptions,
|
||||
instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.cellContainer = templateData.cellContainer;
|
||||
this.editingContainer = templateData.editingContainer;
|
||||
this.localDisposables = new DisposableStore();
|
||||
this._register(this.localDisposables);
|
||||
|
||||
const viewUpdate = () => {
|
||||
if (viewCell.state === CellState.Editing) {
|
||||
// switch to editing mode
|
||||
let width: number;
|
||||
const listDimension = notebookEditor.getLayoutInfo();
|
||||
width = listDimension.width - CELL_MARGIN * 2;
|
||||
// if (listDimension) {
|
||||
// } else {
|
||||
// width = this.cellContainer.clientWidth - 24 /** for scrollbar and margin right */;
|
||||
// }
|
||||
|
||||
const lineNum = viewCell.lineCount;
|
||||
const lineHeight = notebookEditor.getLayoutInfo().fontInfo.lineHeight;
|
||||
const totalHeight = Math.max(lineNum, 1) * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING;
|
||||
|
||||
if (this.editor) {
|
||||
// not first time, we don't need to create editor or bind listeners
|
||||
this.editingContainer!.style.display = 'block';
|
||||
viewCell.attachTextEditor(this.editor!);
|
||||
if (notebookEditor.getActiveCell() === viewCell) {
|
||||
this.editor!.focus();
|
||||
}
|
||||
} else {
|
||||
this.editingContainer!.style.display = 'block';
|
||||
this.editingContainer!.innerHTML = '';
|
||||
this.editor = instantiationService.createInstance(CodeEditorWidget, this.editingContainer!, {
|
||||
...editorOptions,
|
||||
dimension: {
|
||||
width: width,
|
||||
height: totalHeight
|
||||
}
|
||||
}, {});
|
||||
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
this._register({ dispose() { cts.dispose(true); } });
|
||||
raceCancellation(viewCell.resolveTextModel(), cts.token).then(model => {
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor!.setModel(model);
|
||||
if (notebookEditor.getActiveCell() === viewCell) {
|
||||
this.editor!.focus();
|
||||
}
|
||||
|
||||
const realContentHeight = this.editor!.getContentHeight();
|
||||
if (realContentHeight !== totalHeight) {
|
||||
this.editor!.layout(
|
||||
{
|
||||
width: width,
|
||||
height: realContentHeight
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
viewCell.attachTextEditor(this.editor!);
|
||||
|
||||
this.localDisposables.add(model.onDidChangeContent(() => {
|
||||
viewCell.setText(model.getLinesContent());
|
||||
let clientHeight = this.cellContainer.clientHeight;
|
||||
this.cellContainer.innerHTML = '';
|
||||
let renderedHTML = viewCell.getHTML();
|
||||
if (renderedHTML) {
|
||||
this.cellContainer.appendChild(renderedHTML);
|
||||
clientHeight = this.cellContainer.clientHeight;
|
||||
}
|
||||
|
||||
notebookEditor.layoutNotebookCell(viewCell, this.editor!.getContentHeight() + 32 + clientHeight);
|
||||
}));
|
||||
|
||||
if (viewCell.state === CellState.Editing) {
|
||||
this.editor!.focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.localDisposables.add(this.editor.onDidContentSizeChange(e => {
|
||||
let viewLayout = this.editor!.getLayoutInfo();
|
||||
|
||||
if (e.contentHeightChanged) {
|
||||
this.editor!.layout(
|
||||
{
|
||||
width: viewLayout.width,
|
||||
height: e.contentHeight
|
||||
}
|
||||
);
|
||||
const clientHeight = this.cellContainer.clientHeight;
|
||||
notebookEditor.layoutNotebookCell(viewCell, e.contentHeight + 32 + clientHeight);
|
||||
}
|
||||
}));
|
||||
|
||||
let cellWidthResizeObserver = getResizesObserver(templateData.editingContainer!, {
|
||||
width: width,
|
||||
height: totalHeight
|
||||
}, () => {
|
||||
let newWidth = cellWidthResizeObserver.getWidth();
|
||||
let realContentHeight = this.editor!.getContentHeight();
|
||||
let layoutInfo = this.editor!.getLayoutInfo();
|
||||
|
||||
// the dimension generated by the resize observer are float numbers, let's round it a bit to avoid relayout.
|
||||
if (newWidth < layoutInfo.width - 0.3 || layoutInfo.width + 0.3 < newWidth) {
|
||||
this.editor!.layout(
|
||||
{
|
||||
width: newWidth,
|
||||
height: realContentHeight
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
cellWidthResizeObserver.startObserving();
|
||||
this.localDisposables.add(cellWidthResizeObserver);
|
||||
|
||||
let markdownRenderer = viewCell.getMarkdownRenderer();
|
||||
this.cellContainer.innerHTML = '';
|
||||
let renderedHTML = viewCell.getHTML();
|
||||
if (renderedHTML) {
|
||||
this.cellContainer.appendChild(renderedHTML);
|
||||
this.localDisposables.add(markdownRenderer.onDidUpdateRender(() => {
|
||||
const clientHeight = this.cellContainer.clientHeight;
|
||||
notebookEditor.layoutNotebookCell(viewCell, clientHeight);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const clientHeight = this.cellContainer.clientHeight;
|
||||
notebookEditor.layoutNotebookCell(viewCell, totalHeight + 32 + clientHeight);
|
||||
this.editor.focus();
|
||||
} else {
|
||||
this.viewCell.detachTextEditor();
|
||||
if (this.editor) {
|
||||
// switch from editing mode
|
||||
this.editingContainer!.style.display = 'none';
|
||||
const clientHeight = templateData.container.clientHeight;
|
||||
notebookEditor.layoutNotebookCell(viewCell, clientHeight);
|
||||
} else {
|
||||
// first time, readonly mode
|
||||
this.editingContainer!.style.display = 'none';
|
||||
|
||||
this.cellContainer.innerHTML = '';
|
||||
let markdownRenderer = viewCell.getMarkdownRenderer();
|
||||
let renderedHTML = viewCell.getHTML();
|
||||
if (renderedHTML) {
|
||||
this.cellContainer.appendChild(renderedHTML);
|
||||
}
|
||||
|
||||
this.localDisposables.add(markdownRenderer.onDidUpdateRender(() => {
|
||||
const clientHeight = templateData.container.clientHeight;
|
||||
notebookEditor.layoutNotebookCell(viewCell, clientHeight);
|
||||
}));
|
||||
|
||||
this.localDisposables.add(viewCell.onDidChangeContent(() => {
|
||||
this.cellContainer.innerHTML = '';
|
||||
let renderedHTML = viewCell.getHTML();
|
||||
if (renderedHTML) {
|
||||
this.cellContainer.appendChild(renderedHTML);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this._register(viewCell.onDidChangeCellState(() => {
|
||||
this.localDisposables.clear();
|
||||
viewUpdate();
|
||||
}));
|
||||
|
||||
this._register(viewCell.onDidChangeFocusMode(() => {
|
||||
if (viewCell.focusMode === CellFocusMode.Editor) {
|
||||
this.editor?.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
viewUpdate();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.viewCell.detachTextEditor();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { renderMarkdown, MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { TokenizationRegistry } from 'vs/editor/common/modes';
|
||||
|
||||
export interface IMarkdownRenderResult extends IDisposable {
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
||||
export class MarkdownRenderer extends Disposable {
|
||||
|
||||
private _onDidUpdateRender = this._register(new Emitter<void>());
|
||||
readonly onDidUpdateRender: Event<void> = this._onDidUpdateRender.event;
|
||||
|
||||
constructor(
|
||||
@IModeService private readonly _modeService: IModeService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private getOptions(disposeables: DisposableStore): MarkdownRenderOptions {
|
||||
return {
|
||||
codeBlockRenderer: (languageAlias, value) => {
|
||||
// In markdown,
|
||||
// it is possible that we stumble upon language aliases (e.g.js instead of javascript)
|
||||
// it is possible no alias is given in which case we fall back to the current editor lang
|
||||
let modeId: string | null = null;
|
||||
modeId = this._modeService.getModeIdForLanguageName(languageAlias || '');
|
||||
|
||||
this._modeService.triggerMode(modeId || '');
|
||||
return Promise.resolve(true).then(_ => {
|
||||
const promise = TokenizationRegistry.getPromise(modeId || '');
|
||||
if (promise) {
|
||||
return promise.then(support => tokenizeToString(value, support));
|
||||
}
|
||||
return tokenizeToString(value, undefined);
|
||||
}).then(code => {
|
||||
return `<span>${code}</span>`;
|
||||
});
|
||||
},
|
||||
codeBlockRenderCallback: () => this._onDidUpdateRender.fire(),
|
||||
actionHandler: {
|
||||
callback: (content) => {
|
||||
this._openerService.open(content, { fromUserGesture: true }).catch(onUnexpectedError);
|
||||
},
|
||||
disposeables
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render(markdown: IMarkdownString | undefined): IMarkdownRenderResult {
|
||||
const disposeables = new DisposableStore();
|
||||
|
||||
let element: HTMLElement;
|
||||
if (!markdown) {
|
||||
element = document.createElement('span');
|
||||
} else {
|
||||
element = renderMarkdown(markdown, this.getOptions(disposeables), { gfm: true });
|
||||
}
|
||||
|
||||
return {
|
||||
element,
|
||||
dispose: () => disposeables.dispose()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IDimension } from 'vs/editor/common/editorCommon';
|
||||
import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver';
|
||||
|
||||
declare const ResizeObserver: any;
|
||||
|
||||
export interface IResizeObserver {
|
||||
startObserving: () => void;
|
||||
stopObserving: () => void;
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export class BrowserResizeObserver extends Disposable implements IResizeObserver {
|
||||
private readonly referenceDomElement: HTMLElement | null;
|
||||
|
||||
private readonly observer: any;
|
||||
private width: number;
|
||||
private height: number;
|
||||
|
||||
constructor(referenceDomElement: HTMLElement | null, dimension: IDimension | undefined, changeCallback: () => void) {
|
||||
super();
|
||||
|
||||
this.referenceDomElement = referenceDomElement;
|
||||
this.width = -1;
|
||||
this.height = -1;
|
||||
|
||||
this.observer = new ResizeObserver((entries: any) => {
|
||||
for (let entry of entries) {
|
||||
if (entry.target === referenceDomElement && entry.contentRect) {
|
||||
if (this.width !== entry.contentRect.width || this.height !== entry.contentRect.height) {
|
||||
this.width = entry.contentRect.width;
|
||||
this.height = entry.contentRect.height;
|
||||
DOM.scheduleAtNextAnimationFrame(() => {
|
||||
changeCallback();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getWidth(): number {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
getHeight(): number {
|
||||
return this.height;
|
||||
}
|
||||
|
||||
startObserving(): void {
|
||||
this.observer.observe(this.referenceDomElement!);
|
||||
}
|
||||
|
||||
stopObserving(): void {
|
||||
this.observer.unobserve(this.referenceDomElement!);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.observer.disconnect();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function getResizesObserver(referenceDomElement: HTMLElement | null, dimension: IDimension | undefined, changeCallback: () => void): IResizeObserver {
|
||||
if (ResizeObserver) {
|
||||
return new BrowserResizeObserver(referenceDomElement, dimension, changeCallback);
|
||||
} else {
|
||||
return new ElementSizeObserver(referenceDomElement, dimension, changeCallback);
|
||||
}
|
||||
}
|
114
src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts
Normal file
114
src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
|
||||
|
||||
|
||||
/**
|
||||
* It should not modify Undo/Redo stack
|
||||
*/
|
||||
export interface ICellEditingDelegate {
|
||||
insertCell?(index: number, viewCell: CellViewModel): void;
|
||||
deleteCell?(index: number, cell: ICell): void;
|
||||
moveCell?(fromIndex: number, toIndex: number): void;
|
||||
}
|
||||
|
||||
export class InsertCellEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Insert Cell';
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private insertIndex: number,
|
||||
private cell: CellViewModel,
|
||||
private editingDelegate: ICellEditingDelegate
|
||||
) {
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell) {
|
||||
throw new Error('Notebook Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.deleteCell(this.insertIndex, this.cell.cell);
|
||||
}
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.insertCell) {
|
||||
throw new Error('Notebook Insert Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.insertCell(this.insertIndex, this.cell);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteCellEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Delete Cell';
|
||||
|
||||
private _rawCell: ICell;
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private insertIndex: number,
|
||||
cell: CellViewModel,
|
||||
private editingDelegate: ICellEditingDelegate,
|
||||
private instantiationService: IInstantiationService,
|
||||
private notebookViewModel: NotebookViewModel
|
||||
) {
|
||||
this._rawCell = cell.cell;
|
||||
|
||||
// save inmem text to `ICell`
|
||||
this._rawCell.source = [cell.getText()];
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.insertCell) {
|
||||
throw new Error('Notebook Insert Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
const cell = this.instantiationService.createInstance(CellViewModel, this.notebookViewModel.viewType, this.notebookViewModel.handle, this._rawCell);
|
||||
this.editingDelegate.insertCell(this.insertIndex, cell);
|
||||
}
|
||||
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.deleteCell) {
|
||||
throw new Error('Notebook Delete Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.deleteCell(this.insertIndex, this._rawCell);
|
||||
}
|
||||
}
|
||||
|
||||
export class MoveCellEdit implements IResourceUndoRedoElement {
|
||||
type: UndoRedoElementType.Resource = UndoRedoElementType.Resource;
|
||||
label: string = 'Delete Cell';
|
||||
|
||||
constructor(
|
||||
public resource: URI,
|
||||
private fromIndex: number,
|
||||
private toIndex: number,
|
||||
private editingDelegate: ICellEditingDelegate
|
||||
) {
|
||||
}
|
||||
|
||||
undo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.moveCell) {
|
||||
throw new Error('Notebook Move Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.moveCell(this.toIndex, this.fromIndex);
|
||||
}
|
||||
|
||||
redo(): void | Promise<void> {
|
||||
if (!this.editingDelegate.moveCell) {
|
||||
throw new Error('Notebook Move Cell not implemented for Undo/Redo');
|
||||
}
|
||||
|
||||
this.editingDelegate.moveCell(this.fromIndex, this.toIndex);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,511 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. 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 * as UUID from 'vs/base/common/uuid';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
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 { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer';
|
||||
import { CellKind, ICell, IOutput, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { CellFindMatch, CellState, CursorAtBoundary, CellFocusMode, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants';
|
||||
|
||||
export class CellViewModel extends Disposable implements ICellViewModel {
|
||||
|
||||
private _mdRenderer: MarkdownRenderer | null = null;
|
||||
private _html: HTMLElement | null = null;
|
||||
protected readonly _onDidDispose = new Emitter<void>();
|
||||
readonly onDidDispose = this._onDidDispose.event;
|
||||
protected readonly _onDidChangeCellState = new Emitter<void>();
|
||||
readonly onDidChangeCellState = this._onDidChangeCellState.event;
|
||||
protected readonly _onDidChangeFocusMode = new Emitter<void>();
|
||||
readonly onDidChangeFocusMode = this._onDidChangeFocusMode.event;
|
||||
protected readonly _onDidChangeOutputs = new Emitter<NotebookCellOutputsSplice[]>();
|
||||
readonly onDidChangeOutputs = this._onDidChangeOutputs.event;
|
||||
private _outputCollection: number[] = [];
|
||||
protected _outputsTop: PrefixSumComputer | null = null;
|
||||
|
||||
get handle() {
|
||||
return this.cell.handle;
|
||||
}
|
||||
|
||||
get uri() {
|
||||
return this.cell.uri;
|
||||
}
|
||||
|
||||
get cellKind() {
|
||||
return this.cell.cellKind;
|
||||
}
|
||||
get lineCount() {
|
||||
return this.cell.source.length;
|
||||
}
|
||||
get outputs() {
|
||||
return this.cell.outputs;
|
||||
}
|
||||
|
||||
private _state: CellState = CellState.Preview;
|
||||
|
||||
get state(): CellState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
set state(newState: CellState) {
|
||||
if (newState === this._state) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._state = newState;
|
||||
this._onDidChangeCellState.fire();
|
||||
}
|
||||
|
||||
private _focusMode: CellFocusMode = CellFocusMode.Container;
|
||||
|
||||
get focusMode() {
|
||||
return this._focusMode;
|
||||
}
|
||||
|
||||
set focusMode(newMode: CellFocusMode) {
|
||||
this._focusMode = newMode;
|
||||
this._onDidChangeFocusMode.fire();
|
||||
}
|
||||
|
||||
private _selfSizeMonitoring: boolean = false;
|
||||
|
||||
set selfSizeMonitoring(newVal: boolean) {
|
||||
this._selfSizeMonitoring = newVal;
|
||||
}
|
||||
|
||||
get selfSizeMonitoring() {
|
||||
return this._selfSizeMonitoring;
|
||||
}
|
||||
|
||||
private _editorHeight = 0;
|
||||
set editorHeight(height: number) {
|
||||
this._editorHeight = height;
|
||||
}
|
||||
|
||||
get editorHeight(): number {
|
||||
return this._editorHeight;
|
||||
}
|
||||
|
||||
protected readonly _onDidChangeEditorAttachState = new Emitter<boolean>();
|
||||
readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event;
|
||||
|
||||
get editorAttached(): boolean {
|
||||
return !!this._textEditor;
|
||||
}
|
||||
|
||||
private _textModel?: model.ITextModel;
|
||||
private _textEditor?: ICodeEditor;
|
||||
private _buffer: model.ITextBuffer | null;
|
||||
private _editorViewStates: editorCommon.ICodeEditorViewState | null;
|
||||
private _lastDecorationId: number = 0;
|
||||
private _resolvedDecorations = new Map<string, { id?: string, options: model.IModelDeltaDecoration }>();
|
||||
private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;
|
||||
private readonly _onDidChangeCursorSelection: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeCursorSelection: Event<void> = this._onDidChangeCursorSelection.event;
|
||||
|
||||
private _cursorChangeListener: IDisposable | null = null;
|
||||
|
||||
readonly id: string = UUID.generateUuid();
|
||||
|
||||
constructor(
|
||||
readonly viewType: string,
|
||||
readonly notebookHandle: number,
|
||||
readonly cell: ICell,
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
@ITextModelService private readonly _modelService: ITextModelService,
|
||||
) {
|
||||
super();
|
||||
if (this.cell.onDidChangeOutputs) {
|
||||
this._register(this.cell.onDidChangeOutputs((splices) => {
|
||||
this._outputCollection = new Array(this.cell.outputs.length);
|
||||
this._outputsTop = null;
|
||||
this._onDidChangeOutputs.fire(splices);
|
||||
}));
|
||||
}
|
||||
|
||||
this._outputCollection = new Array(this.cell.outputs.length);
|
||||
this._buffer = null;
|
||||
this._editorViewStates = null;
|
||||
}
|
||||
|
||||
restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null) {
|
||||
this._editorViewStates = editorViewStates;
|
||||
}
|
||||
|
||||
saveEditorViewState() {
|
||||
if (this._textEditor) {
|
||||
this._editorViewStates = this.saveViewState();
|
||||
}
|
||||
|
||||
return this._editorViewStates;
|
||||
}
|
||||
|
||||
|
||||
//#region Search
|
||||
private readonly _hasFindResult = this._register(new Emitter<boolean>());
|
||||
public readonly hasFindResult: Event<boolean> = this._hasFindResult.event;
|
||||
|
||||
startFind(value: string): CellFindMatch | null {
|
||||
let cellMatches: model.FindMatch[] = [];
|
||||
|
||||
if (this.assertTextModelAttached()) {
|
||||
cellMatches = this._textModel!.findMatches(value, false, false, false, null, false);
|
||||
} else {
|
||||
if (!this._buffer) {
|
||||
this._buffer = this.cell.resolveTextBufferFactory().create(model.DefaultEndOfLine.LF);
|
||||
}
|
||||
|
||||
const lineCount = this._buffer.getLineCount();
|
||||
const fullRange = new Range(1, 1, lineCount, this._buffer.getLineLength(lineCount) + 1);
|
||||
const searchParams = new SearchParams(value, false, false, null);
|
||||
const searchData = searchParams.parseSearchRequest();
|
||||
|
||||
if (!searchData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
cellMatches = this._buffer.findMatchesLineByLine(fullRange, searchData, false, 1000);
|
||||
}
|
||||
|
||||
return {
|
||||
cell: this,
|
||||
matches: cellMatches
|
||||
};
|
||||
}
|
||||
|
||||
assertTextModelAttached(): boolean {
|
||||
if (this._textModel && this._textEditor && this._textEditor.getModel() === this._textModel) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private saveViewState(): editorCommon.ICodeEditorViewState | null {
|
||||
if (!this._textEditor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._textEditor.saveViewState();
|
||||
}
|
||||
|
||||
|
||||
private restoreViewState(state: editorCommon.ICodeEditorViewState | null): void {
|
||||
if (state) {
|
||||
this._textEditor?.restoreViewState(state);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
hasDynamicHeight() {
|
||||
if (this.selfSizeMonitoring) {
|
||||
// if there is an output rendered in the webview, it should always be false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cellKind === CellKind.Code) {
|
||||
if (this.outputs && this.outputs.length > 0) {
|
||||
// if it contains output, it will be marked as dynamic height
|
||||
// thus when it's being rendered, the list view will `probeHeight`
|
||||
// inside which, we will check domNode's height directly instead of doing another `renderElement` with height undefined.
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getHeight(lineHeight: number) {
|
||||
if (this.cellKind === CellKind.Markdown) {
|
||||
return 100;
|
||||
}
|
||||
else {
|
||||
return this.lineCount * lineHeight + 16 + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING;
|
||||
}
|
||||
}
|
||||
setText(strs: string[]) {
|
||||
this.cell.source = strs;
|
||||
this._html = null;
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this._textModel && !this._textModel.isDisposed() && this.state === CellState.Editing) {
|
||||
let cnt = this._textModel.getLineCount();
|
||||
this.cell.source = this._textModel.getLinesContent().map((str, index) => str + (index !== cnt - 1 ? '\n' : ''));
|
||||
}
|
||||
}
|
||||
getText(): string {
|
||||
if (this._textModel) {
|
||||
return this._textModel.getValue();
|
||||
}
|
||||
|
||||
return this.cell.source.join('\n');
|
||||
}
|
||||
|
||||
getHTML(): HTMLElement | null {
|
||||
if (this.cellKind === CellKind.Markdown) {
|
||||
if (this._html) {
|
||||
return this._html;
|
||||
}
|
||||
let renderer = this.getMarkdownRenderer();
|
||||
this._html = renderer.render({ value: this.getText(), isTrusted: true }).element;
|
||||
return this._html;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async resolveTextModel(): Promise<model.ITextModel> {
|
||||
if (!this._textModel) {
|
||||
const ref = await this._modelService.createModelReference(this.cell.uri);
|
||||
this._textModel = ref.object.textEditorModel;
|
||||
this._buffer = this._textModel.getTextBuffer();
|
||||
this._register(ref);
|
||||
this._register(this._textModel.onDidChangeContent(() => {
|
||||
this.cell.contentChange();
|
||||
this._html = null;
|
||||
this._onDidChangeContent.fire();
|
||||
}));
|
||||
}
|
||||
return this._textModel;
|
||||
}
|
||||
|
||||
attachTextEditor(editor: ICodeEditor) {
|
||||
if (!editor.hasModel()) {
|
||||
throw new Error('Invalid editor: model is missing');
|
||||
}
|
||||
|
||||
if (this._textEditor === editor) {
|
||||
if (this._cursorChangeListener === null) {
|
||||
this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => this._onDidChangeCursorSelection.fire());
|
||||
this._onDidChangeCursorSelection.fire();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._textEditor = editor;
|
||||
|
||||
if (this._editorViewStates) {
|
||||
this.restoreViewState(this._editorViewStates);
|
||||
}
|
||||
|
||||
this._resolvedDecorations.forEach((value, key) => {
|
||||
if (key.startsWith('_lazy_')) {
|
||||
// lazy ones
|
||||
|
||||
const ret = this._textEditor!.deltaDecorations([], [value.options]);
|
||||
this._resolvedDecorations.get(key)!.id = ret[0];
|
||||
} else {
|
||||
const ret = this._textEditor!.deltaDecorations([], [value.options]);
|
||||
this._resolvedDecorations.get(key)!.id = ret[0];
|
||||
}
|
||||
});
|
||||
|
||||
this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => this._onDidChangeCursorSelection.fire());
|
||||
this._onDidChangeCursorSelection.fire();
|
||||
this._onDidChangeEditorAttachState.fire(true);
|
||||
}
|
||||
|
||||
detachTextEditor() {
|
||||
this._editorViewStates = this.saveViewState();
|
||||
|
||||
// decorations need to be cleared first as editors can be resued.
|
||||
this._resolvedDecorations.forEach(value => {
|
||||
let resolvedid = value.id;
|
||||
|
||||
if (resolvedid) {
|
||||
this._textEditor?.deltaDecorations([resolvedid], []);
|
||||
}
|
||||
});
|
||||
this._textEditor = undefined;
|
||||
this._cursorChangeListener?.dispose();
|
||||
this._cursorChangeListener = null;
|
||||
this._onDidChangeEditorAttachState.fire(false);
|
||||
}
|
||||
|
||||
revealRangeInCenter(range: Range) {
|
||||
this._textEditor?.revealRangeInCenter(range, editorCommon.ScrollType.Immediate);
|
||||
}
|
||||
|
||||
setSelection(range: Range) {
|
||||
this._textEditor?.setSelection(range);
|
||||
}
|
||||
|
||||
getLineScrollTopOffset(line: number): number {
|
||||
if (!this._textEditor) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this._textEditor.getTopForLineNumber(line) + EDITOR_TOP_PADDING + EDITOR_TOOLBAR_HEIGHT;
|
||||
}
|
||||
|
||||
addDecoration(decoration: model.IModelDeltaDecoration): string {
|
||||
if (!this._textEditor) {
|
||||
const id = ++this._lastDecorationId;
|
||||
const decorationId = `_lazy_${this.id};${id}`;
|
||||
|
||||
this._resolvedDecorations.set(decorationId, { options: decoration });
|
||||
return decorationId;
|
||||
}
|
||||
|
||||
const result = this._textEditor.deltaDecorations([], [decoration]);
|
||||
this._resolvedDecorations.set(result[0], { id: result[0], options: decoration });
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
removeDecoration(decorationId: string) {
|
||||
const realDecorationId = this._resolvedDecorations.get(decorationId);
|
||||
|
||||
if (this._textEditor && realDecorationId && realDecorationId.id !== undefined) {
|
||||
this._textEditor.deltaDecorations([realDecorationId.id!], []);
|
||||
}
|
||||
|
||||
// lastly, remove all the cache
|
||||
this._resolvedDecorations.delete(decorationId);
|
||||
}
|
||||
|
||||
deltaDecorations(oldDecorations: string[], newDecorations: model.IModelDeltaDecoration[]): string[] {
|
||||
oldDecorations.forEach(id => {
|
||||
this.removeDecoration(id);
|
||||
});
|
||||
|
||||
const ret = newDecorations.map(option => {
|
||||
return this.addDecoration(option);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
onDeselect() {
|
||||
this.state = CellState.Preview;
|
||||
}
|
||||
|
||||
cursorAtBoundary(): CursorAtBoundary {
|
||||
if (!this._textEditor) {
|
||||
return CursorAtBoundary.None;
|
||||
}
|
||||
|
||||
// only validate primary cursor
|
||||
const selection = this._textEditor.getSelection();
|
||||
|
||||
// only validate empty cursor
|
||||
if (!selection || !selection.isEmpty()) {
|
||||
return CursorAtBoundary.None;
|
||||
}
|
||||
|
||||
// we don't allow attaching text editor without a model
|
||||
const lineCnt = this._textEditor.getModel()!.getLineCount();
|
||||
|
||||
if (selection.startLineNumber === lineCnt) {
|
||||
// bottom
|
||||
|
||||
if (selection.startLineNumber === 1) {
|
||||
return CursorAtBoundary.Both;
|
||||
} else {
|
||||
return CursorAtBoundary.Bottom;
|
||||
}
|
||||
}
|
||||
|
||||
if (selection.startLineNumber === 1) {
|
||||
return CursorAtBoundary.Top;
|
||||
}
|
||||
|
||||
return CursorAtBoundary.None;
|
||||
}
|
||||
|
||||
getMarkdownRenderer() {
|
||||
if (!this._mdRenderer) {
|
||||
this._mdRenderer = this._instaService.createInstance(MarkdownRenderer);
|
||||
}
|
||||
return this._mdRenderer;
|
||||
}
|
||||
|
||||
updateOutputHeight(index: number, height: number) {
|
||||
if (index >= this._outputCollection.length) {
|
||||
throw new Error('Output index out of range!');
|
||||
}
|
||||
|
||||
this._outputCollection[index] = height;
|
||||
this._ensureOutputsTop();
|
||||
this._outputsTop!.changeValue(index, height);
|
||||
}
|
||||
|
||||
getOutputOffset(index: number): number {
|
||||
if (index >= this._outputCollection.length) {
|
||||
throw new Error('Output index out of range!');
|
||||
}
|
||||
|
||||
this._ensureOutputsTop();
|
||||
|
||||
return this._outputsTop!.getAccumulatedValue(index - 1);
|
||||
}
|
||||
|
||||
getOutputHeight(output: IOutput): number | undefined {
|
||||
let index = this.cell.outputs.indexOf(output);
|
||||
|
||||
if (index < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (index < this._outputCollection.length) {
|
||||
return this._outputCollection[index];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getOutputTotalHeight(): number {
|
||||
this._ensureOutputsTop();
|
||||
|
||||
return this._outputsTop!.getTotalValue();
|
||||
}
|
||||
|
||||
spliceOutputHeights(start: number, deleteCnt: number, heights: number[]) {
|
||||
this._ensureOutputsTop();
|
||||
|
||||
this._outputsTop!.removeValues(start, deleteCnt);
|
||||
if (heights.length) {
|
||||
const values = new Uint32Array(heights.length);
|
||||
for (let i = 0; i < heights.length; i++) {
|
||||
values[i] = heights[i];
|
||||
}
|
||||
|
||||
this._outputsTop!.insertValues(start, values);
|
||||
}
|
||||
}
|
||||
|
||||
getCellTotalHeight(): number {
|
||||
if (this.outputs.length) {
|
||||
return EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING + 16 + this.getOutputTotalHeight();
|
||||
} else {
|
||||
return EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING + this.getOutputTotalHeight();
|
||||
}
|
||||
}
|
||||
|
||||
protected _ensureOutputsTop(): void {
|
||||
if (!this._outputsTop) {
|
||||
const values = new Uint32Array(this._outputCollection.length);
|
||||
for (let i = 0; i < this._outputCollection.length; i++) {
|
||||
values[i] = this._outputCollection[i];
|
||||
}
|
||||
|
||||
this._outputsTop = new PrefixSumComputer(values);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,380 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
|
||||
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
|
||||
import { ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { IModelDeltaDecoration } from 'vs/editor/common/model';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { CellFindMatch, CellState, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { WorkspaceTextEdit } from 'vs/editor/common/modes';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { InsertCellEdit, DeleteCellEdit, MoveCellEdit } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEdit';
|
||||
|
||||
export interface INotebookEditorViewState {
|
||||
editingCells: { [key: number]: boolean };
|
||||
editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState | null };
|
||||
}
|
||||
|
||||
export interface ICellModelDecorations {
|
||||
ownerId: number;
|
||||
decorations: string[];
|
||||
}
|
||||
|
||||
export interface ICellModelDeltaDecorations {
|
||||
ownerId: number;
|
||||
decorations: IModelDeltaDecoration[];
|
||||
}
|
||||
|
||||
export interface IModelDecorationsChangeAccessor {
|
||||
deltaDecorations(oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[];
|
||||
}
|
||||
|
||||
const invalidFunc = () => { throw new Error(`Invalid change accessor`); };
|
||||
|
||||
|
||||
export type NotebookViewCellsSplice = [
|
||||
number /* start */,
|
||||
number /* delete count */,
|
||||
CellViewModel[]
|
||||
];
|
||||
|
||||
export interface INotebookViewCellsUpdateEvent {
|
||||
synchronous: boolean;
|
||||
splices: NotebookViewCellsSplice[];
|
||||
}
|
||||
|
||||
export class NotebookViewModel extends Disposable {
|
||||
private _localStore: DisposableStore = this._register(new DisposableStore());
|
||||
private _viewCells: CellViewModel[] = [];
|
||||
|
||||
get viewCells(): ICellViewModel[] {
|
||||
return this._viewCells;
|
||||
}
|
||||
|
||||
get notebookDocument() {
|
||||
return this._model.notebook;
|
||||
}
|
||||
|
||||
get renderers() {
|
||||
return this._model.notebook!.renderers;
|
||||
}
|
||||
|
||||
get handle() {
|
||||
return this._model.notebook.handle;
|
||||
}
|
||||
|
||||
get languages() {
|
||||
return this._model.notebook.languages;
|
||||
}
|
||||
|
||||
get uri() {
|
||||
return this._model.notebook.uri;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeViewCells = new Emitter<INotebookViewCellsUpdateEvent>();
|
||||
get onDidChangeViewCells(): Event<INotebookViewCellsUpdateEvent> { return this._onDidChangeViewCells.event; }
|
||||
|
||||
private _lastNotebookEditResource: URI[] = [];
|
||||
|
||||
get lastNotebookEditResource(): URI | null {
|
||||
if (this._lastNotebookEditResource.length) {
|
||||
return this._lastNotebookEditResource[this._lastNotebookEditResource.length - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public viewType: string,
|
||||
private _model: NotebookEditorModel,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IBulkEditService private readonly bulkEditService: IBulkEditService,
|
||||
@IUndoRedoService private readonly undoService: IUndoRedoService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(this._model.onDidChangeCells(e => {
|
||||
this._onDidChangeViewCells.fire({
|
||||
synchronous: true,
|
||||
splices: e.map(splice => {
|
||||
return [splice[0], splice[1], splice[2].map(cell => this.instantiationService.createInstance(CellViewModel, this.viewType, this.handle, cell))];
|
||||
})
|
||||
});
|
||||
}));
|
||||
|
||||
this._viewCells = this._model!.notebook!.cells.map(cell => {
|
||||
const viewCell = this.instantiationService.createInstance(CellViewModel, this.viewType, this._model!.notebook!.handle, cell);
|
||||
this._localStore.add(viewCell);
|
||||
return viewCell;
|
||||
});
|
||||
}
|
||||
|
||||
isDirty() {
|
||||
return this._model.isDirty();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this._viewCells.forEach(cell => {
|
||||
if (cell.getText() !== '') {
|
||||
cell.state = CellState.Preview;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getViewCellIndex(cell: ICellViewModel) {
|
||||
return this._viewCells.indexOf(cell as CellViewModel);
|
||||
}
|
||||
|
||||
private _insertCellDelegate(insertIndex: number, insertCell: CellViewModel) {
|
||||
this._viewCells!.splice(insertIndex, 0, insertCell);
|
||||
this._model.insertCell(insertCell.cell, insertIndex);
|
||||
this._localStore.add(insertCell);
|
||||
this._onDidChangeViewCells.fire({ synchronous: true, splices: [[insertIndex, 0, [insertCell]]] });
|
||||
}
|
||||
|
||||
private _deleteCellDelegate(deleteIndex: number, cell: ICell) {
|
||||
this._viewCells.splice(deleteIndex, 1);
|
||||
this._model.deleteCell(deleteIndex);
|
||||
this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] });
|
||||
}
|
||||
|
||||
insertCell(index: number, cell: ICell, synchronous: boolean): CellViewModel {
|
||||
const newCell = this.instantiationService.createInstance(CellViewModel, this.viewType, this.handle, cell);
|
||||
this._viewCells!.splice(index, 0, newCell);
|
||||
this._model.insertCell(newCell.cell, index);
|
||||
this._localStore.add(newCell);
|
||||
this.undoService.pushElement(new InsertCellEdit(this.uri, index, newCell, {
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this)
|
||||
}));
|
||||
|
||||
this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 0, [newCell]]] });
|
||||
return newCell;
|
||||
}
|
||||
|
||||
deleteCell(index: number, synchronous: boolean) {
|
||||
let viewCell = this._viewCells[index];
|
||||
this._viewCells.splice(index, 1);
|
||||
this._model.deleteCell(index);
|
||||
|
||||
this.undoService.pushElement(new DeleteCellEdit(this.uri, index, viewCell, {
|
||||
insertCell: this._insertCellDelegate.bind(this),
|
||||
deleteCell: this._deleteCellDelegate.bind(this)
|
||||
}, this.instantiationService, this));
|
||||
|
||||
this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] });
|
||||
viewCell.dispose();
|
||||
}
|
||||
|
||||
moveCellToIdx(index: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean = true): boolean {
|
||||
const viewCell = this.viewCells[index] as CellViewModel;
|
||||
if (!viewCell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.viewCells.splice(index, 1);
|
||||
this._model.deleteCell(index);
|
||||
|
||||
this.viewCells!.splice(newIdx, 0, viewCell);
|
||||
this._model.insertCell(viewCell.cell, newIdx);
|
||||
|
||||
if (pushedToUndoStack) {
|
||||
this.undoService.pushElement(new MoveCellEdit(this.uri, index, newIdx, {
|
||||
moveCell: (fromIndex: number, toIndex: number) => {
|
||||
this.moveCellToIdx(fromIndex, toIndex, true, false);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] });
|
||||
this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[newIdx, 0, [viewCell]]] });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
saveEditorViewState(): INotebookEditorViewState {
|
||||
const state: { [key: number]: boolean } = {};
|
||||
this._viewCells.filter(cell => cell.state === CellState.Editing).forEach(cell => state[cell.cell.handle] = true);
|
||||
const editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState } = {};
|
||||
this._viewCells.map(cell => ({ handle: cell.cell.handle, state: cell.saveEditorViewState() })).forEach(viewState => {
|
||||
if (viewState.state) {
|
||||
editorViewStates[viewState.handle] = viewState.state;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
editingCells: state,
|
||||
editorViewStates: editorViewStates
|
||||
};
|
||||
}
|
||||
|
||||
restoreEditorViewState(viewState: INotebookEditorViewState | undefined): void {
|
||||
if (!viewState) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._viewCells.forEach(cell => {
|
||||
const isEditing = viewState.editingCells && viewState.editingCells[cell.handle];
|
||||
const editorViewState = viewState.editorViewStates && viewState.editorViewStates[cell.handle];
|
||||
|
||||
cell.state = isEditing ? CellState.Editing : CellState.Preview;
|
||||
cell.restoreEditorViewState(editorViewState);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor decorations across cells. For example, find decorations for multiple code cells
|
||||
* The reason that we can't completely delegate this to CodeEditorWidget is most of the time, the editors for cells are not created yet but we already have decorations for them.
|
||||
*/
|
||||
changeDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null {
|
||||
const changeAccessor: IModelDecorationsChangeAccessor = {
|
||||
deltaDecorations: (oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] => {
|
||||
return this.deltaDecorationsImpl(oldDecorations, newDecorations);
|
||||
}
|
||||
};
|
||||
|
||||
let result: T | null = null;
|
||||
try {
|
||||
result = callback(changeAccessor);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
}
|
||||
|
||||
changeAccessor.deltaDecorations = invalidFunc;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
deltaDecorationsImpl(oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] {
|
||||
|
||||
const mapping = new Map<number, { cell: CellViewModel; oldDecorations: string[]; newDecorations: IModelDeltaDecoration[] }>();
|
||||
oldDecorations.forEach(oldDecoration => {
|
||||
const ownerId = oldDecoration.ownerId;
|
||||
|
||||
if (!mapping.has(ownerId)) {
|
||||
const cell = this._viewCells.find(cell => cell.handle === ownerId);
|
||||
if (cell) {
|
||||
mapping.set(ownerId, { cell: cell, oldDecorations: [], newDecorations: [] });
|
||||
}
|
||||
}
|
||||
|
||||
const data = mapping.get(ownerId)!;
|
||||
if (data) {
|
||||
data.oldDecorations = oldDecoration.decorations;
|
||||
}
|
||||
});
|
||||
|
||||
newDecorations.forEach(newDecoration => {
|
||||
const ownerId = newDecoration.ownerId;
|
||||
|
||||
if (!mapping.has(ownerId)) {
|
||||
const cell = this._viewCells.find(cell => cell.handle === ownerId);
|
||||
|
||||
if (cell) {
|
||||
mapping.set(ownerId, { cell: cell, oldDecorations: [], newDecorations: [] });
|
||||
}
|
||||
}
|
||||
|
||||
const data = mapping.get(ownerId)!;
|
||||
if (data) {
|
||||
data.newDecorations = newDecoration.decorations;
|
||||
}
|
||||
});
|
||||
|
||||
const ret: ICellModelDecorations[] = [];
|
||||
mapping.forEach((value, ownerId) => {
|
||||
const cellRet = value.cell.deltaDecorations(value.oldDecorations, value.newDecorations);
|
||||
ret.push({
|
||||
ownerId: ownerId,
|
||||
decorations: cellRet
|
||||
});
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search in notebook text model
|
||||
* @param value
|
||||
*/
|
||||
find(value: string): CellFindMatch[] {
|
||||
const matches: CellFindMatch[] = [];
|
||||
this._viewCells.forEach(cell => {
|
||||
const cellMatches = cell.startFind(value);
|
||||
if (cellMatches) {
|
||||
matches.push(cellMatches);
|
||||
}
|
||||
});
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
replaceOne(cell: ICellViewModel, range: Range, text: string): Promise<void> {
|
||||
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' });
|
||||
});
|
||||
}
|
||||
|
||||
async replaceAll(matches: CellFindMatch[], text: string): Promise<void> {
|
||||
if (!matches.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let textEdits: WorkspaceTextEdit[] = [];
|
||||
this._lastNotebookEditResource.push(matches[0].cell.uri);
|
||||
|
||||
matches.forEach(match => {
|
||||
match.matches.forEach(singleMatch => {
|
||||
textEdits.push({
|
||||
edit: { range: singleMatch.range, text: text },
|
||||
resource: match.cell.uri
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(matches.map(match => {
|
||||
return match.cell.resolveTextModel();
|
||||
})).then(async () => {
|
||||
this.bulkEditService.apply({ edits: textEdits }, { quotableLabel: 'Notebook Replace All' });
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
canUndo(): boolean {
|
||||
return this.undoService.canUndo(this.uri);
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.undoService.undo(this.uri);
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.undoService.redo(this.uri);
|
||||
}
|
||||
|
||||
equal(model: NotebookEditorModel) {
|
||||
return this._model === model;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._localStore.clear();
|
||||
this._viewCells.forEach(cell => {
|
||||
cell.save();
|
||||
cell.dispose();
|
||||
});
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ICell, IOutput, NotebookCellOutputsSplice, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { PieceTreeTextBufferFactory, PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
export class NotebookCellTextModel implements ICell {
|
||||
private _onDidChangeOutputs = new Emitter<NotebookCellOutputsSplice[]>();
|
||||
onDidChangeOutputs: Event<NotebookCellOutputsSplice[]> = this._onDidChangeOutputs.event;
|
||||
|
||||
private _onDidChangeContent = new Emitter<void>();
|
||||
onDidChangeContent: Event<void> = this._onDidChangeContent.event;
|
||||
|
||||
private _outputs: IOutput[];
|
||||
|
||||
get outputs(): IOutput[] {
|
||||
return this._outputs;
|
||||
}
|
||||
|
||||
get source() {
|
||||
return this._source;
|
||||
}
|
||||
|
||||
set source(newValue: string[]) {
|
||||
this._source = newValue;
|
||||
this._buffer = null;
|
||||
}
|
||||
|
||||
private _buffer: PieceTreeTextBufferFactory | null = null;
|
||||
|
||||
constructor(
|
||||
readonly uri: URI,
|
||||
public handle: number,
|
||||
private _source: string[],
|
||||
public language: string,
|
||||
public cellKind: CellKind,
|
||||
outputs: IOutput[]
|
||||
) {
|
||||
this._outputs = outputs;
|
||||
}
|
||||
|
||||
contentChange() {
|
||||
this._onDidChangeContent.fire();
|
||||
|
||||
}
|
||||
|
||||
spliceNotebookCellOutputs(splices: NotebookCellOutputsSplice[]): void {
|
||||
splices.reverse().forEach(splice => {
|
||||
this.outputs.splice(splice[0], splice[1], ...splice[2]);
|
||||
});
|
||||
|
||||
this._onDidChangeOutputs.fire(splices);
|
||||
}
|
||||
|
||||
resolveTextBufferFactory(): PieceTreeTextBufferFactory {
|
||||
if (this._buffer) {
|
||||
return this._buffer;
|
||||
}
|
||||
|
||||
let builder = new PieceTreeTextBufferBuilder();
|
||||
builder.acceptChunk(this.source.join('\n'));
|
||||
this._buffer = builder.finish(true);
|
||||
return this._buffer;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. 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 { URI } from 'vs/base/common/uri';
|
||||
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
|
||||
import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
|
||||
export class NotebookTextModel extends Disposable implements INotebookTextModel {
|
||||
private readonly _onWillDispose: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onWillDispose: Event<void> = this._onWillDispose.event;
|
||||
private readonly _onDidChangeCells = new Emitter<NotebookCellsSplice[]>();
|
||||
get onDidChangeCells(): Event<NotebookCellsSplice[]> { return this._onDidChangeCells.event; }
|
||||
private _onDidChangeContent = new Emitter<void>();
|
||||
onDidChangeContent: Event<void> = this._onDidChangeContent.event;
|
||||
private _mapping: Map<number, NotebookCellTextModel> = new Map();
|
||||
private _cellListeners: Map<number, IDisposable> = new Map();
|
||||
cells: NotebookCellTextModel[];
|
||||
activeCell: NotebookCellTextModel | undefined;
|
||||
languages: string[] = [];
|
||||
renderers = new Set<number>();
|
||||
|
||||
constructor(
|
||||
public handle: number,
|
||||
public viewType: string,
|
||||
public uri: URI
|
||||
) {
|
||||
super();
|
||||
this.cells = [];
|
||||
}
|
||||
|
||||
updateLanguages(languages: string[]) {
|
||||
this.languages = languages;
|
||||
}
|
||||
|
||||
updateRenderers(renderers: number[]) {
|
||||
renderers.forEach(render => {
|
||||
this.renderers.add(render);
|
||||
});
|
||||
}
|
||||
|
||||
updateActiveCell(handle: number) {
|
||||
this.activeCell = this._mapping.get(handle);
|
||||
}
|
||||
|
||||
insertNewCell(index: number, cell: NotebookCellTextModel): void {
|
||||
this._mapping.set(cell.handle, cell);
|
||||
this.cells.splice(index, 0, cell);
|
||||
let dirtyStateListener = cell.onDidChangeContent(() => {
|
||||
this._onDidChangeContent.fire();
|
||||
});
|
||||
|
||||
this._cellListeners.set(cell.handle, dirtyStateListener);
|
||||
this._onDidChangeContent.fire();
|
||||
return;
|
||||
}
|
||||
|
||||
removeCell(index: number) {
|
||||
let cell = this.cells[index];
|
||||
this._cellListeners.get(cell.handle)?.dispose();
|
||||
this._cellListeners.delete(cell.handle);
|
||||
this.cells.splice(index, 1);
|
||||
this._onDidChangeContent.fire();
|
||||
}
|
||||
|
||||
|
||||
// TODO@rebornix should this trigger content change event?
|
||||
$spliceNotebookCells(splices: NotebookCellsSplice[]): void {
|
||||
splices.reverse().forEach(splice => {
|
||||
let cellDtos = splice[2];
|
||||
let newCells = cellDtos.map(cell => {
|
||||
let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs || []);
|
||||
this._mapping.set(cell.handle, mainCell);
|
||||
let dirtyStateListener = mainCell.onDidChangeContent(() => {
|
||||
this._onDidChangeContent.fire();
|
||||
});
|
||||
this._cellListeners.set(cell.handle, dirtyStateListener);
|
||||
return mainCell;
|
||||
});
|
||||
|
||||
this.cells.splice(splice[0], splice[1], ...newCells);
|
||||
});
|
||||
|
||||
this._onDidChangeCells.fire(splices);
|
||||
}
|
||||
|
||||
// TODO@rebornix should this trigger content change event?
|
||||
$spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void {
|
||||
let cell = this._mapping.get(cellHandle);
|
||||
cell?.spliceNotebookCellOutputs(splices);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onWillDispose.fire();
|
||||
this._cellListeners.forEach(val => val.dispose());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
328
src/vs/workbench/contrib/notebook/common/notebookCommon.ts
Normal file
328
src/vs/workbench/contrib/notebook/common/notebookCommon.ts
Normal file
|
@ -0,0 +1,328 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { ISplice } from 'vs/base/common/sequence';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
export enum CellKind {
|
||||
Markdown = 1,
|
||||
Code = 2
|
||||
}
|
||||
|
||||
export enum CellOutputKind {
|
||||
Text = 1,
|
||||
Error = 2,
|
||||
Rich = 3
|
||||
}
|
||||
|
||||
export const NOTEBOOK_DISPLAY_ORDER = [
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/svg+xml',
|
||||
'text/markdown',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain'
|
||||
];
|
||||
|
||||
export interface INotebookDisplayOrder {
|
||||
defaultOrder: string[];
|
||||
userOrder?: string[];
|
||||
}
|
||||
|
||||
export interface INotebookMimeTypeSelector {
|
||||
type: string;
|
||||
subTypes?: string[];
|
||||
}
|
||||
|
||||
export interface INotebookRendererInfo {
|
||||
id: ExtensionIdentifier;
|
||||
extensionLocation: URI,
|
||||
preloads: URI[]
|
||||
}
|
||||
|
||||
export interface INotebookSelectors {
|
||||
readonly filenamePattern?: string;
|
||||
}
|
||||
|
||||
export interface IStreamOutput {
|
||||
outputKind: CellOutputKind.Text;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface IErrorOutput {
|
||||
outputKind: CellOutputKind.Error;
|
||||
/**
|
||||
* Exception Name
|
||||
*/
|
||||
ename?: string;
|
||||
/**
|
||||
* Exception Value
|
||||
*/
|
||||
evalue?: string;
|
||||
/**
|
||||
* Exception call stacks
|
||||
*/
|
||||
traceback?: string[];
|
||||
}
|
||||
|
||||
export interface IDisplayOutput {
|
||||
outputKind: CellOutputKind.Rich;
|
||||
/**
|
||||
* { mime_type: value }
|
||||
*/
|
||||
data: { [key: string]: any; }
|
||||
}
|
||||
|
||||
export enum MimeTypeRendererResolver {
|
||||
Core,
|
||||
Active,
|
||||
Lazy
|
||||
}
|
||||
|
||||
export interface IOrderedMimeType {
|
||||
mimeType: string;
|
||||
isResolved: boolean;
|
||||
rendererId?: number;
|
||||
output?: string;
|
||||
}
|
||||
|
||||
export interface ITransformedDisplayOutputDto {
|
||||
outputKind: CellOutputKind.Rich;
|
||||
data: { [key: string]: any; }
|
||||
|
||||
orderedMimeTypes: IOrderedMimeType[];
|
||||
pickedMimeTypeIndex: number;
|
||||
}
|
||||
|
||||
export interface IGenericOutput {
|
||||
outputKind: CellOutputKind;
|
||||
pickedMimeType?: string;
|
||||
pickedRenderer?: number;
|
||||
transformedOutput?: { [key: string]: IDisplayOutput };
|
||||
}
|
||||
|
||||
export type IOutput = ITransformedDisplayOutputDto | IStreamOutput | IErrorOutput;
|
||||
|
||||
export interface ICell {
|
||||
readonly uri: URI;
|
||||
handle: number;
|
||||
source: string[];
|
||||
language: string;
|
||||
cellKind: CellKind;
|
||||
outputs: IOutput[];
|
||||
onDidChangeOutputs?: Event<NotebookCellOutputsSplice[]>;
|
||||
resolveTextBufferFactory(): PieceTreeTextBufferFactory;
|
||||
// TODO@rebornix it should be later on replaced by moving textmodel resolution into CellTextModel
|
||||
contentChange(): void;
|
||||
}
|
||||
|
||||
export interface LanguageInfo {
|
||||
file_extension: string;
|
||||
}
|
||||
|
||||
export interface IMetadata {
|
||||
language_info: LanguageInfo;
|
||||
}
|
||||
|
||||
export interface INotebookTextModel {
|
||||
handle: number;
|
||||
viewType: string;
|
||||
// metadata: IMetadata;
|
||||
readonly uri: URI;
|
||||
languages: string[];
|
||||
cells: ICell[];
|
||||
renderers: Set<number>;
|
||||
onDidChangeCells?: Event<NotebookCellsSplice[]>;
|
||||
onDidChangeContent: Event<void>;
|
||||
onWillDispose(listener: () => void): IDisposable;
|
||||
}
|
||||
|
||||
export interface IRenderOutput {
|
||||
shadowContent?: string;
|
||||
hasDynamicHeight: boolean;
|
||||
}
|
||||
|
||||
export type NotebookCellsSplice = [
|
||||
number /* start */,
|
||||
number /* delete count */,
|
||||
ICell[]
|
||||
];
|
||||
|
||||
export type NotebookCellOutputsSplice = [
|
||||
number /* start */,
|
||||
number /* delete count */,
|
||||
IOutput[]
|
||||
];
|
||||
|
||||
export namespace CellUri {
|
||||
|
||||
export const scheme = 'vscode-notebook';
|
||||
|
||||
export function generate(notebook: URI, handle: number): URI {
|
||||
return notebook.with({
|
||||
query: JSON.stringify({ cell: handle, notebook: notebook.toString() }),
|
||||
scheme,
|
||||
});
|
||||
}
|
||||
|
||||
export function parse(cell: URI): { notebook: URI, handle: number } | undefined {
|
||||
if (cell.scheme !== scheme) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const data = <{ cell: number, notebook: string }>JSON.parse(cell.query);
|
||||
return {
|
||||
handle: data.cell,
|
||||
notebook: URI.parse(data.notebook)
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function mimeTypeSupportedByCore(mimeType: string) {
|
||||
if ([
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/svg+xml',
|
||||
'text/markdown',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain',
|
||||
'text/x-javascript'
|
||||
].indexOf(mimeType) > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// if (isWindows) {
|
||||
// value = value.replace(/\//g, '\\');
|
||||
// }
|
||||
|
||||
function matchGlobUniversal(pattern: string, path: string) {
|
||||
if (isWindows) {
|
||||
pattern = pattern.replace(/\//g, '\\');
|
||||
path = path.replace(/\//g, '\\');
|
||||
}
|
||||
|
||||
return glob.match(pattern, path);
|
||||
}
|
||||
|
||||
|
||||
function getMimeTypeOrder(mimeType: string, userDisplayOrder: string[], documentDisplayOrder: string[], defaultOrder: string[]) {
|
||||
let order = 0;
|
||||
for (let i = 0; i < userDisplayOrder.length; i++) {
|
||||
if (matchGlobUniversal(userDisplayOrder[i], mimeType)) {
|
||||
return order;
|
||||
}
|
||||
order++;
|
||||
}
|
||||
|
||||
for (let i = 0; i < documentDisplayOrder.length; i++) {
|
||||
if (matchGlobUniversal(documentDisplayOrder[i], mimeType)) {
|
||||
return order;
|
||||
}
|
||||
|
||||
order++;
|
||||
}
|
||||
|
||||
for (let i = 0; i < defaultOrder.length; i++) {
|
||||
if (matchGlobUniversal(defaultOrder[i], mimeType)) {
|
||||
return order;
|
||||
}
|
||||
|
||||
order++;
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
export function sortMimeTypes(mimeTypes: string[], userDisplayOrder: string[], documentDisplayOrder: string[], defaultOrder: string[]) {
|
||||
const sorted = mimeTypes.sort((a, b) => {
|
||||
return getMimeTypeOrder(a, userDisplayOrder, documentDisplayOrder, defaultOrder) - getMimeTypeOrder(b, userDisplayOrder, documentDisplayOrder, defaultOrder);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
interface IMutableSplice<T> extends ISplice<T> {
|
||||
deleteCount: number;
|
||||
}
|
||||
|
||||
export function diff<T>(before: T[], after: T[], contains: (a: T) => boolean): ISplice<T>[] {
|
||||
const result: IMutableSplice<T>[] = [];
|
||||
|
||||
function pushSplice(start: number, deleteCount: number, toInsert: T[]): void {
|
||||
if (deleteCount === 0 && toInsert.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latest = result[result.length - 1];
|
||||
|
||||
if (latest && latest.start + latest.deleteCount === start) {
|
||||
latest.deleteCount += deleteCount;
|
||||
latest.toInsert.push(...toInsert);
|
||||
} else {
|
||||
result.push({ start, deleteCount, toInsert });
|
||||
}
|
||||
}
|
||||
|
||||
let beforeIdx = 0;
|
||||
let afterIdx = 0;
|
||||
|
||||
while (true) {
|
||||
if (beforeIdx === before.length) {
|
||||
pushSplice(beforeIdx, 0, after.slice(afterIdx));
|
||||
break;
|
||||
}
|
||||
|
||||
if (afterIdx === after.length) {
|
||||
pushSplice(beforeIdx, before.length - beforeIdx, []);
|
||||
break;
|
||||
}
|
||||
|
||||
const beforeElement = before[beforeIdx];
|
||||
const afterElement = after[afterIdx];
|
||||
|
||||
if (beforeElement === afterElement) {
|
||||
// equal
|
||||
beforeIdx += 1;
|
||||
afterIdx += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (contains(afterElement)) {
|
||||
// `afterElement` exists before, which means some elements before `afterElement` are deleted
|
||||
pushSplice(beforeIdx, 1, []);
|
||||
beforeIdx += 1;
|
||||
} else {
|
||||
// `afterElement` added
|
||||
pushSplice(beforeIdx, 0, [afterElement]);
|
||||
afterIdx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface ICellEditorViewState {
|
||||
selections: editorCommon.ICursorState[];
|
||||
}
|
||||
|
||||
export const NOTEBOOK_EDITOR_CURSOR_BOUNDARY = new RawContextKey<'none' | 'top' | 'bottom' | 'both'>('notebookEditorCursorAtBoundary', 'none');
|
|
@ -0,0 +1,30 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
|
||||
export class NotebookOutputRendererInfo {
|
||||
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly mimeTypes: readonly string[];
|
||||
readonly mimeTypeGlobs: glob.ParsedPattern[];
|
||||
|
||||
constructor(descriptor: {
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly mimeTypes: readonly string[];
|
||||
}) {
|
||||
this.id = descriptor.id;
|
||||
this.displayName = descriptor.displayName;
|
||||
this.mimeTypes = descriptor.mimeTypes;
|
||||
this.mimeTypeGlobs = this.mimeTypes.map(pattern => glob.parse(pattern));
|
||||
}
|
||||
|
||||
matches(mimeType: string) {
|
||||
let matched = this.mimeTypeGlobs.find(pattern => pattern(mimeType));
|
||||
return matched;
|
||||
}
|
||||
}
|
50
src/vs/workbench/contrib/notebook/common/notebookProvider.ts
Normal file
50
src/vs/workbench/contrib/notebook/common/notebookProvider.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
|
||||
export interface NotebookSelector {
|
||||
readonly filenamePattern?: string;
|
||||
readonly excludeFileNamePattern?: string;
|
||||
}
|
||||
|
||||
export class NotebookProviderInfo {
|
||||
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly selector: readonly NotebookSelector[];
|
||||
|
||||
constructor(descriptor: {
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly selector: readonly NotebookSelector[];
|
||||
}) {
|
||||
this.id = descriptor.id;
|
||||
this.displayName = descriptor.displayName;
|
||||
this.selector = descriptor.selector;
|
||||
}
|
||||
|
||||
matches(resource: URI): boolean {
|
||||
return this.selector.some(selector => NotebookProviderInfo.selectorMatches(selector, resource));
|
||||
}
|
||||
|
||||
static selectorMatches(selector: NotebookSelector, resource: URI): boolean {
|
||||
if (selector.filenamePattern) {
|
||||
if (glob.match(selector.filenamePattern.toLowerCase(), basename(resource).toLowerCase())) {
|
||||
if (selector.excludeFileNamePattern) {
|
||||
if (glob.match(selector.excludeFileNamePattern.toLowerCase(), basename(resource).toLowerCase())) {
|
||||
// should exclude
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
341
src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts
Normal file
341
src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts
Normal file
|
@ -0,0 +1,341 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, CellKind, diff, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { TestCell } from 'vs/workbench/contrib/notebook/test/testNotebookEditor';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
suite('NotebookCommon', () => {
|
||||
test('sortMimeTypes default orders', function () {
|
||||
const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER;
|
||||
|
||||
assert.deepEqual(sortMimeTypes(
|
||||
[
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/svg+xml',
|
||||
'text/markdown',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain'
|
||||
], [], [], defaultDisplayOrder),
|
||||
[
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/svg+xml',
|
||||
'text/markdown',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain'
|
||||
]
|
||||
);
|
||||
|
||||
assert.deepEqual(sortMimeTypes(
|
||||
[
|
||||
'application/json',
|
||||
'text/markdown',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'text/plain',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/svg+xml'
|
||||
], [], [], defaultDisplayOrder),
|
||||
[
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/svg+xml',
|
||||
'text/markdown',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain'
|
||||
]
|
||||
);
|
||||
|
||||
assert.deepEqual(sortMimeTypes(
|
||||
[
|
||||
'text/markdown',
|
||||
'application/json',
|
||||
'text/plain',
|
||||
'image/jpeg',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/png',
|
||||
'image/svg+xml'
|
||||
], [], [], defaultDisplayOrder),
|
||||
[
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/svg+xml',
|
||||
'text/markdown',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('sortMimeTypes document orders', function () {
|
||||
const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER;
|
||||
assert.deepEqual(sortMimeTypes(
|
||||
[
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/svg+xml',
|
||||
'text/markdown',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain'
|
||||
], [],
|
||||
[
|
||||
'text/markdown',
|
||||
'text/html',
|
||||
'application/json'
|
||||
], defaultDisplayOrder),
|
||||
[
|
||||
'text/markdown',
|
||||
'text/html',
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'image/svg+xml',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain'
|
||||
]
|
||||
);
|
||||
|
||||
assert.deepEqual(sortMimeTypes(
|
||||
[
|
||||
'text/markdown',
|
||||
'application/json',
|
||||
'text/plain',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/svg+xml',
|
||||
'image/jpeg',
|
||||
'image/png'
|
||||
], [],
|
||||
[
|
||||
'text/html',
|
||||
'text/markdown',
|
||||
'application/json'
|
||||
], defaultDisplayOrder),
|
||||
[
|
||||
'text/html',
|
||||
'text/markdown',
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'image/svg+xml',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('sortMimeTypes user orders', function () {
|
||||
const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER;
|
||||
assert.deepEqual(sortMimeTypes(
|
||||
[
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/svg+xml',
|
||||
'text/markdown',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain'
|
||||
],
|
||||
[
|
||||
'image/png',
|
||||
'text/plain',
|
||||
],
|
||||
[
|
||||
'text/markdown',
|
||||
'text/html',
|
||||
'application/json'
|
||||
], defaultDisplayOrder),
|
||||
[
|
||||
'image/png',
|
||||
'text/plain',
|
||||
'text/markdown',
|
||||
'text/html',
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'image/svg+xml',
|
||||
'image/jpeg',
|
||||
]
|
||||
);
|
||||
|
||||
assert.deepEqual(sortMimeTypes(
|
||||
[
|
||||
'text/markdown',
|
||||
'application/json',
|
||||
'text/plain',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'image/svg+xml',
|
||||
'image/jpeg',
|
||||
'image/png'
|
||||
],
|
||||
[
|
||||
'application/json',
|
||||
'text/html',
|
||||
],
|
||||
[
|
||||
'text/html',
|
||||
'text/markdown',
|
||||
'application/json'
|
||||
], defaultDisplayOrder),
|
||||
[
|
||||
'application/json',
|
||||
'text/html',
|
||||
'text/markdown',
|
||||
'application/javascript',
|
||||
'image/svg+xml',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'text/plain'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('sortMimeTypes glob', function () {
|
||||
const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER;
|
||||
|
||||
// unknown mime types come last
|
||||
assert.deepEqual(sortMimeTypes(
|
||||
[
|
||||
'application/json',
|
||||
'application/vnd-vega.json',
|
||||
'application/vnd-plot.json',
|
||||
'application/javascript',
|
||||
'text/html'
|
||||
], [],
|
||||
[
|
||||
'text/markdown',
|
||||
'text/html',
|
||||
'application/json'
|
||||
], defaultDisplayOrder),
|
||||
[
|
||||
'text/html',
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'application/vnd-vega.json',
|
||||
'application/vnd-plot.json'
|
||||
],
|
||||
'unknown mimetypes keep the ordering'
|
||||
);
|
||||
|
||||
assert.deepEqual(sortMimeTypes(
|
||||
[
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'text/html',
|
||||
'application/vnd-plot.json',
|
||||
'application/vnd-vega.json'
|
||||
], [],
|
||||
[
|
||||
'application/vnd-vega*',
|
||||
'text/markdown',
|
||||
'text/html',
|
||||
'application/json'
|
||||
], defaultDisplayOrder),
|
||||
[
|
||||
'application/vnd-vega.json',
|
||||
'text/html',
|
||||
'application/json',
|
||||
'application/javascript',
|
||||
'application/vnd-plot.json'
|
||||
],
|
||||
'glob *'
|
||||
);
|
||||
});
|
||||
|
||||
test('diff cells', function () {
|
||||
const cells: TestCell[] = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
cells.push(
|
||||
new TestCell('notebook', i, [`var a = ${i};`], 'javascript', CellKind.Code, [])
|
||||
);
|
||||
}
|
||||
|
||||
assert.deepEqual(diff<TestCell>(cells, [], (cell) => {
|
||||
return cells.indexOf(cell) > -1;
|
||||
}), [
|
||||
{
|
||||
start: 0,
|
||||
deleteCount: 5,
|
||||
toInsert: []
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
assert.deepEqual(diff<TestCell>([], cells, (cell) => {
|
||||
return false;
|
||||
}), [
|
||||
{
|
||||
start: 0,
|
||||
deleteCount: 0,
|
||||
toInsert: cells
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
const cellA = new TestCell('notebook', 6, ['var a = 6;'], 'javascript', CellKind.Code, []);
|
||||
const cellB = new TestCell('notebook', 7, ['var a = 7;'], 'javascript', CellKind.Code, []);
|
||||
|
||||
const modifiedCells = [
|
||||
cells[0],
|
||||
cells[1],
|
||||
cellA,
|
||||
cells[3],
|
||||
cellB,
|
||||
cells[4]
|
||||
];
|
||||
|
||||
const splices = diff<TestCell>(cells, modifiedCells, (cell) => {
|
||||
return cells.indexOf(cell) > -1;
|
||||
});
|
||||
|
||||
assert.deepEqual(splices,
|
||||
[
|
||||
{
|
||||
start: 2,
|
||||
deleteCount: 1,
|
||||
toInsert: [cellA]
|
||||
},
|
||||
{
|
||||
start: 4,
|
||||
deleteCount: 0,
|
||||
toInsert: [cellB]
|
||||
}
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
suite('CellUri', function () {
|
||||
|
||||
test('parse, generate', function () {
|
||||
|
||||
const nb = URI.parse('foo:///bar/følder/file.nb');
|
||||
const id = 17;
|
||||
|
||||
const data = CellUri.generate(nb, id);
|
||||
const actual = CellUri.parse(data);
|
||||
assert.ok(Boolean(actual));
|
||||
assert.equal(actual?.handle, id);
|
||||
assert.equal(actual?.notebook.toString(), nb.toString());
|
||||
});
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
|
||||
import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
|
||||
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { withTestNotebook, TestCell } from 'vs/workbench/contrib/notebook/test/testNotebookEditor';
|
||||
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
|
||||
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
|
||||
|
||||
suite('NotebookViewModel', () => {
|
||||
const instantiationService = new TestInstantiationService();
|
||||
const blukEditService = instantiationService.get(IBulkEditService);
|
||||
const undoRedoService = instantiationService.stub(IUndoRedoService, () => { });
|
||||
instantiationService.spy(IUndoRedoService, 'pushElement');
|
||||
|
||||
test('ctor', function () {
|
||||
const notebook = new NotebookTextModel(0, 'notebook', URI.parse('test'));
|
||||
const model = new NotebookEditorModel(notebook);
|
||||
const viewModel = new NotebookViewModel('notebook', model, instantiationService, blukEditService, undoRedoService);
|
||||
assert.equal(viewModel.viewType, 'notebook');
|
||||
});
|
||||
|
||||
test('insert/delete', function () {
|
||||
withTestNotebook(
|
||||
instantiationService,
|
||||
blukEditService,
|
||||
undoRedoService,
|
||||
[
|
||||
[['var a = 1;'], 'javascript', CellKind.Code, []],
|
||||
[['var b = 2;'], 'javascript', CellKind.Code, []]
|
||||
],
|
||||
(editor, viewModel) => {
|
||||
const cell = viewModel.insertCell(1, new TestCell(viewModel.viewType, 0, ['var c = 3;'], 'javascript', CellKind.Code, []), true);
|
||||
assert.equal(viewModel.viewCells.length, 3);
|
||||
assert.equal(viewModel.notebookDocument.cells.length, 3);
|
||||
assert.equal(viewModel.getViewCellIndex(cell), 1);
|
||||
|
||||
viewModel.deleteCell(1, true);
|
||||
assert.equal(viewModel.viewCells.length, 2);
|
||||
assert.equal(viewModel.notebookDocument.cells.length, 2);
|
||||
assert.equal(viewModel.getViewCellIndex(cell), -1);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('index', function () {
|
||||
withTestNotebook(
|
||||
instantiationService,
|
||||
blukEditService,
|
||||
undoRedoService,
|
||||
[
|
||||
[['var a = 1;'], 'javascript', CellKind.Code, []],
|
||||
[['var b = 2;'], 'javascript', CellKind.Code, []]
|
||||
],
|
||||
(editor, viewModel) => {
|
||||
const firstViewCell = viewModel.viewCells[0];
|
||||
const lastViewCell = viewModel.viewCells[viewModel.viewCells.length - 1];
|
||||
|
||||
const insertIndex = viewModel.getViewCellIndex(firstViewCell) + 1;
|
||||
const cell = viewModel.insertCell(insertIndex, new TestCell(viewModel.viewType, 3, ['var c = 3;'], 'javascript', CellKind.Code, []), true);
|
||||
|
||||
const addedCellIndex = viewModel.getViewCellIndex(cell);
|
||||
viewModel.deleteCell(addedCellIndex, true);
|
||||
|
||||
const secondInsertIndex = viewModel.getViewCellIndex(lastViewCell) + 1;
|
||||
const cell2 = viewModel.insertCell(secondInsertIndex, new TestCell(viewModel.viewType, 4, ['var d = 4;'], 'javascript', CellKind.Code, []), true);
|
||||
|
||||
assert.equal(viewModel.viewCells.length, 3);
|
||||
assert.equal(viewModel.notebookDocument.cells.length, 3);
|
||||
assert.equal(viewModel.getViewCellIndex(cell2), 2);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
195
src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts
Normal file
195
src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder';
|
||||
import { CellKind, ICell, IOutput, NotebookCellOutputsSplice, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { NotebookViewModel, IModelDecorationsChangeAccessor } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel';
|
||||
import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
|
||||
import { INotebookEditor, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer';
|
||||
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
|
||||
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
|
||||
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
|
||||
|
||||
export class TestCell implements ICell {
|
||||
uri: URI;
|
||||
private _onDidChangeOutputs = new Emitter<NotebookCellOutputsSplice[]>();
|
||||
onDidChangeOutputs: Event<NotebookCellOutputsSplice[]> = this._onDidChangeOutputs.event;
|
||||
private _isDirty: boolean = false;
|
||||
private _outputs: IOutput[];
|
||||
get outputs(): IOutput[] {
|
||||
return this._outputs;
|
||||
}
|
||||
|
||||
get isDirty() {
|
||||
return this._isDirty;
|
||||
}
|
||||
|
||||
set isDirty(newState: boolean) {
|
||||
this._isDirty = newState;
|
||||
|
||||
}
|
||||
|
||||
constructor(
|
||||
public viewType: string,
|
||||
public handle: number,
|
||||
public source: string[],
|
||||
public language: string,
|
||||
public cellKind: CellKind,
|
||||
outputs: IOutput[]
|
||||
) {
|
||||
this._outputs = outputs;
|
||||
this.uri = CellUri.generate(URI.parse('test:///fake/notebook'), handle);
|
||||
}
|
||||
contentChange(): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
resolveTextBufferFactory(): PieceTreeTextBufferFactory {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class TestNotebookEditor implements INotebookEditor {
|
||||
|
||||
get viewModel() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
constructor(
|
||||
) { }
|
||||
|
||||
setCellSelection(cell: CellViewModel, selection: Range): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
selectElement(cell: CellViewModel): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
moveCellDown(cell: CellViewModel): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
moveCellUp(cell: CellViewModel): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
setSelection(cell: CellViewModel, selection: Range): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
revealRangeInView(cell: CellViewModel, range: Range): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
revealRangeInCenter(cell: CellViewModel, range: Range): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
revealRangeInCenterIfOutsideViewport(cell: CellViewModel, range: Range): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
revealLineInView(cell: CellViewModel, line: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getLayoutInfo(): NotebookLayoutInfo {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
revealLineInCenterIfOutsideViewport(cell: CellViewModel, line: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
revealLineInCenter(cell: CellViewModel, line: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
focus(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
showFind(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
hideFind(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
revealInView(cell: CellViewModel): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
revealInCenter(cell: CellViewModel): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
revealInCenterIfOutsideViewport(cell: CellViewModel): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
async insertNotebookCell(cell: CellViewModel, type: CellKind, direction: 'above' | 'below'): Promise<void> {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
deleteNotebookCell(cell: CellViewModel): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
editNotebookCell(cell: CellViewModel): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
saveNotebookCell(cell: CellViewModel): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
focusNotebookCell(cell: CellViewModel, focusEditor: boolean): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
getActiveCell(): CellViewModel | undefined {
|
||||
// throw new Error('Method not implemented.');
|
||||
return;
|
||||
}
|
||||
layoutNotebookCell(cell: CellViewModel, height: number): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
createInset(cell: CellViewModel, output: IOutput, shadowContent: string, offset: number): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
removeInset(output: IOutput): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
triggerScroll(event: IMouseWheelEvent): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
}
|
||||
getFontInfo(): BareFontInfo | undefined {
|
||||
return BareFontInfo.createFromRawSettings({
|
||||
fontFamily: 'Monaco',
|
||||
}, 1, true);
|
||||
}
|
||||
getOutputRenderer(): OutputRenderer {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export function createTestCellViewModel(instantiationService: IInstantiationService, viewType: string, notebookHandle: number, cellhandle: number, source: string[], language: string, cellKind: CellKind, outputs: IOutput[]) {
|
||||
const mockCell = new TestCell(viewType, cellhandle, source, language, cellKind, outputs);
|
||||
return instantiationService.createInstance(CellViewModel, viewType, notebookHandle, mockCell);
|
||||
}
|
||||
|
||||
export function withTestNotebook(instantiationService: IInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IOutput[]][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel) => void) {
|
||||
const viewType = 'notebook';
|
||||
const editor = new TestNotebookEditor();
|
||||
const notebook = new NotebookTextModel(0, viewType, URI.parse('test'));
|
||||
notebook.cells = cells.map((cell, index) => {
|
||||
return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3]);
|
||||
});
|
||||
const model = new NotebookEditorModel(notebook);
|
||||
const viewModel = new NotebookViewModel(viewType, model, instantiationService, blukEditService, undoRedoService);
|
||||
|
||||
callback(editor, viewModel);
|
||||
|
||||
viewModel.dispose();
|
||||
return;
|
||||
}
|
|
@ -295,7 +295,7 @@ configurationRegistry.registerConfiguration({
|
|||
default: true
|
||||
},
|
||||
'terminal.integrated.allowMnemonics': {
|
||||
markdownDescription: nls.localize('terminal.integrated.allowMnemonics', "Whether to allow menubar mnemonics (eg. alt+f) to trigger the open the menubar. Note that this will cause all alt keystrokes will skip the shell when true."),
|
||||
markdownDescription: nls.localize('terminal.integrated.allowMnemonics', "Whether to allow menubar mnemonics (eg. alt+f) to trigger the open the menubar. Note that this will cause all alt keystrokes will skip the shell when true. This does nothing on macOS."),
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
|
|
|
@ -621,7 +621,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
|
|||
}
|
||||
|
||||
// Skip processing by xterm.js of keyboard events that match menu bar mnemonics
|
||||
if (this._configHelper.config.allowMnemonics && event.altKey) {
|
||||
if (this._configHelper.config.allowMnemonics && !platform.isMacintosh && event.altKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ICodeEditor, isCodeEditor, isDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { CodeEditorServiceImpl } from 'vs/editor/browser/services/codeEditorServiceImpl';
|
||||
import { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { IResourceEditorInput } from 'vs/platform/editor/common/editor';
|
||||
|
@ -32,10 +32,15 @@ export class CodeEditorService extends CodeEditorServiceImpl {
|
|||
return activeTextEditorControl.getModifiedEditor();
|
||||
}
|
||||
|
||||
const activeControl = this.editorService.activeEditorPane?.getControl();
|
||||
if (isCompositeEditor(activeControl) && isCodeEditor(activeControl.activeCodeEditor)) {
|
||||
return activeControl.activeCodeEditor;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
|
||||
async openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
|
||||
|
||||
// Special case: If the active editor is a diff editor and the request to open originates and
|
||||
// targets the modified side of it, we just apply the request there to prevent opening the modified
|
||||
|
@ -55,7 +60,7 @@ export class CodeEditorService extends CodeEditorServiceImpl {
|
|||
const textOptions = TextEditorOptions.create(input.options);
|
||||
textOptions.apply(targetEditor, ScrollType.Smooth);
|
||||
|
||||
return Promise.resolve(targetEditor);
|
||||
return targetEditor;
|
||||
}
|
||||
|
||||
// Open using our normal editor service
|
||||
|
|
|
@ -21,7 +21,7 @@ import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpen
|
|||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { coalesce, distinct } from 'vs/base/common/arrays';
|
||||
import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
|
@ -400,6 +400,9 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
|||
if (isCodeEditor(activeControl) || isDiffEditor(activeControl)) {
|
||||
return activeControl;
|
||||
}
|
||||
if (isCompositeEditor(activeControl) && isCodeEditor(activeControl.activeCodeEditor)) {
|
||||
return activeControl.activeCodeEditor;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
|
@ -251,7 +251,7 @@ export class ProgressService extends Disposable implements IProgressService {
|
|||
return toDisposable(() => promiseResolve());
|
||||
};
|
||||
|
||||
const createNotification = (message: string, increment?: number): INotificationHandle => {
|
||||
const createNotification = (message: string, silent: boolean, increment?: number): INotificationHandle => {
|
||||
const notificationDisposables = new DisposableStore();
|
||||
|
||||
const primaryActions = options.primaryActions ? Array.from(options.primaryActions) : [];
|
||||
|
@ -294,7 +294,8 @@ export class ProgressService extends Disposable implements IProgressService {
|
|||
message,
|
||||
source: options.source,
|
||||
actions: { primary: primaryActions, secondary: secondaryActions },
|
||||
progress: typeof increment === 'number' && increment >= 0 ? { total: 100, worked: increment } : { infinite: true }
|
||||
progress: typeof increment === 'number' && increment >= 0 ? { total: 100, worked: increment } : { infinite: true },
|
||||
silent
|
||||
});
|
||||
|
||||
// Switch to window based progress once the notification
|
||||
|
@ -302,8 +303,7 @@ export class ProgressService extends Disposable implements IProgressService {
|
|||
// Remove that window based progress once the notification
|
||||
// shows again.
|
||||
let windowProgressDisposable: IDisposable | undefined = undefined;
|
||||
notificationDisposables.add(notification.onDidChangeVisibility(visible => {
|
||||
|
||||
const onVisibilityChange = (visible: boolean) => {
|
||||
// Clear any previous running window progress
|
||||
dispose(windowProgressDisposable);
|
||||
|
||||
|
@ -311,7 +311,11 @@ export class ProgressService extends Disposable implements IProgressService {
|
|||
if (!visible && !progressStateModel.done) {
|
||||
windowProgressDisposable = createWindowProgress();
|
||||
}
|
||||
}));
|
||||
};
|
||||
notificationDisposables.add(notification.onDidChangeVisibility(onVisibilityChange));
|
||||
if (silent) {
|
||||
onVisibilityChange(false);
|
||||
}
|
||||
|
||||
// Clear upon dispose
|
||||
Event.once(notification.onDidClose)(() => notificationDisposables.dispose());
|
||||
|
@ -346,10 +350,10 @@ export class ProgressService extends Disposable implements IProgressService {
|
|||
// create notification now or after a delay
|
||||
if (typeof options.delay === 'number' && options.delay > 0) {
|
||||
if (typeof notificationTimeout !== 'number') {
|
||||
notificationTimeout = setTimeout(() => notificationHandle = createNotification(titleAndMessage!, step?.increment), options.delay);
|
||||
notificationTimeout = setTimeout(() => notificationHandle = createNotification(titleAndMessage!, !!options.silent, step?.increment), options.delay);
|
||||
}
|
||||
} else {
|
||||
notificationHandle = createNotification(titleAndMessage, step?.increment);
|
||||
notificationHandle = createNotification(titleAndMessage, !!options.silent, step?.increment);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -142,6 +142,9 @@ import 'vs/workbench/contrib/preferences/browser/preferences.contribution';
|
|||
import 'vs/workbench/contrib/preferences/browser/keybindingsEditorContribution';
|
||||
import 'vs/workbench/contrib/preferences/browser/preferencesSearch';
|
||||
|
||||
// Notebook
|
||||
import 'vs/workbench/contrib/notebook/browser/notebook.contribution';
|
||||
|
||||
// Logs
|
||||
import 'vs/workbench/contrib/logs/common/logs.contribution';
|
||||
|
||||
|
|
Loading…
Reference in a new issue