ifc-language-server/client/node_modules/vscode-languageclient/lib/common/notebook.js
Ryan Schultz 8afacf268a Implemented a working Language Server Protocol (LSP) for IFC files with:
- 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."
2025-12-07 10:20:07 -06:00

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';