- 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."
333 lines
14 KiB
JavaScript
333 lines
14 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.WillDeleteFilesFeature = exports.WillRenameFilesFeature = exports.WillCreateFilesFeature = exports.DidDeleteFilesFeature = exports.DidRenameFilesFeature = exports.DidCreateFilesFeature = void 0;
|
|
const code = require("vscode");
|
|
const minimatch = require("minimatch");
|
|
const proto = require("vscode-languageserver-protocol");
|
|
const UUID = require("./utils/uuid");
|
|
function ensure(target, key) {
|
|
if (target[key] === void 0) {
|
|
target[key] = {};
|
|
}
|
|
return target[key];
|
|
}
|
|
function access(target, key) {
|
|
return target[key];
|
|
}
|
|
function assign(target, key, value) {
|
|
target[key] = value;
|
|
}
|
|
class FileOperationFeature {
|
|
constructor(client, event, registrationType, clientCapability, serverCapability) {
|
|
this._client = client;
|
|
this._event = event;
|
|
this._registrationType = registrationType;
|
|
this._clientCapability = clientCapability;
|
|
this._serverCapability = serverCapability;
|
|
this._filters = new Map();
|
|
}
|
|
getState() {
|
|
return { kind: 'workspace', id: this._registrationType.method, registrations: this._filters.size > 0 };
|
|
}
|
|
filterSize() {
|
|
return this._filters.size;
|
|
}
|
|
get registrationType() {
|
|
return this._registrationType;
|
|
}
|
|
fillClientCapabilities(capabilities) {
|
|
const value = ensure(ensure(capabilities, 'workspace'), 'fileOperations');
|
|
// this happens n times but it is the same value so we tolerate this.
|
|
assign(value, 'dynamicRegistration', true);
|
|
assign(value, this._clientCapability, true);
|
|
}
|
|
initialize(capabilities) {
|
|
const options = capabilities.workspace?.fileOperations;
|
|
const capability = options !== undefined ? access(options, this._serverCapability) : undefined;
|
|
if (capability?.filters !== undefined) {
|
|
try {
|
|
this.register({
|
|
id: UUID.generateUuid(),
|
|
registerOptions: { filters: capability.filters }
|
|
});
|
|
}
|
|
catch (e) {
|
|
this._client.warn(`Ignoring invalid glob pattern for ${this._serverCapability} registration: ${e}`);
|
|
}
|
|
}
|
|
}
|
|
register(data) {
|
|
if (!this._listener) {
|
|
this._listener = this._event(this.send, this);
|
|
}
|
|
const minimatchFilter = data.registerOptions.filters.map((filter) => {
|
|
const matcher = new minimatch.Minimatch(filter.pattern.glob, FileOperationFeature.asMinimatchOptions(filter.pattern.options));
|
|
if (!matcher.makeRe()) {
|
|
throw new Error(`Invalid pattern ${filter.pattern.glob}!`);
|
|
}
|
|
return { scheme: filter.scheme, matcher, kind: filter.pattern.matches };
|
|
});
|
|
this._filters.set(data.id, minimatchFilter);
|
|
}
|
|
unregister(id) {
|
|
this._filters.delete(id);
|
|
if (this._filters.size === 0 && this._listener) {
|
|
this._listener.dispose();
|
|
this._listener = undefined;
|
|
}
|
|
}
|
|
clear() {
|
|
this._filters.clear();
|
|
if (this._listener) {
|
|
this._listener.dispose();
|
|
this._listener = undefined;
|
|
}
|
|
}
|
|
getFileType(uri) {
|
|
return FileOperationFeature.getFileType(uri);
|
|
}
|
|
async filter(event, prop) {
|
|
// (Asynchronously) map each file onto a boolean of whether it matches
|
|
// any of the globs.
|
|
const fileMatches = await Promise.all(event.files.map(async (item) => {
|
|
const uri = prop(item);
|
|
// Use fsPath to make this consistent with file system watchers but help
|
|
// minimatch to use '/' instead of `\\` if present.
|
|
const path = uri.fsPath.replace(/\\/g, '/');
|
|
for (const filters of this._filters.values()) {
|
|
for (const filter of filters) {
|
|
if (filter.scheme !== undefined && filter.scheme !== uri.scheme) {
|
|
continue;
|
|
}
|
|
if (filter.matcher.match(path)) {
|
|
// The pattern matches. If kind is undefined then everything is ok
|
|
if (filter.kind === undefined) {
|
|
return true;
|
|
}
|
|
const fileType = await this.getFileType(uri);
|
|
// If we can't determine the file type than we treat it as a match.
|
|
// Dropping it would be another alternative.
|
|
if (fileType === undefined) {
|
|
this._client.error(`Failed to determine file type for ${uri.toString()}.`);
|
|
return true;
|
|
}
|
|
if ((fileType === code.FileType.File && filter.kind === proto.FileOperationPatternKind.file) || (fileType === code.FileType.Directory && filter.kind === proto.FileOperationPatternKind.folder)) {
|
|
return true;
|
|
}
|
|
}
|
|
else if (filter.kind === proto.FileOperationPatternKind.folder) {
|
|
const fileType = await FileOperationFeature.getFileType(uri);
|
|
if (fileType === code.FileType.Directory && filter.matcher.match(`${path}/`)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}));
|
|
// Filter the files to those that matched.
|
|
const files = event.files.filter((_, index) => fileMatches[index]);
|
|
return { ...event, files };
|
|
}
|
|
static async getFileType(uri) {
|
|
try {
|
|
return (await code.workspace.fs.stat(uri)).type;
|
|
}
|
|
catch (e) {
|
|
return undefined;
|
|
}
|
|
}
|
|
static asMinimatchOptions(options) {
|
|
// The spec doesn't state that dot files don't match. So we make
|
|
// matching those the default.
|
|
const result = { dot: true };
|
|
if (options?.ignoreCase === true) {
|
|
result.nocase = true;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
class NotificationFileOperationFeature extends FileOperationFeature {
|
|
constructor(client, event, notificationType, clientCapability, serverCapability, accessUri, createParams) {
|
|
super(client, event, notificationType, clientCapability, serverCapability);
|
|
this._notificationType = notificationType;
|
|
this._accessUri = accessUri;
|
|
this._createParams = createParams;
|
|
}
|
|
async send(originalEvent) {
|
|
// Create a copy of the event that has the files filtered to match what the
|
|
// server wants.
|
|
const filteredEvent = await this.filter(originalEvent, this._accessUri);
|
|
if (filteredEvent.files.length) {
|
|
const next = async (event) => {
|
|
return this._client.sendNotification(this._notificationType, this._createParams(event));
|
|
};
|
|
return this.doSend(filteredEvent, next);
|
|
}
|
|
}
|
|
}
|
|
class CachingNotificationFileOperationFeature extends NotificationFileOperationFeature {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._fsPathFileTypes = new Map();
|
|
}
|
|
async getFileType(uri) {
|
|
const fsPath = uri.fsPath;
|
|
if (this._fsPathFileTypes.has(fsPath)) {
|
|
return this._fsPathFileTypes.get(fsPath);
|
|
}
|
|
const type = await FileOperationFeature.getFileType(uri);
|
|
if (type) {
|
|
this._fsPathFileTypes.set(fsPath, type);
|
|
}
|
|
return type;
|
|
}
|
|
async cacheFileTypes(event, prop) {
|
|
// Calling filter will force the matching logic to run. For any item
|
|
// that requires a getFileType lookup, the overriden getFileType will
|
|
// be called that will cache the result so that when onDidRename fires,
|
|
// it can still be checked even though the item no longer exists on disk
|
|
// in its original location.
|
|
await this.filter(event, prop);
|
|
}
|
|
clearFileTypeCache() {
|
|
this._fsPathFileTypes.clear();
|
|
}
|
|
unregister(id) {
|
|
super.unregister(id);
|
|
if (this.filterSize() === 0 && this._willListener) {
|
|
this._willListener.dispose();
|
|
this._willListener = undefined;
|
|
}
|
|
}
|
|
clear() {
|
|
super.clear();
|
|
if (this._willListener) {
|
|
this._willListener.dispose();
|
|
this._willListener = undefined;
|
|
}
|
|
}
|
|
}
|
|
class DidCreateFilesFeature extends NotificationFileOperationFeature {
|
|
constructor(client) {
|
|
super(client, code.workspace.onDidCreateFiles, proto.DidCreateFilesNotification.type, 'didCreate', 'didCreate', (i) => i, client.code2ProtocolConverter.asDidCreateFilesParams);
|
|
}
|
|
doSend(event, next) {
|
|
const middleware = this._client.middleware.workspace;
|
|
return middleware?.didCreateFiles
|
|
? middleware.didCreateFiles(event, next)
|
|
: next(event);
|
|
}
|
|
}
|
|
exports.DidCreateFilesFeature = DidCreateFilesFeature;
|
|
class DidRenameFilesFeature extends CachingNotificationFileOperationFeature {
|
|
constructor(client) {
|
|
super(client, code.workspace.onDidRenameFiles, proto.DidRenameFilesNotification.type, 'didRename', 'didRename', (i) => i.oldUri, client.code2ProtocolConverter.asDidRenameFilesParams);
|
|
}
|
|
register(data) {
|
|
if (!this._willListener) {
|
|
this._willListener = code.workspace.onWillRenameFiles(this.willRename, this);
|
|
}
|
|
super.register(data);
|
|
}
|
|
willRename(e) {
|
|
e.waitUntil(this.cacheFileTypes(e, (i) => i.oldUri));
|
|
}
|
|
doSend(event, next) {
|
|
this.clearFileTypeCache();
|
|
const middleware = this._client.middleware.workspace;
|
|
return middleware?.didRenameFiles
|
|
? middleware.didRenameFiles(event, next)
|
|
: next(event);
|
|
}
|
|
}
|
|
exports.DidRenameFilesFeature = DidRenameFilesFeature;
|
|
class DidDeleteFilesFeature extends CachingNotificationFileOperationFeature {
|
|
constructor(client) {
|
|
super(client, code.workspace.onDidDeleteFiles, proto.DidDeleteFilesNotification.type, 'didDelete', 'didDelete', (i) => i, client.code2ProtocolConverter.asDidDeleteFilesParams);
|
|
}
|
|
register(data) {
|
|
if (!this._willListener) {
|
|
this._willListener = code.workspace.onWillDeleteFiles(this.willDelete, this);
|
|
}
|
|
super.register(data);
|
|
}
|
|
willDelete(e) {
|
|
e.waitUntil(this.cacheFileTypes(e, (i) => i));
|
|
}
|
|
doSend(event, next) {
|
|
this.clearFileTypeCache();
|
|
const middleware = this._client.middleware.workspace;
|
|
return middleware?.didDeleteFiles
|
|
? middleware.didDeleteFiles(event, next)
|
|
: next(event);
|
|
}
|
|
}
|
|
exports.DidDeleteFilesFeature = DidDeleteFilesFeature;
|
|
class RequestFileOperationFeature extends FileOperationFeature {
|
|
constructor(client, event, requestType, clientCapability, serverCapability, accessUri, createParams) {
|
|
super(client, event, requestType, clientCapability, serverCapability);
|
|
this._requestType = requestType;
|
|
this._accessUri = accessUri;
|
|
this._createParams = createParams;
|
|
}
|
|
async send(originalEvent) {
|
|
const waitUntil = this.waitUntil(originalEvent);
|
|
originalEvent.waitUntil(waitUntil);
|
|
}
|
|
async waitUntil(originalEvent) {
|
|
// Create a copy of the event that has the files filtered to match what the
|
|
// server wants.
|
|
const filteredEvent = await this.filter(originalEvent, this._accessUri);
|
|
if (filteredEvent.files.length) {
|
|
const next = (event) => {
|
|
return this._client.sendRequest(this._requestType, this._createParams(event), event.token)
|
|
.then(this._client.protocol2CodeConverter.asWorkspaceEdit);
|
|
};
|
|
return this.doSend(filteredEvent, next);
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
class WillCreateFilesFeature extends RequestFileOperationFeature {
|
|
constructor(client) {
|
|
super(client, code.workspace.onWillCreateFiles, proto.WillCreateFilesRequest.type, 'willCreate', 'willCreate', (i) => i, client.code2ProtocolConverter.asWillCreateFilesParams);
|
|
}
|
|
doSend(event, next) {
|
|
const middleware = this._client.middleware.workspace;
|
|
return middleware?.willCreateFiles
|
|
? middleware.willCreateFiles(event, next)
|
|
: next(event);
|
|
}
|
|
}
|
|
exports.WillCreateFilesFeature = WillCreateFilesFeature;
|
|
class WillRenameFilesFeature extends RequestFileOperationFeature {
|
|
constructor(client) {
|
|
super(client, code.workspace.onWillRenameFiles, proto.WillRenameFilesRequest.type, 'willRename', 'willRename', (i) => i.oldUri, client.code2ProtocolConverter.asWillRenameFilesParams);
|
|
}
|
|
doSend(event, next) {
|
|
const middleware = this._client.middleware.workspace;
|
|
return middleware?.willRenameFiles
|
|
? middleware.willRenameFiles(event, next)
|
|
: next(event);
|
|
}
|
|
}
|
|
exports.WillRenameFilesFeature = WillRenameFilesFeature;
|
|
class WillDeleteFilesFeature extends RequestFileOperationFeature {
|
|
constructor(client) {
|
|
super(client, code.workspace.onWillDeleteFiles, proto.WillDeleteFilesRequest.type, 'willDelete', 'willDelete', (i) => i, client.code2ProtocolConverter.asWillDeleteFilesParams);
|
|
}
|
|
doSend(event, next) {
|
|
const middleware = this._client.middleware.workspace;
|
|
return middleware?.willDeleteFiles
|
|
? middleware.willDeleteFiles(event, next)
|
|
: next(event);
|
|
}
|
|
}
|
|
exports.WillDeleteFilesFeature = WillDeleteFilesFeature;
|