UNPKG

vscode-languageclient

Version:
380 lines (379 loc) 15.9 kB
"use strict"; /* -------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.WillDeleteFilesFeature = exports.WillRenameFilesFeature = exports.WillCreateFilesFeature = exports.DidDeleteFilesFeature = exports.DidRenameFilesFeature = exports.DidCreateFilesFeature = void 0; const code = __importStar(require("vscode")); const minimatch = __importStar(require("minimatch")); const proto = __importStar(require("vscode-languageserver-protocol")); const UUID = __importStar(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 { _client; _event; _registrationType; _clientCapability; _serverCapability; _listener; // This property must stay private. Otherwise the type `minimatch.IMinimatch` becomes public and as a consequence we would need to // ship the d.ts files for minimatch to make the compiler happy when compiling against the vscode-languageclient library _filters; 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.info(`Unable to determine file type for ${uri.toString()}. Treating as a match.`); 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 { _notificationType; _accessUri; _createParams; 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 { _willListener; _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 { _requestType; _accessUri; _createParams; 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;