diff --git a/javascript/src/annotations.ts b/javascript/src/annotations.ts new file mode 100644 index 0000000..4eef28d --- /dev/null +++ b/javascript/src/annotations.ts @@ -0,0 +1,71 @@ +/* ----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +import type { JSONValue } from '@lumino/coreutils'; +import type { ISignal } from '@lumino/signaling'; + +/** + * Generic annotation change + */ +export type AnnotationsChange = { + oldValue?: T; + newValue?: T; +}; + +/** + * Annotation interface. + */ +export interface IAnnotation { + sender: string; + pos: JSONValue; + content: JSONValue; +} + +/** + * Annotations interface. + * This interface must be implemented by the shared documents that want + * to include annotations. + */ +export interface IAnnotations { + /** + * The annotation changed signal. + */ + readonly annotationChanged: ISignal>; + + /** + * Return an iterator that yields every annotation key. + */ + readonly annotations: Array; + + /** + * Get the value for an annotation + * + * @param key Key to get + */ + getAnnotation(key: string): T | undefined; + + /** + * Set the value of an annotation + * + * @param key Key to set + * @param value New value + */ + setAnnotation(key: string, value: T): void; + + /** + * Update the value of an existing annotation + * + * @param key Key to update + * @param value New value + */ + updateAnnotation(key: string, value: T): void; + + /** + * Delete an annotation + * + * @param key Key to delete + */ + deleteAnnotation(key: string): void; +} diff --git a/javascript/src/api.ts b/javascript/src/api.ts index 6daad0f..d8fe747 100644 --- a/javascript/src/api.ts +++ b/javascript/src/api.ts @@ -23,6 +23,8 @@ import type { import type { IObservableDisposable } from '@lumino/disposable'; import type { ISignal } from '@lumino/signaling'; +import type { IAnnotation, IAnnotations } from './annotations.js'; + /** * Changes on Sequence-like data are expressed as Quill-inspired deltas. * @@ -95,6 +97,11 @@ export interface ISharedDocument extends ISharedBase { */ readonly state: JSONObject; + /** + * The changed signal. + */ + readonly changed: ISignal; + /** * Get the value for a state attribute * @@ -109,11 +116,6 @@ export interface ISharedDocument extends ISharedBase { * @param value New attribute value */ setState(key: string, value: JSONValue): void; - - /** - * The changed signal. - */ - readonly changed: ISignal; } /** @@ -399,8 +401,10 @@ export namespace SharedCell { * Implements an API for nbformat.IBaseCell. */ export interface ISharedBaseCell< - Metadata extends nbformat.IBaseCellMetadata = nbformat.IBaseCellMetadata -> extends ISharedText { + Metadata extends nbformat.IBaseCellMetadata = nbformat.IBaseCellMetadata, + Annotation extends IAnnotation = IAnnotation +> extends ISharedText, + IAnnotations { /** * The type of the cell. */ diff --git a/javascript/src/index.ts b/javascript/src/index.ts index 68d6b03..5e1e3f3 100644 --- a/javascript/src/index.ts +++ b/javascript/src/index.ts @@ -10,6 +10,8 @@ export * from './api.js'; export * from './utils.js'; +export * from './annotations.js'; + export * from './ytext.js'; export * from './ydocument.js'; export * from './yfile.js'; diff --git a/javascript/src/ycell.ts b/javascript/src/ycell.ts index 969100a..7bfb50a 100644 --- a/javascript/src/ycell.ts +++ b/javascript/src/ycell.ts @@ -6,8 +6,10 @@ import type * as nbformat from '@jupyterlab/nbformat'; import { JSONExt, JSONObject, PartialJSONValue, UUID } from '@lumino/coreutils'; import { ISignal, Signal } from '@lumino/signaling'; + import { Awareness } from 'y-protocols/awareness'; import * as Y from 'yjs'; + import type { CellChange, IMapChange, @@ -18,6 +20,8 @@ import type { ISharedRawCell, SharedCell } from './api.js'; +import type { AnnotationsChange, IAnnotation } from './annotations.js'; + import { IYText } from './ytext.js'; import { YNotebook } from './ynotebook.js'; @@ -128,7 +132,7 @@ export const createStandaloneCell = (cell: SharedCell.Cell): YCellType => createCell(cell); export class YBaseCell - implements ISharedBaseCell, IYText + implements ISharedBaseCell, IYText { /** * Create a new YCell that works standalone. It cannot be @@ -227,6 +231,21 @@ export class YBaseCell return this._isDisposed; } + /** + * The annotation changed signal. + */ + get annotationChanged(): ISignal> { + return this._annotationChanged; + } + + /** + * Return an iterator that yields every annotation key. + */ + get annotations(): Array { + const annotation = this._ymetadata.get('annotations'); + return Object.keys(annotation); + } + /** * Whether the cell is standalone or not. * @@ -370,6 +389,66 @@ export class YBaseCell Signal.clearData(this); } + /** + * Get the value for an annotation + * + * @param key Key to get + */ + getAnnotation(key: string): IAnnotation | undefined { + const annotations = this._ymetadata.get('annotations'); + if (annotations && key in annotations) { + return JSONExt.deepCopy(annotations[key]); + } else { + return undefined; + } + } + + /** + * Set the value of an annotation + * + * @param key Key to set + * @param value New value + */ + setAnnotation(key: string, value: IAnnotation): void { + const clone = JSONExt.deepCopy(value as any); + const annotations = this._ymetadata.get('annotations') ?? {}; + annotations[key] = clone; + this._ymetadata.set('annotations', annotations); + } + + /** + * Update the value of an existing annotation + * + * @param key Key to update + * @param value New value + */ + updateAnnotation(key: string, value: Partial): void { + const annotations = this._ymetadata.get('annotations'); + if (!annotations || !(key in annotations)) { + return; + } + + const annotation = annotations[key]; + const clone = JSONExt.deepCopy(value as any); + for (const [key, value] of Object.entries(clone)) { + annotation[key] = value; + } + this._ymetadata.set('annotations', { ...annotations, key: annotation }); + } + + /** + * Delete an annotation + * + * @param key Key to delete + */ + deleteAnnotation(key: string): void { + const annotations = this._ymetadata.get('annotations'); + if (annotations && key in annotations) { + delete annotations[key]; + this._ymetadata.set('annotations', annotations); + } + } + /** * Get cell id. * @@ -661,18 +740,24 @@ export class YBaseCell }; protected _metadataChanged = new Signal(this); + /** * The notebook that this cell belongs to. */ protected _notebook: YNotebook | null = null; + private _awareness: Awareness | null; - private _changed = new Signal(this); - private _disposed = new Signal(this); private _isDisposed = false; private _prevSourceLength: number; private _undoManager: Y.UndoManager | null = null; private _ymetadata: Y.Map; private _ysource: Y.Text; + + private _disposed = new Signal(this); + private _changed = new Signal(this); + private _annotationChanged = new Signal>( + this + ); } /**