"use strict"; /* -------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ Object.defineProperty(exports, "__esModule", { value: true }); exports.NotebookDocumentSyncFeature = void 0; const vscode = require("vscode"); const minimatch = require("minimatch"); const proto = require("vscode-languageserver-protocol"); const UUID = require("./utils/uuid"); const Is = require("./utils/is"); function ensure(target, key) { if (target[key] === void 0) { target[key] = {}; } return target[key]; } var Converter; (function (Converter) { let c2p; (function (c2p) { function asVersionedNotebookDocumentIdentifier(notebookDocument, base) { return { version: notebookDocument.version, uri: base.asUri(notebookDocument.uri) }; } c2p.asVersionedNotebookDocumentIdentifier = asVersionedNotebookDocumentIdentifier; function asNotebookDocument(notebookDocument, cells, base) { const result = proto.NotebookDocument.create(base.asUri(notebookDocument.uri), notebookDocument.notebookType, notebookDocument.version, asNotebookCells(cells, base)); if (Object.keys(notebookDocument.metadata).length > 0) { result.metadata = asMetadata(notebookDocument.metadata); } return result; } c2p.asNotebookDocument = asNotebookDocument; function asNotebookCells(cells, base) { return cells.map(cell => asNotebookCell(cell, base)); } c2p.asNotebookCells = asNotebookCells; function asMetadata(metadata) { const seen = new Set(); return deepCopy(seen, metadata); } c2p.asMetadata = asMetadata; function asNotebookCell(cell, base) { const result = proto.NotebookCell.create(asNotebookCellKind(cell.kind), base.asUri(cell.document.uri)); if (Object.keys(cell.metadata).length > 0) { result.metadata = asMetadata(cell.metadata); } if (cell.executionSummary !== undefined && (Is.number(cell.executionSummary.executionOrder) && Is.boolean(cell.executionSummary.success))) { result.executionSummary = { executionOrder: cell.executionSummary.executionOrder, success: cell.executionSummary.success }; } return result; } c2p.asNotebookCell = asNotebookCell; function asNotebookCellKind(kind) { switch (kind) { case vscode.NotebookCellKind.Markup: return proto.NotebookCellKind.Markup; case vscode.NotebookCellKind.Code: return proto.NotebookCellKind.Code; } } function deepCopy(seen, value) { if (seen.has(value)) { throw new Error(`Can't deep copy cyclic structures.`); } if (Array.isArray(value)) { const result = []; for (const elem of value) { if (elem !== null && typeof elem === 'object' || Array.isArray(elem)) { result.push(deepCopy(seen, elem)); } else { if (elem instanceof RegExp) { throw new Error(`Can't transfer regular expressions to the server`); } result.push(elem); } } return result; } else { const props = Object.keys(value); const result = Object.create(null); for (const prop of props) { const elem = value[prop]; if (elem !== null && typeof elem === 'object' || Array.isArray(elem)) { result[prop] = deepCopy(seen, elem); } else { if (elem instanceof RegExp) { throw new Error(`Can't transfer regular expressions to the server`); } result[prop] = elem; } } return result; } } function asTextContentChange(event, base) { const params = base.asChangeTextDocumentParams(event, event.document.uri, event.document.version); return { document: params.textDocument, changes: params.contentChanges }; } c2p.asTextContentChange = asTextContentChange; function asNotebookDocumentChangeEvent(event, base) { const result = Object.create(null); if (event.metadata) { result.metadata = Converter.c2p.asMetadata(event.metadata); } if (event.cells !== undefined) { const cells = Object.create(null); const changedCells = event.cells; if (changedCells.structure) { cells.structure = { array: { start: changedCells.structure.array.start, deleteCount: changedCells.structure.array.deleteCount, cells: changedCells.structure.array.cells !== undefined ? changedCells.structure.array.cells.map(cell => Converter.c2p.asNotebookCell(cell, base)) : undefined }, didOpen: changedCells.structure.didOpen !== undefined ? changedCells.structure.didOpen.map(cell => base.asOpenTextDocumentParams(cell.document).textDocument) : undefined, didClose: changedCells.structure.didClose !== undefined ? changedCells.structure.didClose.map(cell => base.asCloseTextDocumentParams(cell.document).textDocument) : undefined }; } if (changedCells.data !== undefined) { cells.data = changedCells.data.map(cell => Converter.c2p.asNotebookCell(cell, base)); } if (changedCells.textContent !== undefined) { cells.textContent = changedCells.textContent.map(event => Converter.c2p.asTextContentChange(event, base)); } if (Object.keys(cells).length > 0) { result.cells = cells; } } return result; } c2p.asNotebookDocumentChangeEvent = asNotebookDocumentChangeEvent; })(c2p = Converter.c2p || (Converter.c2p = {})); })(Converter || (Converter = {})); var $NotebookCell; (function ($NotebookCell) { function computeDiff(originalCells, modifiedCells, compareMetadata) { const originalLength = originalCells.length; const modifiedLength = modifiedCells.length; let startIndex = 0; while (startIndex < modifiedLength && startIndex < originalLength && equals(originalCells[startIndex], modifiedCells[startIndex], compareMetadata)) { startIndex++; } if (startIndex < modifiedLength && startIndex < originalLength) { let originalEndIndex = originalLength - 1; let modifiedEndIndex = modifiedLength - 1; while (originalEndIndex >= 0 && modifiedEndIndex >= 0 && equals(originalCells[originalEndIndex], modifiedCells[modifiedEndIndex], compareMetadata)) { originalEndIndex--; modifiedEndIndex--; } const deleteCount = (originalEndIndex + 1) - startIndex; const newCells = startIndex === modifiedEndIndex + 1 ? undefined : modifiedCells.slice(startIndex, modifiedEndIndex + 1); return newCells !== undefined ? { start: startIndex, deleteCount, cells: newCells } : { start: startIndex, deleteCount }; } else if (startIndex < modifiedLength) { return { start: startIndex, deleteCount: 0, cells: modifiedCells.slice(startIndex) }; } else if (startIndex < originalLength) { return { start: startIndex, deleteCount: originalLength - startIndex }; } else { // The two arrays are the same. return undefined; } } $NotebookCell.computeDiff = computeDiff; /** * We only sync kind, document, execution and metadata to the server. So we only need to compare those. */ function equals(one, other, compareMetaData = true) { if (one.kind !== other.kind || one.document.uri.toString() !== other.document.uri.toString() || one.document.languageId !== other.document.languageId || !equalsExecution(one.executionSummary, other.executionSummary)) { return false; } return !compareMetaData || (compareMetaData && equalsMetadata(one.metadata, other.metadata)); } function equalsExecution(one, other) { if (one === other) { return true; } if (one === undefined || other === undefined) { return false; } return one.executionOrder === other.executionOrder && one.success === other.success && equalsTiming(one.timing, other.timing); } function equalsTiming(one, other) { if (one === other) { return true; } if (one === undefined || other === undefined) { return false; } return one.startTime === other.startTime && one.endTime === other.endTime; } function equalsMetadata(one, other) { if (one === other) { return true; } if (one === null || one === undefined || other === null || other === undefined) { return false; } if (typeof one !== typeof other) { return false; } if (typeof one !== 'object') { return false; } const oneArray = Array.isArray(one); const otherArray = Array.isArray(other); if (oneArray !== otherArray) { return false; } if (oneArray && otherArray) { if (one.length !== other.length) { return false; } for (let i = 0; i < one.length; i++) { if (!equalsMetadata(one[i], other[i])) { return false; } } } if (isObjectLiteral(one) && isObjectLiteral(other)) { const oneKeys = Object.keys(one); const otherKeys = Object.keys(other); if (oneKeys.length !== otherKeys.length) { return false; } oneKeys.sort(); otherKeys.sort(); if (!equalsMetadata(oneKeys, otherKeys)) { return false; } for (let i = 0; i < oneKeys.length; i++) { const prop = oneKeys[i]; if (!equalsMetadata(one[prop], other[prop])) { return false; } } return true; } return false; } function isObjectLiteral(value) { return value !== null && typeof value === 'object'; } $NotebookCell.isObjectLiteral = isObjectLiteral; })($NotebookCell || ($NotebookCell = {})); var $NotebookDocumentFilter; (function ($NotebookDocumentFilter) { function matchNotebook(filter, notebookDocument) { if (typeof filter === 'string') { return filter === '*' || notebookDocument.notebookType === filter; } if (filter.notebookType !== undefined && filter.notebookType !== '*' && notebookDocument.notebookType !== filter.notebookType) { return false; } const uri = notebookDocument.uri; if (filter.scheme !== undefined && filter.scheme !== '*' && uri.scheme !== filter.scheme) { return false; } if (filter.pattern !== undefined) { const matcher = new minimatch.Minimatch(filter.pattern, { noext: true }); if (!matcher.makeRe()) { return false; } if (!matcher.match(uri.fsPath)) { return false; } } return true; } $NotebookDocumentFilter.matchNotebook = matchNotebook; })($NotebookDocumentFilter || ($NotebookDocumentFilter = {})); var $NotebookDocumentSyncOptions; (function ($NotebookDocumentSyncOptions) { function asDocumentSelector(options) { const selector = options.notebookSelector; const result = []; for (const element of selector) { const notebookType = (typeof element.notebook === 'string' ? element.notebook : element.notebook?.notebookType) ?? '*'; const scheme = (typeof element.notebook === 'string') ? undefined : element.notebook?.scheme; const pattern = (typeof element.notebook === 'string') ? undefined : element.notebook?.pattern; if (element.cells !== undefined) { for (const cell of element.cells) { result.push(asDocumentFilter(notebookType, scheme, pattern, cell.language)); } } else { result.push(asDocumentFilter(notebookType, scheme, pattern, undefined)); } } return result; } $NotebookDocumentSyncOptions.asDocumentSelector = asDocumentSelector; function asDocumentFilter(notebookType, scheme, pattern, language) { return scheme === undefined && pattern === undefined ? { notebook: notebookType, language } : { notebook: { notebookType, scheme, pattern }, language }; } })($NotebookDocumentSyncOptions || ($NotebookDocumentSyncOptions = {})); var SyncInfo; (function (SyncInfo) { function create(cells) { return { cells, uris: new Set(cells.map(cell => cell.document.uri.toString())) }; } SyncInfo.create = create; })(SyncInfo || (SyncInfo = {})); class NotebookDocumentSyncFeatureProvider { constructor(client, options) { this.client = client; this.options = options; this.notebookSyncInfo = new Map(); this.notebookDidOpen = new Set(); this.disposables = []; this.selector = client.protocol2CodeConverter.asDocumentSelector($NotebookDocumentSyncOptions.asDocumentSelector(options)); // open vscode.workspace.onDidOpenNotebookDocument((notebookDocument) => { this.notebookDidOpen.add(notebookDocument.uri.toString()); this.didOpen(notebookDocument); }, undefined, this.disposables); for (const notebookDocument of vscode.workspace.notebookDocuments) { this.notebookDidOpen.add(notebookDocument.uri.toString()); this.didOpen(notebookDocument); } // Notebook document changed. vscode.workspace.onDidChangeNotebookDocument(event => this.didChangeNotebookDocument(event), undefined, this.disposables); //save if (this.options.save === true) { vscode.workspace.onDidSaveNotebookDocument(notebookDocument => this.didSave(notebookDocument), undefined, this.disposables); } // close vscode.workspace.onDidCloseNotebookDocument((notebookDocument) => { this.didClose(notebookDocument); this.notebookDidOpen.delete(notebookDocument.uri.toString()); }, undefined, this.disposables); } getState() { for (const notebook of vscode.workspace.notebookDocuments) { const matchingCells = this.getMatchingCells(notebook); if (matchingCells !== undefined) { return { kind: 'document', id: '$internal', registrations: true, matches: true }; } } return { kind: 'document', id: '$internal', registrations: true, matches: false }; } get mode() { return 'notebook'; } handles(textDocument) { return vscode.languages.match(this.selector, textDocument) > 0; } didOpenNotebookCellTextDocument(notebookDocument, cell) { if (vscode.languages.match(this.selector, cell.document) === 0) { return; } if (!this.notebookDidOpen.has(notebookDocument.uri.toString())) { // We have never received an open notification for the notebook document. // VS Code guarantees that we first get cell document open and then // notebook open. So simply wait for the notebook open. return; } const syncInfo = this.notebookSyncInfo.get(notebookDocument.uri.toString()); // In VS Code we receive a notebook open before a cell document open. // The document and the cell is synced. const cellMatches = this.cellMatches(notebookDocument, cell); if (syncInfo !== undefined) { const cellIsSynced = syncInfo.uris.has(cell.document.uri.toString()); if ((cellMatches && cellIsSynced) || (!cellMatches && !cellIsSynced)) { // The cell doesn't match and was not synced or it matches and is synced. // In both cases nothing to do. // // Note that if the language mode of a document changes we remove the // cell and add it back to update the language mode on the server side. return; } if (cellMatches) { // don't use cells from above since there might be more matching cells in the notebook // Since we had a matching cell above we will have matching cells now. const matchingCells = this.getMatchingCells(notebookDocument); if (matchingCells !== undefined) { const event = this.asNotebookDocumentChangeEvent(notebookDocument, undefined, syncInfo, matchingCells); if (event !== undefined) { this.doSendChange(event, matchingCells).catch(() => { }); } } } } else { // No sync info. But we have a open event for the notebook document // itself. If the cell matches then we need to send an open with // exactly that cell. if (cellMatches) { this.doSendOpen(notebookDocument, [cell]).catch(() => { }); } } } didChangeNotebookCellTextDocument(notebookDocument, event) { // No match with the selector if (vscode.languages.match(this.selector, event.document) === 0) { return; } this.doSendChange({ notebook: notebookDocument, cells: { textContent: [event] } }, undefined).catch(() => { }); } didCloseNotebookCellTextDocument(notebookDocument, cell) { const syncInfo = this.notebookSyncInfo.get(notebookDocument.uri.toString()); if (syncInfo === undefined) { // The notebook document got never synced. So it doesn't matter if a cell // document closes. return; } const cellUri = cell.document.uri; const index = syncInfo.cells.findIndex((item) => item.document.uri.toString() === cellUri.toString()); if (index === -1) { // The cell never got synced or it got deleted and we now received the document // close event. return; } if (index === 0 && syncInfo.cells.length === 1) { // The last cell. Close the notebook document in the server. this.doSendClose(notebookDocument, syncInfo.cells).catch(() => { }); } else { const newCells = syncInfo.cells.slice(); const deleted = newCells.splice(index, 1); this.doSendChange({ notebook: notebookDocument, cells: { structure: { array: { start: index, deleteCount: 1 }, didClose: deleted } } }, newCells).catch(() => { }); } } dispose() { for (const disposable of this.disposables) { disposable.dispose(); } } didOpen(notebookDocument, matchingCells = this.getMatchingCells(notebookDocument), syncInfo = this.notebookSyncInfo.get(notebookDocument.uri.toString())) { if (syncInfo !== undefined) { if (matchingCells !== undefined) { const event = this.asNotebookDocumentChangeEvent(notebookDocument, undefined, syncInfo, matchingCells); if (event !== undefined) { this.doSendChange(event, matchingCells).catch(() => { }); } } else { this.doSendClose(notebookDocument, []).catch(() => { }); } } else { // Check if we need to sync the notebook document. if (matchingCells === undefined) { return; } this.doSendOpen(notebookDocument, matchingCells).catch(() => { }); } } didChangeNotebookDocument(event) { const notebookDocument = event.notebook; const syncInfo = this.notebookSyncInfo.get(notebookDocument.uri.toString()); if (syncInfo === undefined) { // We have no changes to the cells. Since the notebook wasn't synced // it will not be synced now. if (event.contentChanges.length === 0) { return; } // Check if we have new matching cells. const cells = this.getMatchingCells(notebookDocument); // No matching cells and the notebook never synced. So still no need // to sync it. if (cells === undefined) { return; } // Open the notebook document and ignore the rest of the changes // this the notebooks will be synced with the correct settings. this.didOpen(notebookDocument, cells, syncInfo); } else { // The notebook is synced. First check if we have no matching // cells anymore and if so close the notebook const cells = this.getMatchingCells(notebookDocument); if (cells === undefined) { this.didClose(notebookDocument, syncInfo); return; } const newEvent = this.asNotebookDocumentChangeEvent(event.notebook, event, syncInfo, cells); if (newEvent !== undefined) { this.doSendChange(newEvent, cells).catch(() => { }); } } } didSave(notebookDocument) { const syncInfo = this.notebookSyncInfo.get(notebookDocument.uri.toString()); if (syncInfo === undefined) { return; } this.doSendSave(notebookDocument).catch(() => { }); } didClose(notebookDocument, syncInfo = this.notebookSyncInfo.get(notebookDocument.uri.toString())) { if (syncInfo === undefined) { return; } const syncedCells = notebookDocument.getCells().filter(cell => syncInfo.uris.has(cell.document.uri.toString())); this.doSendClose(notebookDocument, syncedCells).catch(() => { }); } async sendDidOpenNotebookDocument(notebookDocument) { const cells = this.getMatchingCells(notebookDocument); if (cells === undefined) { return; } return this.doSendOpen(notebookDocument, cells); } async doSendOpen(notebookDocument, cells) { const send = async (notebookDocument, cells) => { const nb = Converter.c2p.asNotebookDocument(notebookDocument, cells, this.client.code2ProtocolConverter); const cellDocuments = cells.map(cell => this.client.code2ProtocolConverter.asTextDocumentItem(cell.document)); try { await this.client.sendNotification(proto.DidOpenNotebookDocumentNotification.type, { notebookDocument: nb, cellTextDocuments: cellDocuments }); } catch (error) { this.client.error('Sending DidOpenNotebookDocumentNotification failed', error); throw error; } }; const middleware = this.client.middleware?.notebooks; this.notebookSyncInfo.set(notebookDocument.uri.toString(), SyncInfo.create(cells)); return middleware?.didOpen !== undefined ? middleware.didOpen(notebookDocument, cells, send) : send(notebookDocument, cells); } async sendDidChangeNotebookDocument(event) { return this.doSendChange(event, undefined); } async doSendChange(event, cells = this.getMatchingCells(event.notebook)) { const send = async (event) => { try { await this.client.sendNotification(proto.DidChangeNotebookDocumentNotification.type, { notebookDocument: Converter.c2p.asVersionedNotebookDocumentIdentifier(event.notebook, this.client.code2ProtocolConverter), change: Converter.c2p.asNotebookDocumentChangeEvent(event, this.client.code2ProtocolConverter) }); } catch (error) { this.client.error('Sending DidChangeNotebookDocumentNotification failed', error); throw error; } }; const middleware = this.client.middleware?.notebooks; if (event.cells?.structure !== undefined) { this.notebookSyncInfo.set(event.notebook.uri.toString(), SyncInfo.create(cells ?? [])); } return middleware?.didChange !== undefined ? middleware?.didChange(event, send) : send(event); } async sendDidSaveNotebookDocument(notebookDocument) { return this.doSendSave(notebookDocument); } async doSendSave(notebookDocument) { const send = async (notebookDocument) => { try { await this.client.sendNotification(proto.DidSaveNotebookDocumentNotification.type, { notebookDocument: { uri: this.client.code2ProtocolConverter.asUri(notebookDocument.uri) } }); } catch (error) { this.client.error('Sending DidSaveNotebookDocumentNotification failed', error); throw error; } }; const middleware = this.client.middleware?.notebooks; return middleware?.didSave !== undefined ? middleware.didSave(notebookDocument, send) : send(notebookDocument); } async sendDidCloseNotebookDocument(notebookDocument) { return this.doSendClose(notebookDocument, this.getMatchingCells(notebookDocument) ?? []); } async doSendClose(notebookDocument, cells) { const send = async (notebookDocument, cells) => { try { await this.client.sendNotification(proto.DidCloseNotebookDocumentNotification.type, { notebookDocument: { uri: this.client.code2ProtocolConverter.asUri(notebookDocument.uri) }, cellTextDocuments: cells.map(cell => this.client.code2ProtocolConverter.asTextDocumentIdentifier(cell.document)) }); } catch (error) { this.client.error('Sending DidCloseNotebookDocumentNotification failed', error); throw error; } }; const middleware = this.client.middleware?.notebooks; this.notebookSyncInfo.delete(notebookDocument.uri.toString()); return middleware?.didClose !== undefined ? middleware.didClose(notebookDocument, cells, send) : send(notebookDocument, cells); } asNotebookDocumentChangeEvent(notebook, event, syncInfo, matchingCells) { if (event !== undefined && event.notebook !== notebook) { throw new Error('Notebook must be identical'); } const result = { notebook: notebook }; if (event?.metadata !== undefined) { result.metadata = Converter.c2p.asMetadata(event.metadata); } let matchingCellsSet; if (event?.cellChanges !== undefined && event.cellChanges.length > 0) { const data = []; // Only consider the new matching cells. matchingCellsSet = new Set(matchingCells.map(cell => cell.document.uri.toString())); for (const cellChange of event.cellChanges) { if (matchingCellsSet.has(cellChange.cell.document.uri.toString()) && (cellChange.executionSummary !== undefined || cellChange.metadata !== undefined)) { data.push(cellChange.cell); } } if (data.length > 0) { result.cells = result.cells ?? {}; result.cells.data = data; } } if (((event?.contentChanges !== undefined && event.contentChanges.length > 0) || event === undefined) && syncInfo !== undefined && matchingCells !== undefined) { // We still have matching cells. Check if the cell changes // affect the notebook on the server side. const oldCells = syncInfo.cells; const newCells = matchingCells; // meta data changes are reported using on the cell itself. So we can ignore comparing // it which has a positive effect on performance. const diff = $NotebookCell.computeDiff(oldCells, newCells, false); let addedCells; let removedCells; if (diff !== undefined) { addedCells = diff.cells === undefined ? new Map() : new Map(diff.cells.map(cell => [cell.document.uri.toString(), cell])); removedCells = diff.deleteCount === 0 ? new Map() : new Map(oldCells.slice(diff.start, diff.start + diff.deleteCount).map(cell => [cell.document.uri.toString(), cell])); // Remove the onces that got deleted and inserted again. for (const key of Array.from(removedCells.keys())) { if (addedCells.has(key)) { removedCells.delete(key); addedCells.delete(key); } } result.cells = result.cells ?? {}; const didOpen = []; const didClose = []; if (addedCells.size > 0 || removedCells.size > 0) { for (const cell of addedCells.values()) { didOpen.push(cell); } for (const cell of removedCells.values()) { didClose.push(cell); } } result.cells.structure = { array: diff, didOpen, didClose }; } } // The notebook is a property as well. return Object.keys(result).length > 1 ? result : undefined; } getMatchingCells(notebookDocument, cells = notebookDocument.getCells()) { if (this.options.notebookSelector === undefined) { return undefined; } for (const item of this.options.notebookSelector) { if (item.notebook === undefined || $NotebookDocumentFilter.matchNotebook(item.notebook, notebookDocument)) { const filtered = this.filterCells(notebookDocument, cells, item.cells); return filtered.length === 0 ? undefined : filtered; } } return undefined; } cellMatches(notebookDocument, cell) { const cells = this.getMatchingCells(notebookDocument, [cell]); return cells !== undefined && cells[0] === cell; } filterCells(notebookDocument, cells, cellSelector) { const filtered = cellSelector !== undefined ? cells.filter((cell) => { const cellLanguage = cell.document.languageId; return cellSelector.some((filter => (filter.language === '*' || cellLanguage === filter.language))); }) : cells; return typeof this.client.clientOptions.notebookDocumentOptions?.filterCells === 'function' ? this.client.clientOptions.notebookDocumentOptions.filterCells(notebookDocument, filtered) : filtered; } } class NotebookDocumentSyncFeature { constructor(client) { this.client = client; this.registrations = new Map(); this.registrationType = proto.NotebookDocumentSyncRegistrationType.type; // We don't receive an event for cells where the document changes its language mode // Since we allow servers to filter on the language mode we fire such an event ourselves. vscode.workspace.onDidOpenTextDocument((textDocument) => { if (textDocument.uri.scheme !== NotebookDocumentSyncFeature.CellScheme) { return; } const [notebookDocument, notebookCell] = this.findNotebookDocumentAndCell(textDocument); if (notebookDocument === undefined || notebookCell === undefined) { return; } for (const provider of this.registrations.values()) { if (provider instanceof NotebookDocumentSyncFeatureProvider) { provider.didOpenNotebookCellTextDocument(notebookDocument, notebookCell); } } }); vscode.workspace.onDidChangeTextDocument((event) => { if (event.contentChanges.length === 0) { return; } const textDocument = event.document; if (textDocument.uri.scheme !== NotebookDocumentSyncFeature.CellScheme) { return; } const [notebookDocument,] = this.findNotebookDocumentAndCell(textDocument); if (notebookDocument === undefined) { return; } for (const provider of this.registrations.values()) { if (provider instanceof NotebookDocumentSyncFeatureProvider) { provider.didChangeNotebookCellTextDocument(notebookDocument, event); } } }); vscode.workspace.onDidCloseTextDocument((textDocument) => { if (textDocument.uri.scheme !== NotebookDocumentSyncFeature.CellScheme) { return; } // There are two cases when we receive a close for a text document // 1: the cell got removed. This is handled in `onDidChangeNotebookCells` // 2: the language mode of a cell changed. This keeps the URI stable so // we will still find the cell and the notebook document. const [notebookDocument, notebookCell] = this.findNotebookDocumentAndCell(textDocument); if (notebookDocument === undefined || notebookCell === undefined) { return; } for (const provider of this.registrations.values()) { if (provider instanceof NotebookDocumentSyncFeatureProvider) { provider.didCloseNotebookCellTextDocument(notebookDocument, notebookCell); } } }); } getState() { if (this.registrations.size === 0) { return { kind: 'document', id: this.registrationType.method, registrations: false, matches: false }; } for (const provider of this.registrations.values()) { const state = provider.getState(); if (state.kind === 'document' && state.registrations === true && state.matches === true) { return { kind: 'document', id: this.registrationType.method, registrations: true, matches: true }; } } return { kind: 'document', id: this.registrationType.method, registrations: true, matches: false }; } fillClientCapabilities(capabilities) { const synchronization = ensure(ensure(capabilities, 'notebookDocument'), 'synchronization'); synchronization.dynamicRegistration = true; synchronization.executionSummarySupport = true; } preInitialize(capabilities) { const options = capabilities.notebookDocumentSync; if (options === undefined) { return; } this.dedicatedChannel = this.client.protocol2CodeConverter.asDocumentSelector($NotebookDocumentSyncOptions.asDocumentSelector(options)); } initialize(capabilities) { const options = capabilities.notebookDocumentSync; if (options === undefined) { return; } const id = options.id ?? UUID.generateUuid(); this.register({ id, registerOptions: options }); } register(data) { const provider = new NotebookDocumentSyncFeatureProvider(this.client, data.registerOptions); this.registrations.set(data.id, provider); } unregister(id) { const provider = this.registrations.get(id); provider && provider.dispose(); } clear() { for (const provider of this.registrations.values()) { provider.dispose(); } this.registrations.clear(); } handles(textDocument) { if (textDocument.uri.scheme !== NotebookDocumentSyncFeature.CellScheme) { return false; } if (this.dedicatedChannel !== undefined && vscode.languages.match(this.dedicatedChannel, textDocument) > 0) { return true; } for (const provider of this.registrations.values()) { if (provider.handles(textDocument)) { return true; } } return false; } getProvider(notebookCell) { for (const provider of this.registrations.values()) { if (provider.handles(notebookCell.document)) { return provider; } } return undefined; } findNotebookDocumentAndCell(textDocument) { const uri = textDocument.uri.toString(); for (const notebookDocument of vscode.workspace.notebookDocuments) { for (const cell of notebookDocument.getCells()) { if (cell.document.uri.toString() === uri) { return [notebookDocument, cell]; } } } return [undefined, undefined]; } } exports.NotebookDocumentSyncFeature = NotebookDocumentSyncFeature; NotebookDocumentSyncFeature.CellScheme = 'vscode-notebook-cell';