Notebook 文档同步

Notebook 正变得越来越流行。通过向语言服务器协议添加对它们的支持,笔记本编辑器可以分别在 NotebookNotebook cell 中重用服务器提供的语言智能。为了重用协议部分,从而重用服务器实现,在 LSP 中按以下方式对笔记本进行建模:

  • Notebook document: 通常存储在磁盘文件中的笔记本单元的集合。笔记本文档具有类型,可以使用资源 URI 进行唯一标识。

  • notebook cell: 保存实际的文本内容。单元格有类型(代码或 markdown)。单元格的实际文本内容存储在文本文档中,该文本文档可以像所有其他文本文档一样同步到服务器。单元格文本文档具有 URI,但服务器不应依赖此 URI 的任何格式,因为如何创建这些 URI 取决于客户端。URI 在所有 notebook cell 中必须是唯一的,因此可用于唯一标识 notebook cellnotebook cell 的文本文档。

这两个概念的定义如下:

/**
 * A notebook document.
 *
 * @since 3.17.0
 */
export interface NotebookDocument {

	/**
	 * The notebook document's URI.
	 */
	uri: URI;

	/**
	 * The type of the notebook.
	 */
	notebookType: string;

	/**
	 * The version number of this document (it will increase after each
	 * change, including undo/redo).
	 */
	version: integer;

	/**
	 * Additional metadata stored with the notebook
	 * document.
	 */
	metadata?: LSPObject;

	/**
	 * The cells of a notebook.
	 */
	cells: NotebookCell[];
}
/**
 * A notebook cell.
 *
 * A cell's document URI must be unique across ALL notebook
 * cells and can therefore be used to uniquely identify a
 * notebook cell or the cell's text document.
 *
 * @since 3.17.0
 */
export interface NotebookCell {

	/**
	 * The cell's kind
	 */
	kind: NotebookCellKind;

	/**
	 * The URI of the cell's text document
	 * content.
	 */
	document: DocumentUri;

	/**
	 * Additional metadata stored with the cell.
	 */
	metadata?: LSPObject;

	/**
	 * Additional execution summary information
	 * if supported by the client.
	 */
	executionSummary?: ExecutionSummary;
}
/**
 * A notebook cell kind.
 *
 * @since 3.17.0
 */
export namespace NotebookCellKind {

	/**
	 * A markup-cell is formatted source that is used for display.
	 */
	export const Markup: 1 = 1;

	/**
	 * A code-cell is source code.
	 */
	export const Code: 2 = 2;
}
export interface ExecutionSummary {
	/**
	 * A strict monotonically increasing value
	 * indicating the execution order of a cell
	 * inside a notebook.
	 */
	executionOrder: uinteger;

	/**
	 * Whether the execution was successful or
	 * not if known by the client.
	 */
	success?: boolean;
}

接下来,我们将介绍如何将笔记本、笔记本单元格和笔记本单元格的内容同步到语言服务器。同步单元格的文本内容相对容易,因为客户端应将它们建模为文本文档。但是,由于笔记本单元的文本文档的 URI 应该是不透明的,因此服务器无法知道其 schemepath。所知道的只是笔记本文档本身。因此,我们为笔记本单元格文档引入了一个特殊的过滤器:

/**
 * A notebook cell text document filter denotes a cell text
 * document by different properties.
 *
 * @since 3.17.0
 */
export interface NotebookCellTextDocumentFilter {
	/**
	 * A filter that matches against the notebook
	 * containing the notebook cell. If a string
	 * value is provided it matches against the
	 * notebook type. '*' matches every notebook.
	 */
	notebook: string | NotebookDocumentFilter;

	/**
	 * A language id like `python`.
	 *
	 * Will be matched against the language id of the
	 * notebook cell document. '*' matches every language.
	 */
	language?: string;
}
/**
 * A notebook document filter denotes a notebook document by
 * different properties.
 *
 * @since 3.17.0
 */
export type NotebookDocumentFilter = {
	/** The type of the enclosing notebook. */
	notebookType?: string;

	/** A Uri [scheme](#Uri.scheme), like `file` or `untitled`. */
	scheme?: string;

	/** A glob pattern. */
	pattern?: string;
}

NotebookDocumentFilter 中的 notebookType, schemepattern 至少存在一个必填,这里为了简化,都定义为了可选。

给定这些结构,可以按如下方式识别 Jupyter Notebook 中存储在磁盘上的 Python 单元文档,该文档的路径中包含 books1 的文件夹中;

{
	notebook: {
		scheme: 'file',
		pattern '**/books1/**',
		notebookType: 'jupyter-notebook'
	},
	language: 'python'
}

NotebookCellTextDocumentFilter 可用于为某些请求(如代码完成或悬停)注册提供程序。如果注册了此类提供程序,则客户端将使用单元格文本文档的 URI 作为文档 URI 向服务器发送相应的 textDocument/* 请求。

在某些情况下,仅仅知道单元格的文本内容不足以让服务器推理单元格内容并提供良好的语言智能。有时需要了解笔记本文档的所有单元格,包括笔记本文档本身。考虑一个笔记本,它有两个 JavaScript 单元格,其中包含以下内容:

单元格一:

function add(a, b) {
	return a + b;
}

单元格二:

add/*<cursor>*/;

在标记的光标位置的第二个单元格中请求代码辅助应该建议函数 add,这只有在服务器知道单元格 1 和单元格 2 并且知道它们属于同一个笔记本文档时才有可能。

因此,在同步单元格文本内容时,该协议将支持两种模式:

  • cellContent: 在此模式下,仅使用标准 textDocument/did* 通知将单元格文本内容同步到服务器。没有 Notebook document,也没有单元格结构同步。此模式对于 Notebook 服务器便于采用,因为服务器可以重用大部分实现逻辑。

  • notebook: 在此模式下,Notebook 文档、Notebook 单元格和 Notebook 单元格文本内容将同步到服务器。为了允许服务器创建与 Notebook 文档一致的视图,不使用标准 textDocument/did* 通知同步单元格文本内容。相反,它使用特殊的 notebookDocument/did* 通知进行同步。这可确保单元格及其文本内容使用一个 open、change 或 close 事件到达服务器。

要请求单元格内容,只能使用普通的文档选择器。例如,选择器 [{ language: 'python' }] 会将 Python Notebook 文档单元格同步到服务器。但是,由于这也可能同步不需要的文档,因此文档筛选器也可以是 NotebookCellTextDocumentFilter。所以 { notebook: { scheme: 'file', notebookType: 'jupyter-notebook' }, language: 'python' } 同步存储在磁盘上的 Jupyter Notebook 中的所有 Python 单元格。

若要同步整个笔记本文档,服务器在其服务器能力中提供 notebookDocumentSync。例如:

{
	notebookDocumentSync: {
		notebookSelector: [
			{
				notebook: { scheme: 'file', notebookType: 'jupyter-notebook' },
				cells: [{ language: 'python' }]
			}
		]
	}
}

如果 Notebook 存储在磁盘上,则将 Notebook(包括所有 Python 单元)同步到服务器。

客户端能力(Client capability):

  • 属性路径: notebookDocument.synchronization
  • 属性类型: NotebookDocumentSyncClientCapabilities, 定义如下:
/**
 * Notebook specific client capabilities.
 *
 * @since 3.17.0
 */
export interface NotebookDocumentSyncClientCapabilities {

	/**
	 * Whether implementation supports dynamic registration. If this is
	 * set to `true` the client supports the new
	 * `(NotebookDocumentSyncRegistrationOptions & NotebookDocumentSyncOptions)`
	 * return value for the corresponding server capability as well.
	 */
	dynamicRegistration?: boolean;

	/**
	 * The client supports sending execution summary data per cell.
	 */
	executionSummarySupport?: boolean;
}

服务端能力(Server capability):

  • 属性路径: notebookDocumentSync
  • 属性类型: NotebookDocumentSyncOptions | NotebookDocumentSyncRegistrationOptions, NotebookDocumentOptions 定义如下:
/**
 * Options specific to a notebook plus its cells
 * to be synced to the server.
 *
 * If a selector provides a notebook document
 * filter but no cell selector all cells of a
 * matching notebook document will be synced.
 *
 * If a selector provides no notebook document
 * filter but only a cell selector all notebook
 * documents that contain at least one matching
 * cell will be synced.
 *
 * @since 3.17.0
 */
export interface NotebookDocumentSyncOptions {
	/**
	 * The notebooks to be synced
	 */
	notebookSelector: ({
		/**
		 * The notebook to be synced. If a string
		 * value is provided it matches against the
		 * notebook type. '*' matches every notebook.
		 */
		notebook: string | NotebookDocumentFilter;

		/**
		 * The cells of the matching notebook to be synced.
		 */
		cells?: { language: string }[];
	} | {
		/**
		 * The notebook to be synced. If a string
		 * value is provided it matches against the
		 * notebook type. '*' matches every notebook.
		 */
		notebook?: string | NotebookDocumentFilter;

		/**
		 * The cells of the matching notebook to be synced.
		 */
		cells: { language: string }[];
	})[];

	/**
	 * Whether save notification should be forwarded to
	 * the server. Will only be honored if mode === `notebook`.
	 */
	save?: boolean;
}

注册选项(Registration Options): notebookDocumentSyncRegistrationOptions, 定义如下:

/**
 * Registration options specific to a notebook.
 *
 * @since 3.17.0
 */
export interface NotebookDocumentSyncRegistrationOptions extends
	NotebookDocumentSyncOptions, StaticRegistrationOptions {
}

DidOpenNotebookDocument 通知

打开 Notebook 文档时,打开通知将从客户端发送到服务器。仅当服务器提供 notebookDocumentSync 能力时,客户端才会发送它。

通知(Notification):

  • method: "notebookDocument/didOpen"
  • params: DidOpenNotebookDocumentParams, 定义如下:
/**
 * The params sent in an open notebook document notification.
 *
 * @since 3.17.0
 */
export interface DidOpenNotebookDocumentParams {

	/**
	 * The notebook document that got opened.
	 */
	notebookDocument: NotebookDocument;

	/**
	 * The text documents that represent the content
	 * of a notebook cell.
	 */
	cellTextDocuments: TextDocumentItem[];
}

DidChangeNotebookDocument 通知

当笔记本文档发生更改时,更改通知将从客户端发送到服务器。仅当服务器提供 notebookDocumentSync 能力时,客户端才会发送它。

通知(Notification):

  • method: "notebookDocument/didChange"
  • params: DidChangeNotebookDocumentParams, 定义如下:
/**
 * The params sent in a change notebook document notification.
 *
 * @since 3.17.0
 */
export interface DidChangeNotebookDocumentParams {

	/**
	 * The notebook document that did change. The version number points
	 * to the version after all provided changes have been applied.
	 */
	notebookDocument: VersionedNotebookDocumentIdentifier;

	/**
	 * The actual changes to the notebook document.
	 *
	 * The change describes single state change to the notebook document.
	 * So it moves a notebook document, its cells and its cell text document
	 * contents from state S to S'.
	 *
	 * To mirror the content of a notebook using change events use the
	 * following approach:
	 * - start with the same initial content
	 * - apply the 'notebookDocument/didChange' notifications in the order
	 *   you receive them.
	 */
	change: NotebookDocumentChangeEvent;
}
/**
 * A versioned notebook document identifier.
 *
 * @since 3.17.0
 */
export interface VersionedNotebookDocumentIdentifier {

	/**
	 * The version number of this notebook document.
	 */
	version: integer;

	/**
	 * The notebook document's URI.
	 */
	uri: URI;
}
/**
 * A change event for a notebook document.
 *
 * @since 3.17.0
 */
export interface NotebookDocumentChangeEvent {
	/**
	 * The changed meta data if any.
	 */
	metadata?: LSPObject;

	/**
	 * Changes to cells
	 */
	cells?: {
		/**
		 * Changes to the cell structure to add or
		 * remove cells.
		 */
		structure?: {
			/**
			 * The change to the cell array.
			 */
			array: NotebookCellArrayChange;

			/**
			 * Additional opened cell text documents.
			 */
			didOpen?: TextDocumentItem[];

			/**
			 * Additional closed cell text documents.
			 */
			didClose?: TextDocumentIdentifier[];
		};

		/**
		 * Changes to notebook cells properties like its
		 * kind, execution summary or metadata.
		 */
		data?: NotebookCell[];

		/**
		 * Changes to the text content of notebook cells.
		 */
		textContent?: {
			document: VersionedTextDocumentIdentifier;
			changes: TextDocumentContentChangeEvent[];
		}[];
	};
}
/**
 * A change describing how to move a `NotebookCell`
 * array from state S to S'.
 *
 * @since 3.17.0
 */
export interface NotebookCellArrayChange {
	/**
	 * The start offset of the cell that changed.
	 */
	start: uinteger;

	/**
	 * The deleted cells
	 */
	deleteCount: uinteger;

	/**
	 * The new cells, if any
	 */
	cells?: NotebookCell[];
}

DidSaveNotebookDocument 通知

保存笔记本文档时,保存通知将从客户端发送到服务器。仅当服务器提供 notebookDocumentSync 能力时,客户端才会发送它。

通知(Notification):

  • method: "notebookDocument/didSave"
  • params: DidSaveNotebookDocumentParams, 定义如下:
/**
 * The params sent in a save notebook document notification.
 *
 * @since 3.17.0
 */
export interface DidSaveNotebookDocumentParams {
	/**
	 * The notebook document that got saved.
	 */
	notebookDocument: NotebookDocumentIdentifier;
}

DidCloseNotebookDocument 通知

关闭笔记本文档时,关闭通知将从客户端发送到服务器。仅当服务器提供 notebookDocumentSync 能力时,客户端才会发送它。

通知(Notification):

  • method: "notebookDocument/didClose"
  • params: DidCloseNotebookDocumentParams, 定义如下:
/**
 * The params sent in a close notebook document notification.
 *
 * @since 3.17.0
 */
export interface DidCloseNotebookDocumentParams {

	/**
	 * The notebook document that got closed.
	 */
	notebookDocument: NotebookDocumentIdentifier;

	/**
	 * The text documents that represent the content
	 * of a notebook cell that got closed.
	 */
	cellTextDocuments: TextDocumentIdentifier[];
}
/**
 * A literal to identify a notebook document in the client.
 *
 * @since 3.17.0
 */
export interface NotebookDocumentIdentifier {
	/**
	 * The notebook document's URI.
	 */
	uri: URI;
}