- Hover provider showing entity information and type - Go-to-definition (F12) for entity references - Basic IFC file validation (ISO-10303-21 header check) - Entity parsing with regex-based detection - Proper CommonJS module system (avoiding ES module issues) This replaces the broken baseline from ifc-developer-tools which had: - Non-functional ES module configuration - Circular dependency issues - Parser crashes - Non-working PositionVisitor Built on Microsoft's LSP example template for a clean, maintainable foundation. Next: Add hierarchical entity dependency tree in hover tooltip."
851 lines
38 KiB
JavaScript
851 lines
38 KiB
JavaScript
"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';
|