From 8bf15dc1d9b186b11c1f954b4c51500e611d3072 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Tue, 30 Jun 2020 11:59:13 +0000 Subject: [PATCH] fix #7269: complete support vscode.workspace.fs API Signed-off-by: Anton Kosyakov --- .theia/settings.json | 2 +- CHANGELOG.md | 8 +- configs/base.eslintrc.json | 2 +- configs/errors.eslintrc.json | 4 +- configs/warnings.eslintrc.json | 8 +- examples/api-tests/src/find-replace.spec.js | 3 +- examples/api-tests/src/keybindings.spec.js | 3 +- .../api-tests/src/launch-preferences.spec.js | 21 +- examples/api-tests/src/monaco-api.spec.js | 11 +- examples/api-tests/src/navigator.spec.js | 91 + examples/api-tests/src/saveable.spec.js | 200 ++- examples/api-tests/src/typescript.spec.js | 17 +- .../api-tests/src/undo-redo-selectAll.spec.js | 3 +- package.json | 11 +- packages/callhierarchy/src/common/glob.ts | 5 +- packages/callhierarchy/src/common/paths.ts | 4 +- packages/callhierarchy/src/common/strings.ts | 127 -- packages/core/package.json | 7 +- .../browser/common-frontend-contribution.ts | 63 +- packages/core/src/browser/core-preferences.ts | 11 + .../core/src/browser/encoding-registry.ts | 97 ++ .../browser/frontend-application-module.ts | 5 + .../src/browser/supported-encodings.ts | 0 .../browser/test/mock-env-variables-server.ts | 7 + packages/core/src/common/buffer.ts | 195 +++ .../src/common/char-code.ts | 0 packages/core/src/common/encoding-service.ts | 224 +++ .../src/common/encodings.ts} | 18 +- .../env-variables/env-variables-protocol.ts | 8 + packages/core/src/common/event.ts | 78 +- .../src/common/messaging/proxy-factory.ts | 5 +- .../common/preferences/preference-schema.ts | 2 +- packages/core/src/common/promise-util.ts | 16 + packages/core/src/common/resource.ts | 15 +- packages/core/src/common/stream.ts | 504 ++++++ packages/core/src/common/strings.ts | 184 +- .../core/src/common/ternary-search-tree.ts | 417 +++++ packages/core/src/common/uri.ts | 13 +- .../env-variables/env-variables-server.ts | 34 + .../browser/debug-configuration-manager.ts | 20 +- .../src/browser/debug-session-contribution.ts | 8 +- packages/debug/src/browser/debug-session.tsx | 13 +- packages/editor/src/browser/editor-command.ts | 4 +- .../editor/src/browser/editor-contribution.ts | 2 +- .../editor/src/browser/editor-preferences.ts | 7 - .../src/browser/quick-file-open.ts | 5 +- packages/filesystem/package.json | 10 +- .../browser/download/file-download-service.ts | 4 - .../file-dialog/file-dialog-service.ts | 26 +- .../browser/file-dialog/file-dialog-tree.ts | 4 +- .../filesystem/src/browser/file-resource.ts | 221 ++- .../filesystem/src/browser/file-selection.ts | 2 +- .../filesystem/src/browser/file-service.ts | 1516 +++++++++++++++++ .../src/browser/file-tree/file-tree-model.ts | 106 +- .../browser/file-tree/file-tree-widget.tsx | 52 +- .../src/browser/file-tree/file-tree.ts | 48 +- .../filesystem-frontend-contribution.ts | 88 +- .../src/browser/filesystem-frontend-module.ts | 192 ++- .../src/browser/filesystem-preferences.ts | 16 + .../src/browser/filesystem-proxy-factory.ts | 45 - .../src/browser/filesystem-watcher.ts | 154 +- .../remote-file-service-contribution.ts} | 29 +- .../common/delegating-file-system-provider.ts | 204 +++ packages/filesystem/src/common/files.ts | 697 ++++++++ .../filesystem/src/common/filesystem-utils.ts | 4 +- .../src/common/filesystem-watcher-protocol.ts | 10 +- packages/filesystem/src/common/filesystem.ts | 122 +- packages/filesystem/src/common/io.ts | 111 ++ .../src/common/remote-file-system-provider.ts | 402 +++++ .../src/common/test/mock-filesystem.ts | 107 -- .../electron-file-dialog-service.ts | 6 +- .../src/node/disk-file-system-provider.ts | 870 ++++++++++ .../src/node/download/directory-archiver.ts | 16 +- .../node/download/file-download-handler.ts | 20 +- packages/filesystem/src/node/encoding-util.ts | 54 - .../src/node/file-change-collection.spec.ts | 12 +- .../src/node/filesystem-backend-module.ts | 51 +- .../src/node/node-filesystem.spec.ts | 818 --------- .../filesystem/src/node/node-filesystem.ts | 616 ------- .../src/browser/getting-started-widget.tsx | 10 +- .../src/browser/diff/git-diff-contribution.ts | 28 +- .../git/src/browser/git-quick-open-service.ts | 59 +- .../browser/git-repository-provider.spec.ts | 63 +- .../src/browser/git-repository-provider.ts | 27 +- packages/git/src/browser/git-scm-provider.ts | 44 +- .../git/src/electron-node/askpass/askpass.ts | 2 +- .../keymaps/src/browser/keymaps-service.ts | 15 +- .../markers/src/browser/marker-manager.ts | 17 +- .../marker-tree-label-provider.spec.ts | 36 +- .../browser/problem/problem-manager.spec.ts | 9 +- .../problem/problem-tree-model.spec.ts | 8 +- .../src/browser/mini-browser-content.ts | 37 +- .../src/node/mini-browser-endpoint.ts | 18 +- .../src/browser/monaco-bulk-edit-service.ts | 2 +- .../monaco/src/browser/monaco-editor-model.ts | 66 +- packages/monaco/src/browser/monaco-editor.ts | 25 +- .../monaco-snippet-suggest-provider.ts | 14 +- .../src/browser/monaco-text-model-service.ts | 6 +- .../src/browser/monaco-theming-service.ts | 13 +- .../monaco/src/browser/monaco-workspace.ts | 83 +- .../src/browser/navigator-diff.spec.ts | 27 +- .../navigator/src/browser/navigator-diff.ts | 14 +- .../src/browser/navigator-model.spec.ts | 327 ---- .../navigator/src/browser/navigator-model.ts | 8 +- .../src/browser/navigator-tree.spec.ts | 191 --- .../navigator/src/browser/navigator-tree.ts | 2 +- .../src/browser/navigator-widget.tsx | 32 +- .../browser/output-editor-model-factory.ts | 5 +- .../src/browser/hosted-plugin-informer.ts | 5 +- .../browser/hosted-plugin-manager-client.ts | 13 +- .../src/node/hosted-instance-manager.ts | 6 +- .../plugin-ext-vscode/compile.tsconfig.json | 3 + packages/plugin-ext-vscode/package.json | 3 +- .../src/browser/plugin-vscode-contribution.ts | 47 + .../browser/plugin-vscode-frontend-module.ts | 6 +- packages/plugin-ext/src/common/arrays.ts | 40 + .../src/common/character-classifier.ts | 73 + .../plugin-ext/src/common/link-computer.ts | 354 ++++ .../src/common/plugin-api-rpc-model.ts | 30 +- .../plugin-ext/src/common/plugin-api-rpc.ts | 79 +- .../plugin-ext/src/common/plugin-protocol.ts | 5 +- .../plugin-ext/src/common/rpc-protocol.ts | 13 +- packages/plugin-ext/src/common/uint.ts | 37 + .../plugin-ext/src/common/uri-components.ts | 74 +- .../src/hosted/browser/hosted-plugin.ts | 31 +- .../src/main/browser/debug/debug-main.ts | 8 +- .../debug/plugin-debug-session-factory.ts | 10 +- .../src/main/browser/dialogs-main.ts | 36 +- .../main/browser/editor/untitled-resource.ts | 25 +- .../src/main/browser/file-system-main-impl.ts | 253 +++ .../src/main/browser/file-system-main.ts | 201 --- .../in-plugin-filesystem-watcher-manager.ts | 125 -- .../src/main/browser/main-context.ts | 13 +- .../browser/main-file-system-event-service.ts | 79 + .../browser/plugin-ext-frontend-module.ts | 5 - .../main/browser/plugin-icon-theme-service.ts | 27 +- .../src/main/browser/plugin-storage.ts | 4 +- .../main/browser/preference-registry-main.ts | 4 +- .../main/browser/text-editor-model-service.ts | 2 +- .../src/main/browser/text-editors-main.ts | 11 +- .../src/main/browser/webview/webview.ts | 10 +- .../src/main/browser/workspace-main.ts | 76 +- .../src/main/common/plugin-paths-protocol.ts | 4 +- .../main/node/paths/plugin-paths-service.ts | 45 +- .../file-system-event-service-ext-impl.ts | 256 +++ .../src/plugin/file-system-ext-impl.ts | 396 +++++ packages/plugin-ext/src/plugin/file-system.ts | 142 -- .../src/plugin/in-plugin-filesystem-proxy.ts | 106 -- .../in-plugin-filesystem-watcher-proxy.ts | 150 -- packages/plugin-ext/src/plugin/languages.ts | 2 +- .../src/plugin/languages/link-provider.ts | 4 +- .../plugin-ext/src/plugin/plugin-context.ts | 47 +- .../plugin-ext/src/plugin/type-converters.ts | 26 +- packages/plugin-ext/src/plugin/types-impl.ts | 60 +- .../plugin-ext/src/plugin/window-state.ts | 2 +- packages/plugin-ext/src/plugin/workspace.ts | 100 +- packages/plugin/src/theia-proposed.d.ts | 12 + packages/plugin/src/theia.d.ts | 968 ++++++----- .../src/browser/folder-preference-provider.ts | 4 +- .../browser/folders-preferences-provider.ts | 6 +- .../src/browser/preferences-contribution.ts | 23 +- .../src/browser/user-preference-provider.ts | 2 +- .../util/preference-scope-command-manager.ts | 11 +- .../views/preference-scope-tabbar-widget.tsx | 14 +- .../workspace-file-preference-provider.ts | 2 +- .../browser/workspace-preference-provider.ts | 4 +- .../process/src/node/multi-ring-buffer.ts | 2 +- .../browser/history/scm-history-widget.tsx | 46 +- packages/scm/src/browser/scm-contribution.ts | 3 +- .../scm/src/browser/scm-quick-open-service.ts | 9 +- ...arch-in-workspace-frontend-contribution.ts | 24 +- .../browser/search-in-workspace-service.ts | 2 +- packages/task/src/browser/quick-open-task.ts | 6 +- .../src/browser/task-configuration-manager.ts | 21 +- packages/task/src/browser/task-service.ts | 2 +- .../browser/task-terminal-widget-manager.ts | 2 +- .../browser/terminal-frontend-contribution.ts | 23 +- .../src/browser/terminal-linkmatcher-files.ts | 11 +- .../src/browser/terminal-widget-impl.ts | 2 +- packages/userstorage/src/browser/index.ts | 3 - .../src/browser/user-storage-contribution.ts | 62 + .../browser/user-storage-frontend-module.ts | 16 +- .../src/browser/user-storage-resource.ts | 74 - .../user-storage-service-filesystem.spec.ts | 235 --- .../user-storage-service-filesystem.ts | 127 -- .../src/browser/user-storage-service.ts | 31 - .../src/package.spec.ts} | 17 +- .../workspace/src/browser/diff-service.ts | 17 +- .../src/browser/quick-open-workspace.ts | 24 +- .../src/browser/workspace-commands.ts | 55 +- .../src/browser/workspace-delete-handler.ts | 33 +- .../browser/workspace-duplicate-handler.ts | 20 +- .../workspace-frontend-contribution.ts | 83 +- .../src/browser/workspace-service.spec.ts | 935 ---------- .../src/browser/workspace-service.ts | 173 +- .../src/browser/workspace-storage-service.ts | 4 +- .../workspace-uri-contribution.spec.ts | 66 +- .../src/browser/workspace-uri-contribution.ts | 6 +- .../workspace/src/browser/workspace-utils.ts | 4 +- .../workspace-variable-contribution.ts | 18 +- packages/workspace/src/common/utils.ts | 5 + yarn.lock | 550 +++--- 202 files changed, 10150 insertions(+), 6958 deletions(-) create mode 100644 examples/api-tests/src/navigator.spec.js delete mode 100644 packages/callhierarchy/src/common/strings.ts create mode 100644 packages/core/src/browser/encoding-registry.ts rename packages/{editor => core}/src/browser/supported-encodings.ts (100%) create mode 100644 packages/core/src/common/buffer.ts rename packages/{callhierarchy => core}/src/common/char-code.ts (100%) create mode 100644 packages/core/src/common/encoding-service.ts rename packages/{filesystem/src/node/drivelist.d.ts => core/src/common/encodings.ts} (72%) create mode 100644 packages/core/src/common/stream.ts create mode 100644 packages/core/src/common/ternary-search-tree.ts create mode 100644 packages/filesystem/src/browser/file-service.ts delete mode 100644 packages/filesystem/src/browser/filesystem-proxy-factory.ts rename packages/filesystem/src/{common/test/mock-filesystem-watcher-server.ts => browser/remote-file-service-contribution.ts} (50%) create mode 100644 packages/filesystem/src/common/delegating-file-system-provider.ts create mode 100644 packages/filesystem/src/common/files.ts create mode 100644 packages/filesystem/src/common/io.ts create mode 100644 packages/filesystem/src/common/remote-file-system-provider.ts delete mode 100644 packages/filesystem/src/common/test/mock-filesystem.ts create mode 100644 packages/filesystem/src/node/disk-file-system-provider.ts delete mode 100644 packages/filesystem/src/node/encoding-util.ts delete mode 100644 packages/filesystem/src/node/node-filesystem.spec.ts delete mode 100644 packages/filesystem/src/node/node-filesystem.ts delete mode 100644 packages/navigator/src/browser/navigator-model.spec.ts delete mode 100644 packages/navigator/src/browser/navigator-tree.spec.ts create mode 100644 packages/plugin-ext-vscode/src/browser/plugin-vscode-contribution.ts create mode 100644 packages/plugin-ext/src/common/arrays.ts create mode 100644 packages/plugin-ext/src/common/character-classifier.ts create mode 100644 packages/plugin-ext/src/common/link-computer.ts create mode 100644 packages/plugin-ext/src/common/uint.ts create mode 100644 packages/plugin-ext/src/main/browser/file-system-main-impl.ts delete mode 100644 packages/plugin-ext/src/main/browser/file-system-main.ts delete mode 100644 packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts create mode 100644 packages/plugin-ext/src/main/browser/main-file-system-event-service.ts create mode 100644 packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts create mode 100644 packages/plugin-ext/src/plugin/file-system-ext-impl.ts delete mode 100644 packages/plugin-ext/src/plugin/file-system.ts delete mode 100644 packages/plugin-ext/src/plugin/in-plugin-filesystem-proxy.ts delete mode 100644 packages/plugin-ext/src/plugin/in-plugin-filesystem-watcher-proxy.ts create mode 100644 packages/userstorage/src/browser/user-storage-contribution.ts delete mode 100644 packages/userstorage/src/browser/user-storage-resource.ts delete mode 100644 packages/userstorage/src/browser/user-storage-service-filesystem.spec.ts delete mode 100644 packages/userstorage/src/browser/user-storage-service-filesystem.ts delete mode 100644 packages/userstorage/src/browser/user-storage-service.ts rename packages/{filesystem/src/common/test/index.ts => userstorage/src/package.spec.ts} (62%) delete mode 100644 packages/workspace/src/browser/workspace-service.spec.ts diff --git a/.theia/settings.json b/.theia/settings.json index b5a2d34a9fe5e..23199246981cf 100644 --- a/.theia/settings.json +++ b/.theia/settings.json @@ -12,4 +12,4 @@ }, "typescript.tsdk": "node_modules/typescript/lib", "clang-format.language.typescript.enable": false -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index beee86122cee5..1d968712c8e7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,13 @@ - [output] `OutputWidget#setInput` has been removed. The _Output_ view automatically shows the channel when calling `OutputChannel#show`. Moved the `OutputCommands` namespace from the `output-contribution` to its dedicated `output-commands` module to overcome a DI cycle. [#8243](https://github.com/eclipse-theia/theia/pull/8243) - [example app] updated yarn.lock so that the latest version of `vscode-ripgrep` is used (`v1.8.0`). This way we can benefit from the recently added support for it using proxy settings when fetching the platform-specific `ripgrep` executable, after npm package install. This should make it a lot easier to build our example application in corporate settings, behind a firewall. [#8280](https://github.com/eclipse-theia/theia/pull/8280) - Note to downstream IDE designers: this change will not have an effect beyond this repo's example application. If it's desirable for your product to have the latest `vscode-ripgrep`, you should do similarly in your own `yarn.lock`. - + +- [[filesystem]](#1_4_0_deprecate_file_sytem) `FileSystem` and `FileSystemWatcher` services are deprecated [#7908](https://github.com/eclipse-theia/theia/pull/7908) + - On the backend there is no anymore `FileSystem` implementation. One has to use Node.js APIs instead. + - On the frontend `FileService` should be used instead. It was ported from VS Code for compatibility with VS Code extensions. + - On the frontend `EnvVariableServer` should be used instead to access the current user home and available drives. + +- [[userstorage]](#1_4_0_usestorage_as_fs_provider) `UserStorageService` was replaced by the user data fs provider [#7908](https://github.com/eclipse-theia/theia/pull/7908) ## v1.4.0 diff --git a/configs/base.eslintrc.json b/configs/base.eslintrc.json index e843055aa7a64..7cd019a0794d6 100644 --- a/configs/base.eslintrc.json +++ b/configs/base.eslintrc.json @@ -22,4 +22,4 @@ "node_modules", "*.d.ts" ] -} +} \ No newline at end of file diff --git a/configs/errors.eslintrc.json b/configs/errors.eslintrc.json index 4190500706670..989629607cc40 100644 --- a/configs/errors.eslintrc.json +++ b/configs/errors.eslintrc.json @@ -1,6 +1,5 @@ { "rules": { - "@typescript-eslint/class-name-casing": "error", "@typescript-eslint/consistent-type-definitions": "error", "@typescript-eslint/indent": "off", "@typescript-eslint/no-explicit-any": "error", @@ -35,7 +34,6 @@ "guard-for-in": "error", "id-blacklist": "off", "id-match": "off", - "import/no-deprecated": "error", "import/no-extraneous-dependencies": "off", "max-len": [ "error", @@ -131,4 +129,4 @@ } ] } -} +} \ No newline at end of file diff --git a/configs/warnings.eslintrc.json b/configs/warnings.eslintrc.json index 41c9ed548bcfc..8403d0a9dc363 100644 --- a/configs/warnings.eslintrc.json +++ b/configs/warnings.eslintrc.json @@ -1,6 +1,10 @@ { + "plugins": [ + "deprecation" + ], "rules": { "@typescript-eslint/await-thenable": "warn", - "no-return-await": "warn" + "no-return-await": "warn", + "deprecation/deprecation": "warn" } -} +} \ No newline at end of file diff --git a/examples/api-tests/src/find-replace.spec.js b/examples/api-tests/src/find-replace.spec.js index a6d677db5d3b6..d31c5140209c1 100644 --- a/examples/api-tests/src/find-replace.spec.js +++ b/examples/api-tests/src/find-replace.spec.js @@ -19,7 +19,6 @@ describe('Find and Replace', function () { const { assert } = chai; - const Uri = require('@theia/core/lib/common/uri'); const { animationFrame } = require('@theia/core/lib/browser/browser'); const { DisposableCollection } = require('@theia/core/lib/common/disposable'); const { CommonCommands } = require('@theia/core/lib/browser/common-frontend-contribution'); @@ -40,7 +39,7 @@ describe('Find and Replace', function () { const navigatorContribution = container.get(FileNavigatorContribution); const shell = container.get(ApplicationShell); - const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri); + const rootUri = workspaceService.tryGetRoots()[0].resource; const fileUri = rootUri.resolve('webpack.config.js'); const toTearDown = new DisposableCollection(); diff --git a/examples/api-tests/src/keybindings.spec.js b/examples/api-tests/src/keybindings.spec.js index 30df977f67068..5dd210500eac1 100644 --- a/examples/api-tests/src/keybindings.spec.js +++ b/examples/api-tests/src/keybindings.spec.js @@ -30,7 +30,6 @@ describe('Keybindings', function () { const { Deferred } = require('@theia/core/lib/common/promise-util'); const { Key } = require('@theia/core/lib/browser/keys'); const { EditorManager } = require('@theia/editor/lib/browser/editor-manager'); - const Uri = require('@theia/core/lib/common/uri'); const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); /** @type {import('inversify').Container} */ @@ -74,7 +73,7 @@ describe('Keybindings', function () { when: 'false' })); - const editor = await editorManager.open(new Uri.default(workspaceService.tryGetRoots()[0].uri).resolve('package.json'), { + const editor = await editorManager.open(workspaceService.tryGetRoots()[0].resource.resolve('package.json'), { mode: 'activate', selection: { start: { diff --git a/examples/api-tests/src/launch-preferences.spec.js b/examples/api-tests/src/launch-preferences.spec.js index c47cfc2309ae9..c5d376e262b76 100644 --- a/examples/api-tests/src/launch-preferences.spec.js +++ b/examples/api-tests/src/launch-preferences.spec.js @@ -31,9 +31,9 @@ describe('Launch Preferences', function () { const { assert } = chai; const { PreferenceService, PreferenceScope } = require('@theia/core/lib/browser/preferences/preference-service'); - const Uri = require('@theia/core/lib/common/uri'); const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); - const { FileSystem } = require('@theia/filesystem/lib/common/filesystem'); + const { FileService } = require('@theia/filesystem/lib/browser/file-service'); + const { FileResourceResolver } = require('@theia/filesystem/lib/browser/file-resource'); const { MonacoTextModelService } = require('@theia/monaco/lib/browser/monaco-text-model-service'); const { MonacoWorkspace } = require('@theia/monaco/lib/browser/monaco-workspace'); @@ -41,10 +41,10 @@ describe('Launch Preferences', function () { /** @type {import('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */ const preferences = container.get(PreferenceService); const workspaceService = container.get(WorkspaceService); - /** @type {import('@theia/filesystem/lib/common/filesystem').FileSystem} */ - const fileSystem = container.get(FileSystem); + const fileService = container.get(FileService); const textModelService = container.get(MonacoTextModelService); const workspace = container.get(MonacoWorkspace); + const fileResourceResolver = container.get(FileResourceResolver); const defaultLaunch = { 'configurations': [], @@ -388,7 +388,7 @@ describe('Launch Preferences', function () { } - const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri); + const rootUri = workspaceService.tryGetRoots()[0].resource; function deleteWorkspacePreferences() { const promises = []; @@ -418,22 +418,21 @@ describe('Launch Preferences', function () { } return Promise.all([ ...promises, - fileSystem.delete(rootUri.resolve('.theia').toString(), { moveToTrash: false }).catch(() => { }), - fileSystem.delete(rootUri.resolve('.vscode').toString(), { moveToTrash: false }).catch(() => { }) + fileService.delete(rootUri.resolve('.theia'), { fromUserGesture: false, recursive: true }).catch(() => { }), + fileService.delete(rootUri.resolve('.vscode'), { fromUserGesture: false, recursive: true }).catch(() => { }) ]); } - const client = /** @type {import('@theia/filesystem/lib/common/filesystem').FileSystemClient} */ (fileSystem.getClient()); - const originalShouldOverwrite = client.shouldOverwrite; + const originalShouldOverwrite = fileResourceResolver['shouldOverwrite']; before(async () => { // fail tests if out of async happens - client.shouldOverwrite = async () => (assert.fail('should be in sync'), false); + fileResourceResolver['shouldOverwrite'] = async () => (assert.fail('should be in sync'), false); await deleteWorkspacePreferences(); }); after(() => { - client.shouldOverwrite = originalShouldOverwrite; + fileResourceResolver['shouldOverwrite'] = originalShouldOverwrite; }); /** diff --git a/examples/api-tests/src/monaco-api.spec.js b/examples/api-tests/src/monaco-api.spec.js index 8fe67d78b4569..f6b3f3e8ab606 100644 --- a/examples/api-tests/src/monaco-api.spec.js +++ b/examples/api-tests/src/monaco-api.spec.js @@ -21,7 +21,6 @@ describe('Monaco API', async function () { const { assert } = chai; const { EditorManager } = require('@theia/editor/lib/browser/editor-manager'); - const Uri = require('@theia/core/lib/common/uri'); const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor'); const { MonacoResolvedKeybinding } = require('@theia/monaco/lib/browser/monaco-resolved-keybinding'); @@ -39,7 +38,7 @@ describe('Monaco API', async function () { before(async () => { const root = workspaceService.tryGetRoots()[0]; - const editor = await editorManager.open(new Uri.default(root.uri).resolve('package.json'), { + const editor = await editorManager.open(root.resource.resolve('package.json'), { mode: 'reveal' }); monacoEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editor)); @@ -67,11 +66,11 @@ describe('Monaco API', async function () { const platform = window.navigator.platform; let expected; - if (platform.includes("Mac")){ + if (platform.includes('Mac')) { // Mac os expected = { label: '⌃⇧⌥⌘K', - ariaLabel: "⌃⇧⌥⌘K", + ariaLabel: '⌃⇧⌥⌘K', electronAccelerator: 'Ctrl+Shift+Alt+Cmd+K', userSettingsLabel: 'ctrl+shift+alt+cmd+K', WYSIWYG: true, @@ -87,7 +86,7 @@ describe('Monaco API', async function () { dispatchParts: [ 'ctrl+shift+alt+meta+K' ] - } + }; } else { expected = { label: 'Ctrl+Shift+Alt+K', @@ -107,7 +106,7 @@ describe('Monaco API', async function () { dispatchParts: [ 'ctrl+shift+alt+K' ] - } + }; } assert.deepStrictEqual({ diff --git a/examples/api-tests/src/navigator.spec.js b/examples/api-tests/src/navigator.spec.js new file mode 100644 index 0000000000000..303049e91bf70 --- /dev/null +++ b/examples/api-tests/src/navigator.spec.js @@ -0,0 +1,91 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +// @ts-check +describe('Navigator', function () { + + const { assert } = chai; + + const { FileService } = require('@theia/filesystem/lib/browser/file-service'); + const { DirNode, FileNode } = require('@theia/filesystem/lib/browser/file-tree/file-tree'); + const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); + const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution'); + + /** @type {import('inversify').Container} */ + const container = window['theia'].container; + const fileService = container.get(FileService); + const workspaceService = container.get(WorkspaceService); + const navigatorContribution = container.get(FileNavigatorContribution); + + const rootUri = workspaceService.tryGetRoots()[0].resource; + const fileUri = rootUri.resolve('.test/nested/source/text.txt'); + const targetUri = rootUri.resolve('.test/target'); + + beforeEach(async () => { + await fileService.create(fileUri, 'foo', { fromUserGesture: false, overwrite: true }); + await fileService.createFolder(targetUri); + }); + + afterEach(async () => { + await fileService.delete(targetUri.parent, { fromUserGesture: false, useTrash: false, recursive: true }); + }); + + /** @type {Array<['copy' | 'move', boolean]>} */ + const operations = [ + ['copy', false], + ['move', false] + ]; + /** @type {Array<['file' | 'dir', boolean]>} */ + const fileTypes = [ + ['file', false], + ['dir', false], + ]; + for (const [operation, onlyOperation] of operations) { + for (const [fileType, onlyFileType] of fileTypes) { + const ExpectedNodeType = fileType === 'file' ? FileNode : DirNode; + (onlyOperation || onlyFileType ? it.only : it)(operation + ' ' + fileType, async function () { + const navigator = await navigatorContribution.openView({ reveal: true }); + await navigator.model.refresh(); + + const sourceUri = fileType === 'file' ? fileUri : fileUri.parent; + const sourceNode = await navigator.model.revealFile(sourceUri); + if (!ExpectedNodeType.is(sourceNode)) { + return assert.isTrue(ExpectedNodeType.is(sourceNode)); + } + + const targetNode = await navigator.model.revealFile(targetUri); + if (!DirNode.is(targetNode)) { + return assert.isTrue(DirNode.is(targetNode)); + } + + let actualUri; + if (operation === 'copy') { + actualUri = await navigator.model.copy(sourceUri, targetNode); + } else { + actualUri = await navigator.model.move(sourceNode, targetNode); + } + if (!actualUri) { + return assert.isDefined(actualUri); + } + + await navigator.model.refresh(targetNode); + const actualNode = await navigator.model.revealFile(actualUri); + assert.isTrue(ExpectedNodeType.is(actualNode)); + }); + } + } + +}); diff --git a/examples/api-tests/src/saveable.spec.js b/examples/api-tests/src/saveable.spec.js index bdfc2ab704c7a..27c807946bc09 100644 --- a/examples/api-tests/src/saveable.spec.js +++ b/examples/api-tests/src/saveable.spec.js @@ -23,18 +23,19 @@ describe('Saveable', function () { const { EditorManager } = require('@theia/editor/lib/browser/editor-manager'); const { EditorWidget } = require('@theia/editor/lib/browser/editor-widget'); const { PreferenceService } = require('@theia/core/lib/browser/preferences/preference-service'); - const Uri = require('@theia/core/lib/common/uri'); const { Saveable, SaveableWidget } = require('@theia/core/lib/browser/saveable'); const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); - const { FileSystem } = require('@theia/filesystem/lib/common/filesystem'); + const { FileService } = require('@theia/filesystem/lib/browser/file-service'); + const { FileResource } = require('@theia/filesystem/lib/browser/file-resource'); + const { ETAG_DISABLED } = require('@theia/filesystem/lib/common/files'); const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor'); const { Deferred } = require('@theia/core/lib/common/promise-util'); + const { Disposable, DisposableCollection } = require('@theia/core/lib/common/disposable'); const container = window.theia.container; const editorManager = container.get(EditorManager); const workspaceService = container.get(WorkspaceService); - /** @type {import('@theia/filesystem/lib/common/filesystem').FileSystem} */ - const fileSystem = container.get(FileSystem); + const fileService = container.get(FileService); /** @type {import('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */ const preferences = container.get(PreferenceService); @@ -43,32 +44,42 @@ describe('Saveable', function () { /** @type {MonacoEditor} */ let editor; - const client = fileSystem.getClient(); - const originalShouldOverwrite = client.shouldOverwrite; - - const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri); + const rootUri = workspaceService.tryGetRoots()[0].resource; const fileUri = rootUri.resolve('.test/foo.txt'); + /** + * @param {FileResource['shouldOverwrite']} shouldOverwrite + * @returns {Disposable} + */ + function setShouldOverwrite(shouldOverwrite) { + const resource = editor.document['resource']; + assert.isTrue(resource instanceof FileResource); + const fileResource = /** @type {FileResource} */ (resource); + const originalShouldOverwrite = fileResource['shouldOverwrite']; + fileResource['shouldOverwrite'] = shouldOverwrite; + return Disposable.create(() => fileResource['shouldOverwrite'] = originalShouldOverwrite); + } + + const toTearDown = new DisposableCollection(); + const autoSave = preferences.get('editor.autoSave', undefined, rootUri.toString()); beforeEach(async () => { preferences.set('editor.autoSave', 'off', undefined, rootUri.toString()); await editorManager.closeAll({ save: false }); - await fileSystem.createFile(fileUri.toString(), { content: 'foo' }); + await fileService.create(fileUri, 'foo', { fromUserGesture: false, overwrite: true }); widget = /** @type {EditorWidget & SaveableWidget} */ (await editorManager.open(fileUri, { mode: 'reveal' })); editor = MonacoEditor.get(widget); - - client.shouldOverwrite = async () => (assert.fail('should be in sync'), false); }); afterEach(async () => { + toTearDown.dispose(); preferences.set('editor.autoSave', autoSave, undefined, rootUri.toString()); - client.shouldOverwrite = originalShouldOverwrite; editor = undefined; widget = undefined; await editorManager.closeAll({ save: false }); - await fileSystem.delete(fileUri.parent.toString(), { moveToTrash: false }); + await fileService.delete(fileUri.parent, { fromUserGesture: false, useTrash: false, recursive: true }); }); it('normal save', async function () { @@ -79,8 +90,8 @@ describe('Saveable', function () { await Saveable.save(widget); assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty after '${edit}' save`); assert.equal(editor.getControl().getValue().trimRight(), edit, `model should be updated with '${edit}'`); - const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content.trimRight(), edit, `fs should be updated with '${edit}'`); + const state = await fileService.read(fileUri); + assert.equal(state.value.trimRight(), edit, `fs should be updated with '${edit}'`); } }); @@ -106,11 +117,11 @@ describe('Saveable', function () { let outOfSync = false; let outOfSyncCount = 0; - client.shouldOverwrite = async () => { + toTearDown.push(setShouldOverwrite(async () => { outOfSync = true; outOfSyncCount++; return false; - }; + })); let incrementalUpdate = false; const saveContentChanges = resource.saveContentChanges; @@ -129,60 +140,60 @@ describe('Saveable', function () { assert.equal(outOfSyncCount, 1, 'user should be prompted only once with out of sync dialog'); assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save'); assert.equal(editor.getControl().getValue().trimRight(), longContent.substring(3), 'model should be updated'); - const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, 'baz', 'fs should NOT be updated'); + const state = await fileService.read(fileUri); + assert.equal(state.value, 'baz', 'fs should NOT be updated'); }); - it('accept rejected save', async () => { + it('accept rejected save', async function () { let outOfSync = false; - client.shouldOverwrite = async () => { + toTearDown.push(setShouldOverwrite(async () => { outOfSync = true; return false; - }; + })); editor.getControl().setValue('bar'); assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save'); const resource = editor.document['resource']; const version = resource.version; - await resource.saveContents('baz'); + await resource.saveContents('bazz'); assert.notEqual(version, resource.version, 'latest version should be different after write'); await Saveable.save(widget); assert.isTrue(outOfSync, 'file should be out of sync'); assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save'); assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated'); - let state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, 'baz', 'fs should NOT be updated'); + let state = await fileService.read(fileUri); + assert.equal(state.value, 'bazz', 'fs should NOT be updated'); outOfSync = false; - client.shouldOverwrite = async () => { + toTearDown.push(setShouldOverwrite(async () => { outOfSync = true; return true; - }; + })); assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save'); await Saveable.save(widget); assert.isTrue(outOfSync, 'file should be out of sync'); assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save'); assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated'); - state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content.trimRight(), 'bar', 'fs should be updated'); + state = await fileService.read(fileUri); + assert.equal(state.value.trimRight(), 'bar', 'fs should be updated'); }); it('accept new save', async () => { let outOfSync = false; - client.shouldOverwrite = async () => { + toTearDown.push(setShouldOverwrite(async () => { outOfSync = true; return true; - }; + })); editor.getControl().setValue('bar'); assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save'); - await fileSystem.touchFile(fileUri.toString()); + await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED }); await Saveable.save(widget); assert.isTrue(outOfSync, 'file should be out of sync'); assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save'); assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated'); - const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content.trimRight(), 'bar', 'fs should be updated'); + const state = await fileService.read(fileUri); + assert.equal(state.value.trimRight(), 'bar', 'fs should be updated'); }); it('cancel save on close', async () => { @@ -194,8 +205,8 @@ describe('Saveable', function () { }); assert.isTrue(Saveable.isDirty(widget), 'should be still dirty after canceled close'); assert.isFalse(widget.isDisposed, 'should NOT be disposed after canceled close'); - const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, 'foo', 'fs should NOT be updated after canceled close'); + const state = await fileService.read(fileUri); + assert.equal(state.value, 'foo', 'fs should NOT be updated after canceled close'); }); it('reject save on close', async () => { @@ -205,44 +216,44 @@ describe('Saveable', function () { shouldSave: () => false }); assert.isTrue(widget.isDisposed, 'should be disposed after rejected close'); - const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, 'foo', 'fs should NOT be updated after rejected close'); + const state = await fileService.read(fileUri); + assert.equal(state.value, 'foo', 'fs should NOT be updated after rejected close'); }); it('accept save on close and reject it', async () => { let outOfSync = false; - client.shouldOverwrite = async () => { + toTearDown.push(setShouldOverwrite(async () => { outOfSync = true; return false; - }; + })); editor.getControl().setValue('bar'); assert.isTrue(Saveable.isDirty(widget), 'should be dirty before rejecting save on close'); - await fileSystem.touchFile(fileUri.toString()); + await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED }); await widget.closeWithSaving({ shouldSave: () => true }); assert.isTrue(outOfSync, 'file should be out of sync'); assert.isTrue(widget.isDisposed, 'model should be disposed after close'); - const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content, 'foo', 'fs should NOT be updated'); + const state = await fileService.read(fileUri); + assert.equal(state.value, 'foo2', 'fs should NOT be updated'); }); it('accept save on close and accept new save', async () => { let outOfSync = false; - client.shouldOverwrite = async () => { + toTearDown.push(setShouldOverwrite(async () => { outOfSync = true; return true; - }; + })); editor.getControl().setValue('bar'); assert.isTrue(Saveable.isDirty(widget), 'should be dirty before accepting save on close'); - await fileSystem.touchFile(fileUri.toString()); + await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED }); await widget.closeWithSaving({ shouldSave: () => true }); assert.isTrue(outOfSync, 'file should be out of sync'); assert.isTrue(widget.isDisposed, 'model should be disposed after close'); - const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content.trimRight(), 'bar', 'fs should be updated'); + const state = await fileService.read(fileUri); + assert.equal(state.value.trimRight(), 'bar', 'fs should be updated'); }); it('normal close', async () => { @@ -252,8 +263,8 @@ describe('Saveable', function () { shouldSave: () => true }); assert.isTrue(widget.isDisposed, 'model should be disposed after close'); - const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content.trimRight(), 'bar', 'fs should be updated'); + const state = await fileService.read(fileUri); + assert.equal(state.value.trimRight(), 'bar', 'fs should be updated'); }); it('delete file for saved', async () => { @@ -261,7 +272,7 @@ describe('Saveable', function () { const waitForDisposed = new Deferred(); const listener = editor.onDispose(() => waitForDisposed.resolve()); try { - await fileSystem.delete(fileUri.toString(), { moveToTrash: false }); + await fileService.delete(fileUri); await waitForDisposed.promise; assert.isTrue(widget.isDisposed, 'model should be disposed after delete'); } finally { @@ -277,7 +288,7 @@ describe('Saveable', function () { const listener = () => waitForDidChangeTitle.resolve(); widget.title.changed.connect(listener); try { - await fileSystem.delete(fileUri.toString(), { moveToTrash: false }); + await fileService.delete(fileUri); await waitForDidChangeTitle.promise; assert.isTrue(widget.title.label.endsWith('(deleted from disk)'), 'should be marked as deleted'); assert.isTrue(Saveable.isDirty(widget), 'should be dirty after delete'); @@ -289,7 +300,7 @@ describe('Saveable', function () { waitForDidChangeTitle = new Deferred(); widget.title.changed.connect(listener); try { - await fileSystem.createFile(fileUri.toString(), { content: 'foo' }); + await fileService.create(fileUri, 'foo'); await waitForDidChangeTitle.promise; assert.isFalse(widget.title.label.endsWith('(deleted from disk)'), 'should NOT be marked as deleted'); assert.isTrue(Saveable.isDirty(widget), 'should be dirty after added again'); @@ -307,7 +318,7 @@ describe('Saveable', function () { const waitForInvalid = new Deferred(); const listener = editor.document.onDidChangeValid(() => waitForInvalid.resolve()); try { - await fileSystem.delete(fileUri.toString(), { moveToTrash: false }); + await fileService.delete(fileUri); await waitForInvalid.promise; assert.isFalse(editor.document.valid, 'should be invalid after delete'); } finally { @@ -318,19 +329,19 @@ describe('Saveable', function () { await Saveable.save(widget); assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save'); assert.isTrue(editor.document.valid, 'should be valid after save'); - const state = await fileSystem.resolveContent(fileUri.toString()); - assert.equal(state.content.trimRight(), 'bar', 'fs should be updated'); + const state = await fileService.read(fileUri); + assert.equal(state.value.trimRight(), 'bar', 'fs should be updated'); }); it('move file for saved', async function () { assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty before move'); const targetUri = fileUri.parent.resolve('bar.txt'); - await fileSystem.move(fileUri.toString(), targetUri.toString(), { overwrite: true }); + await fileService.move(fileUri, targetUri, { overwrite: true }); assert.isTrue(widget.isDisposed, 'old model should be disposed after move'); - const renamed = await editorManager.getByUri(targetUri); - assert.equal(renamed.getResourceUri().toString(), targetUri.toString(), 'new model should be created after move'); + const renamed = /** @type {EditorWidget} */ (await editorManager.getByUri(targetUri)); + assert.equal(String(renamed.getResourceUri()), targetUri.toString(), 'new model should be created after move'); assert.equal(renamed.editor.document.getText(), 'foo', 'new model should be created after move'); assert.isFalse(Saveable.isDirty(renamed), 'new model should NOT be dirty after move'); }); @@ -341,11 +352,11 @@ describe('Saveable', function () { const targetUri = fileUri.parent.resolve('bar.txt'); - await fileSystem.move(fileUri.toString(), targetUri.toString(), { overwrite: true }); + await fileService.move(fileUri, targetUri, { overwrite: true }); assert.isTrue(widget.isDisposed, 'old model should be disposed after move'); - const renamed = await editorManager.getByUri(targetUri); - assert.equal(renamed.getResourceUri().toString(), targetUri.toString(), 'new model should be created after move'); + const renamed = /** @type {EditorWidget} */ (await editorManager.getByUri(targetUri)); + assert.equal(String(renamed.getResourceUri()), targetUri.toString(), 'new model should be created after move'); assert.equal(renamed.editor.document.getText(), 'bar', 'new model should be created after move'); assert.isTrue(Saveable.isDirty(renamed), 'new model should be dirty after move'); @@ -363,4 +374,69 @@ describe('Saveable', function () { } }); + it('decode without save', async function () { + assert.strictEqual('utf8', editor.document.getEncoding()); + assert.strictEqual('foo', editor.document.getText()); + await editor.setEncoding('utf16le', 1 /* EncodingMode.Decode */); + assert.strictEqual('utf16le', editor.document.getEncoding()); + assert.notEqual('foo', editor.document.getText().trimRight()); + assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after decode'); + + await widget.closeWithSaving({ + shouldSave: () => undefined + }); + assert.isTrue(widget.isDisposed, 'widget should be disposed after close'); + + widget = /** @type {EditorWidget & SaveableWidget} */ + (await editorManager.open(fileUri, { mode: 'reveal' })); + editor = MonacoEditor.get(widget); + + assert.strictEqual('utf8', editor.document.getEncoding()); + assert.strictEqual('foo', editor.document.getText().trimRight()); + }); + + it('decode with save', async function () { + assert.strictEqual('utf8', editor.document.getEncoding()); + assert.strictEqual('foo', editor.document.getText()); + await editor.setEncoding('utf16le', 1 /* EncodingMode.Decode */); + assert.strictEqual('utf16le', editor.document.getEncoding()); + assert.notEqual('foo', editor.document.getText().trimRight()); + assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after decode'); + + await Saveable.save(widget); + + await widget.closeWithSaving({ + shouldSave: () => undefined + }); + assert.isTrue(widget.isDisposed, 'widget should be disposed after close'); + + widget = /** @type {EditorWidget & SaveableWidget} */ + (await editorManager.open(fileUri, { mode: 'reveal' })); + editor = MonacoEditor.get(widget); + + assert.strictEqual('utf16le', editor.document.getEncoding()); + assert.notEqual('foo', editor.document.getText().trimRight()); + }); + + it('encode', async function () { + assert.strictEqual('utf8', editor.document.getEncoding()); + assert.strictEqual('foo', editor.document.getText()); + await editor.setEncoding('utf16le', 0 /* EncodingMode.Encode */); + assert.strictEqual('utf16le', editor.document.getEncoding()); + assert.strictEqual('foo', editor.document.getText().trimRight()); + assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after encode'); + + await widget.closeWithSaving({ + shouldSave: () => undefined + }); + assert.isTrue(widget.isDisposed, 'widget should be disposed after close'); + + widget = /** @type {EditorWidget & SaveableWidget} */ + (await editorManager.open(fileUri, { mode: 'reveal' })); + editor = MonacoEditor.get(widget); + + assert.strictEqual('utf16le', editor.document.getEncoding()); + assert.strictEqual('foo', editor.document.getText().trimRight()); + }); + }); diff --git a/examples/api-tests/src/typescript.spec.js b/examples/api-tests/src/typescript.spec.js index b74ebc00ef0e8..8d79c64ee16d1 100644 --- a/examples/api-tests/src/typescript.spec.js +++ b/examples/api-tests/src/typescript.spec.js @@ -37,7 +37,7 @@ describe('TypeScript', function () { const { animationFrame } = require('@theia/core/lib/browser/browser'); const { PreferenceService, PreferenceScope } = require('@theia/core/lib/browser/preferences/preference-service'); const { ProgressStatusBarItem } = require('@theia/core/lib/browser/progress-status-bar-item'); - const { FileSystem } = require('@theia/filesystem/lib/common/filesystem'); + const { FileService } = require('@theia/filesystem/lib/browser/file-service'); const { PluginViewRegistry } = require('@theia/plugin-ext/lib/main/browser/view/plugin-view-registry'); const container = window.theia.container; @@ -52,20 +52,18 @@ describe('TypeScript', function () { /** @type {import('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */ const preferences = container.get(PreferenceService); const progressStatusBarItem = container.get(ProgressStatusBarItem); - /** @type {import('@theia/filesystem/lib/common/filesystem').FileSystem} */ - const fileSystem = container.get(FileSystem); + const fileService = container.get(FileService); const pluginViewRegistry = container.get(PluginViewRegistry); const typescriptPluginId = 'vscode.typescript-language-features'; const referencesPluginId = 'ms-vscode.references-view'; - const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri); + const rootUri = workspaceService.tryGetRoots()[0].resource; const serverUri = rootUri.resolve('src-gen/backend/test-server.js'); const inversifyUri = rootUri.resolve('../../node_modules/inversify/dts/inversify.d.ts').normalizePath(); const containerUri = rootUri.resolve('../../node_modules/inversify/dts/container/container.d.ts').normalizePath(); before(async function () { - await fileSystem.createFile(serverUri.toString(), { - content: `// @ts-check + await fileService.create(serverUri, `// @ts-check require('reflect-metadata'); const path = require('path'); const express = require('express'); @@ -128,8 +126,7 @@ module.exports = (port, host, argv) => Promise.resolve() } throw reason; }); - ` - }); + `, { fromUserGesture: false, overwrite: true }); await pluginService.didStart; await Promise.all([typescriptPluginId, referencesPluginId].map(async pluginId => { if (!pluginService.getPlugin(pluginId)) { @@ -140,7 +137,7 @@ module.exports = (port, host, argv) => Promise.resolve() }); after(async function () { - await fileSystem.delete(serverUri.toString()); + await fileService.delete(serverUri, { fromUserGesture: false }); }); beforeEach(async function () { @@ -260,7 +257,7 @@ module.exports = (port, host, argv) => Promise.resolve() assert.isFalse(contextKeyService.match('listFocus')); } - it('document formating should be visible and enabled', async () => { + it('document formating should be visible and enabled', async function () { await openEditor(serverUri); const menu = menuFactory.createContextMenu(EDITOR_CONTEXT_MENU); const item = menu.items.find(i => i.command === 'editor.action.formatDocument'); diff --git a/examples/api-tests/src/undo-redo-selectAll.spec.js b/examples/api-tests/src/undo-redo-selectAll.spec.js index db7afc2b3b8f6..9ffe3a97a64fa 100644 --- a/examples/api-tests/src/undo-redo-selectAll.spec.js +++ b/examples/api-tests/src/undo-redo-selectAll.spec.js @@ -20,7 +20,6 @@ describe('Undo, Redo and Select All', function () { const { assert } = chai; - const Uri = require('@theia/core/lib/common/uri'); const { animationFrame } = require('@theia/core/lib/browser/browser'); const { DisposableCollection } = require('@theia/core/lib/common/disposable'); const { CommonCommands } = require('@theia/core/lib/browser/common-frontend-contribution'); @@ -42,7 +41,7 @@ describe('Undo, Redo and Select All', function () { const shell = container.get(ApplicationShell); const scmContribution = container.get(ScmContribution); - const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri); + const rootUri = workspaceService.tryGetRoots()[0].resource; const fileUri = rootUri.resolve('webpack.config.js'); const toTearDown = new DisposableCollection(); diff --git a/package.json b/package.json index 0b5760dc9a5e8..dd1add16c4260 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,15 @@ "@types/sinon": "^2.3.5", "@types/temp": "^0.8.29", "@types/uuid": "^7.0.3", - "@typescript-eslint/eslint-plugin": "^2.16.0", - "@typescript-eslint/eslint-plugin-tslint": "^2.16.0", - "@typescript-eslint/parser": "^2.16.0", + "@typescript-eslint/eslint-plugin": "^3.1.0", + "@typescript-eslint/eslint-plugin-tslint": "^3.1.0", + "@typescript-eslint/parser": "^3.1.0", "chai-string": "^1.4.0", "colors": "^1.4.0", "concurrently": "^3.5.0", "electron-mocha": "^8.2.0", "eslint": "^6.8.0", + "eslint-plugin-deprecation": "^1.1.0", "eslint-plugin-import": "^2.20.0", "eslint-plugin-no-null": "^1.0.2", "ignore-styles": "^5.0.1", @@ -35,8 +36,8 @@ "sinon": "^3.3.0", "temp": "^0.8.3", "tslint": "^5.12.0", - "typedoc": "^0.15.0-0", - "typedoc-plugin-external-module-map": "^1.0.0", + "typedoc": "^0.17.7", + "typedoc-plugin-external-module-map": "^1.2.1", "typescript": "^3.9.2", "uuid": "^8.0.0" }, diff --git a/packages/callhierarchy/src/common/glob.ts b/packages/callhierarchy/src/common/glob.ts index 432a40d2b21c7..a242a093751e9 100644 --- a/packages/callhierarchy/src/common/glob.ts +++ b/packages/callhierarchy/src/common/glob.ts @@ -20,9 +20,10 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as strings from './strings'; +import * as strings from '@theia/core/lib/common/strings'; import * as paths from './paths'; -import { CharCode } from './char-code'; +import { CharCode } from '@theia/core/lib/common/char-code'; + /* eslint-disable no-shadow, no-null/no-null */ export interface IExpression { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/callhierarchy/src/common/paths.ts b/packages/callhierarchy/src/common/paths.ts index 6500cb9b1d83d..7318c75d2c710 100644 --- a/packages/callhierarchy/src/common/paths.ts +++ b/packages/callhierarchy/src/common/paths.ts @@ -23,8 +23,8 @@ /* eslint-disable no-null/no-null */ 'use strict'; import { isWindows } from '@theia/core/lib/common/os'; -import { startsWithIgnoreCase } from './strings'; -import { CharCode } from './char-code'; +import { startsWithIgnoreCase } from '@theia/core/lib/common/strings'; +import { CharCode } from '@theia/core/lib/common/char-code'; /** * The forward slash path separator. diff --git a/packages/callhierarchy/src/common/strings.ts b/packages/callhierarchy/src/common/strings.ts deleted file mode 100644 index 1d5d2b3692e72..0000000000000 --- a/packages/callhierarchy/src/common/strings.ts +++ /dev/null @@ -1,127 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -// based on https://github.com/Microsoft/vscode/blob/bf7ac9201e7a7d01741d4e6e64b5dc9f3197d97b/src/vs/base/common/strings.ts -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CharCode } from './char-code'; -/** - * Determines if haystack ends with needle. - */ -export function endsWith(haystack: string, needle: string): boolean { - const diff = haystack.length - needle.length; - if (diff > 0) { - return haystack.indexOf(needle, diff) === diff; - } else if (diff === 0) { - return haystack === needle; - } else { - return false; - } -} -export function isLowerAsciiLetter(code: number): boolean { - return code >= CharCode.a && code <= CharCode.z; -} - -export function isUpperAsciiLetter(code: number): boolean { - return code >= CharCode.A && code <= CharCode.Z; -} - -function isAsciiLetter(code: number): boolean { - return isLowerAsciiLetter(code) || isUpperAsciiLetter(code); -} -export function equalsIgnoreCase(a: string, b: string): boolean { - const len1 = a ? a.length : 0; - const len2 = b ? b.length : 0; - - if (len1 !== len2) { - return false; - } - - return doEqualsIgnoreCase(a, b); -} - -function doEqualsIgnoreCase(a: string, b: string, stopAt = a.length): boolean { - if (typeof a !== 'string' || typeof b !== 'string') { - return false; - } - - for (let i = 0; i < stopAt; i++) { - const codeA = a.charCodeAt(i); - const codeB = b.charCodeAt(i); - - if (codeA === codeB) { - continue; - } - - // a-z A-Z - if (isAsciiLetter(codeA) && isAsciiLetter(codeB)) { - const diff = Math.abs(codeA - codeB); - if (diff !== 0 && diff !== 32) { - return false; - } - } - - // Any other charcode - // tslint:disable-next-line:one-line - else { - if (String.fromCharCode(codeA).toLowerCase() !== String.fromCharCode(codeB).toLowerCase()) { - return false; - } - } - } - - return true; -} - -/** - * Escapes regular expression characters in a given string - */ -export function escapeRegExpCharacters(value: string): string { - return value.replace(/[\-\\\{\}\*\+\?\|\^\$\.\[\]\(\)\#]/g, '\\$&'); -} - -export function startsWithIgnoreCase(str: string, candidate: string): boolean { - const candidateLength = candidate.length; - if (candidate.length > str.length) { - return false; - } - - return doEqualsIgnoreCase(str, candidate, candidateLength); -} - -export function* split(s: string, splitter: string): IterableIterator { - let start = 0; - while (start < s.length) { - let end = s.indexOf(splitter, start); - if (end === -1) { - end = s.length; - } - - yield s.substring(start, end); - start = end + splitter.length; - } -} - -export function escapeInvisibleChars(value: string): string { - return value.replace(/\n/g, '\\n').replace(/\r/g, '\\r'); -} - -export function unescapeInvisibleChars(value: string): string { - return value.replace(/\\n/g, '\n').replace(/\\r/g, '\r'); -} diff --git a/packages/core/package.json b/packages/core/package.json index 7ac27c4d2cea1..b2d2755f1ddb6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,18 +19,22 @@ "@types/react-dom": "^16.8.0", "@types/react-virtualized": "^9.18.3", "@types/route-parser": "^0.1.1", + "@types/safer-buffer": "^2.1.0", "@types/ws": "^5.1.2", "@types/yargs": "^11.1.0", "ajv": "^6.5.3", "body-parser": "^1.17.2", "cookie": "^0.4.0", + "drivelist": "^9.0.2", "es6-promise": "^4.2.4", "express": "^4.16.3", "file-icons-js": "^1.0.3", "font-awesome": "^4.7.0", "fs-extra": "^4.0.2", "fuzzy": "^0.1.3", + "iconv-lite": "^0.6.0", "inversify": "^5.0.1", + "jschardet": "^2.1.1", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "nsfw": "^1.2.9", @@ -42,6 +46,7 @@ "reconnecting-websocket": "^4.2.0", "reflect-metadata": "^0.1.10", "route-parser": "^0.0.5", + "safer-buffer": "^2.1.2", "vscode-languageserver-protocol": "^3.15.0-next.8", "vscode-languageserver-types": "^3.15.0-next", "vscode-uri": "^2.1.1", @@ -102,4 +107,4 @@ "nyc": { "extends": "../../configs/nyc.json" } -} +} \ No newline at end of file diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index ed581ee9fa406..76cffe1b49754 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -17,7 +17,7 @@ /* eslint-disable max-len, @typescript-eslint/indent */ import debounce = require('lodash.debounce'); -import { injectable, inject, postConstruct } from 'inversify'; +import { injectable, inject } from 'inversify'; import { TabBar, Widget, Title } from '@phosphor/widgets'; import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry } from '../common/menu'; import { KeybindingContribution, KeybindingRegistry } from './keybinding'; @@ -48,6 +48,9 @@ import { CorePreferences } from './core-preferences'; import { ThemeService } from './theming'; import { PreferenceService, PreferenceScope } from './preferences'; import { ClipboardService } from './clipboard-service'; +import { EncodingRegistry } from './encoding-registry'; +import { UTF8 } from '../common/encodings'; +import { EnvVariablesServer } from '../common/env-variables'; export namespace CommonMenus { @@ -317,8 +320,20 @@ export class CommonFrontendContribution implements FrontendApplicationContributi @inject(ClipboardService) protected readonly clipboardService: ClipboardService; - @postConstruct() - protected init(): void { + @inject(EncodingRegistry) + protected readonly encodingRegistry: EncodingRegistry; + + @inject(EnvVariablesServer) + protected readonly environments: EnvVariablesServer; + + async configure(): Promise { + const configDirUri = await this.environments.getConfigDirUri(); + // Global settings + this.encodingRegistry.registerOverride({ + encoding: UTF8, + parent: new URI(configDirUri) + }); + this.contextKeyService.createKey('isLinux', OS.type() === OS.Type.Linux); this.contextKeyService.createKey('isMac', OS.type() === OS.Type.OSX); this.contextKeyService.createKey('isWindows', OS.type() === OS.Type.Windows); @@ -925,16 +940,16 @@ export class CommonFrontendContribution implements FrontendApplicationContributi this.quickOpenService.open({ onType: (_, accept) => accept(items) }, { - placeholder: 'Select File Icon Theme', - fuzzyMatchLabel: true, - selectIndex: () => items.findIndex(item => item.id === this.iconThemes.current), - onClose: () => { - if (resetTo) { - previewTheme.cancel(); - this.iconThemes.current = resetTo; - } + placeholder: 'Select File Icon Theme', + fuzzyMatchLabel: true, + selectIndex: () => items.findIndex(item => item.id === this.iconThemes.current), + onClose: () => { + if (resetTo) { + previewTheme.cancel(); + this.iconThemes.current = resetTo; } - }); + } + }); } protected selectColorTheme(): void { @@ -964,19 +979,19 @@ export class CommonFrontendContribution implements FrontendApplicationContributi this.quickOpenService.open({ onType: (_, accept) => accept(items) }, { - placeholder: 'Select Color Theme (Up/Down Keys to Preview)', - fuzzyMatchLabel: true, - selectIndex: () => { - const current = this.themeService.getCurrentTheme().id; - return items.findIndex(item => item.id === current); - }, - onClose: () => { - if (resetTo) { - previewTheme.cancel(); - this.themeService.setCurrentTheme(resetTo); - } + placeholder: 'Select Color Theme (Up/Down Keys to Preview)', + fuzzyMatchLabel: true, + selectIndex: () => { + const current = this.themeService.getCurrentTheme().id; + return items.findIndex(item => item.id === current); + }, + onClose: () => { + if (resetTo) { + previewTheme.cancel(); + this.themeService.setCurrentTheme(resetTo); } - }); + } + }); } registerColors(colors: ColorRegistry): void { diff --git a/packages/core/src/browser/core-preferences.ts b/packages/core/src/browser/core-preferences.ts index 7ec11247c3649..0bbbda9d654b3 100644 --- a/packages/core/src/browser/core-preferences.ts +++ b/packages/core/src/browser/core-preferences.ts @@ -16,6 +16,7 @@ import { interfaces } from 'inversify'; import { createPreferenceProxy, PreferenceProxy, PreferenceService, PreferenceContribution, PreferenceSchema } from './preferences'; +import { SUPPORTED_ENCODINGS } from './supported-encodings'; export const corePreferenceSchema: PreferenceSchema = { 'type': 'object', @@ -62,6 +63,15 @@ export const corePreferenceSchema: PreferenceSchema = { type: 'boolean', default: false, description: 'Controls whether to suppress notification popups.' + }, + 'files.encoding': { + 'type': 'string', + 'enum': Object.keys(SUPPORTED_ENCODINGS), + 'default': 'utf8', + 'description': 'The default character set encoding to use when reading and writing files. This setting can also be configured per language.', + 'scope': 'language-overridable', + 'enumDescriptions': Object.keys(SUPPORTED_ENCODINGS).map(key => SUPPORTED_ENCODINGS[key].labelLong), + 'included': Object.keys(SUPPORTED_ENCODINGS).length > 1 } } }; @@ -74,6 +84,7 @@ export interface CoreConfiguration { 'workbench.colorTheme'?: string; 'workbench.iconTheme'?: string | null; 'workbench.silentNotifications': boolean; + 'files.encoding': string } export const CorePreferences = Symbol('CorePreferences'); diff --git a/packages/core/src/browser/encoding-registry.ts b/packages/core/src/browser/encoding-registry.ts new file mode 100644 index 0000000000000..6f8fb2d086fcf --- /dev/null +++ b/packages/core/src/browser/encoding-registry.ts @@ -0,0 +1,97 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/services/textfile/browser/textFileService.ts#L491 + +import { injectable, inject } from 'inversify'; +import URI from '../common/uri'; +import { Disposable } from 'vscode-ws-jsonrpc'; +import { CorePreferences } from './core-preferences'; +import { EncodingService as EncodingService } from '../common/encoding-service'; +import { UTF8 } from '../common/encodings'; + +export interface EncodingOverride { + parent?: URI; + extension?: string; + scheme?: string; + encoding: string; +} + +@injectable() +export class EncodingRegistry { + + protected readonly encodingOverrides: EncodingOverride[] = []; + + @inject(CorePreferences) + protected readonly preferences: CorePreferences; + + @inject(EncodingService) + protected readonly encodingService: EncodingService; + + registerOverride(override: EncodingOverride): Disposable { + this.encodingOverrides.push(override); + return Disposable.create(() => { + const index = this.encodingOverrides.indexOf(override); + if (index !== -1) { + this.encodingOverrides.splice(index, 1); + } + }); + } + + getEncodingForResource(resource: URI, preferredEncoding?: string): string { + let fileEncoding: string; + + const override = this.getEncodingOverride(resource); + if (override) { + fileEncoding = override; // encoding override always wins + } else if (preferredEncoding) { + fileEncoding = preferredEncoding; // preferred encoding comes second + } else { + fileEncoding = this.preferences.get('files.encoding', undefined, resource.toString()); + } + + if (!fileEncoding || !this.encodingService.exists(fileEncoding)) { + return UTF8; // the default is UTF 8 + } + + return this.encodingService.toIconvEncoding(fileEncoding); + } + + protected getEncodingOverride(resource: URI): string | undefined { + if (this.encodingOverrides && this.encodingOverrides.length) { + for (const override of this.encodingOverrides) { + if (override.parent && resource.isEqualOrParent(override.parent)) { + return override.encoding; + } + + if (override.extension && resource.path.ext === `.${override.extension}`) { + return override.encoding; + } + + if (override.scheme && override.scheme === resource.scheme) { + return override.encoding; + } + } + } + + return undefined; + } + +} diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 1f461ba905ea7..7c4241334c6f9 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -93,6 +93,8 @@ import { ProgressBar } from './progress-bar'; import { ProgressBarFactory, ProgressBarOptions } from './progress-bar-factory'; import { CommandOpenHandler } from './command-open-handler'; import { LanguageService } from './language-service'; +import { EncodingRegistry } from './encoding-registry'; +import { EncodingService } from '../common/encoding-service'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -212,6 +214,9 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(LanguageService).toSelf().inSingletonScope(); + bind(EncodingService).toSelf().inSingletonScope(); + bind(EncodingRegistry).toSelf().inSingletonScope(); + bind(ResourceContextKey).toSelf().inSingletonScope(); bind(CommonFrontendContribution).toSelf().inSingletonScope(); [FrontendApplicationContribution, CommandContribution, KeybindingContribution, MenuContribution, ColorContribution].forEach(serviceIdentifier => diff --git a/packages/editor/src/browser/supported-encodings.ts b/packages/core/src/browser/supported-encodings.ts similarity index 100% rename from packages/editor/src/browser/supported-encodings.ts rename to packages/core/src/browser/supported-encodings.ts diff --git a/packages/core/src/browser/test/mock-env-variables-server.ts b/packages/core/src/browser/test/mock-env-variables-server.ts index 350bb243e8806..dd79527edfa7f 100644 --- a/packages/core/src/browser/test/mock-env-variables-server.ts +++ b/packages/core/src/browser/test/mock-env-variables-server.ts @@ -21,6 +21,13 @@ export class MockEnvVariablesServerImpl implements EnvVariablesServer { constructor(protected readonly configDirUri: URI) { } + getHomeDirUri(): Promise { + throw new Error('Method not implemented.'); + } + getDrives(): Promise { + throw new Error('Method not implemented.'); + } + async getConfigDirUri(): Promise { return this.configDirUri.toString(); } diff --git a/packages/core/src/common/buffer.ts b/packages/core/src/common/buffer.ts new file mode 100644 index 0000000000000..58c01d26837b2 --- /dev/null +++ b/packages/core/src/common/buffer.ts @@ -0,0 +1,195 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/base/common/buffer.ts + +import { Buffer as SaferBuffer } from 'safer-buffer'; +import * as iconv from 'iconv-lite'; +import * as streams from './stream'; + +const hasBuffer = (typeof Buffer !== 'undefined'); +const hasTextEncoder = (typeof TextEncoder !== 'undefined'); +const hasTextDecoder = (typeof TextDecoder !== 'undefined'); + +let textEncoder: TextEncoder | null; +let textDecoder: TextDecoder | null; + +export class BinaryBuffer { + + static alloc(byteLength: number): BinaryBuffer { + if (hasBuffer) { + return new BinaryBuffer(Buffer.allocUnsafe(byteLength)); + } else { + return new BinaryBuffer(new Uint8Array(byteLength)); + } + } + + static wrap(actual: Uint8Array): BinaryBuffer { + if (hasBuffer && !(Buffer.isBuffer(actual))) { + // https://nodejs.org/dist/latest-v10.x/docs/api/buffer.html#buffer_class_method_buffer_from_arraybuffer_byteoffset_length + // Create a zero-copy Buffer wrapper around the ArrayBuffer pointed to by the Uint8Array + actual = Buffer.from(actual.buffer, actual.byteOffset, actual.byteLength); + } + return new BinaryBuffer(actual); + } + + static fromString(source: string): BinaryBuffer { + if (hasBuffer) { + return new BinaryBuffer(Buffer.from(source)); + } else if (hasTextEncoder) { + if (!textEncoder) { + textEncoder = new TextEncoder(); + } + return new BinaryBuffer(textEncoder.encode(source)); + } else { + return new BinaryBuffer(iconv.encode(source, 'utf8')); + } + } + + static concat(buffers: BinaryBuffer[], totalLength?: number): BinaryBuffer { + if (typeof totalLength === 'undefined') { + totalLength = 0; + for (let i = 0, len = buffers.length; i < len; i++) { + totalLength += buffers[i].byteLength; + } + } + + const ret = BinaryBuffer.alloc(totalLength); + let offset = 0; + for (let i = 0, len = buffers.length; i < len; i++) { + const element = buffers[i]; + ret.set(element, offset); + offset += element.byteLength; + } + + return ret; + } + + readonly buffer: Uint8Array; + readonly byteLength: number; + + private constructor(buffer: Uint8Array) { + this.buffer = buffer; + this.byteLength = this.buffer.byteLength; + } + + toString(): string { + if (hasBuffer) { + return this.buffer.toString(); + } else if (hasTextDecoder) { + if (!textDecoder) { + textDecoder = new TextDecoder(); + } + return textDecoder.decode(this.buffer); + } else { + return iconv.decode(SaferBuffer.from(this.buffer), 'utf8'); + } + } + + slice(start?: number, end?: number): BinaryBuffer { + // IMPORTANT: use subarray instead of slice because TypedArray#slice + // creates shallow copy and NodeBuffer#slice doesn't. The use of subarray + // ensures the same, performant, behaviour. + return new BinaryBuffer(this.buffer.subarray(start, end)); + } + + set(array: BinaryBuffer, offset?: number): void; + set(array: Uint8Array, offset?: number): void; + set(array: BinaryBuffer | Uint8Array, offset?: number): void { + if (array instanceof BinaryBuffer) { + this.buffer.set(array.buffer, offset); + } else { + this.buffer.set(array, offset); + } + } + + readUInt32BE(offset: number): number { + return ( + this.buffer[offset] * 2 ** 24 + + this.buffer[offset + 1] * 2 ** 16 + + this.buffer[offset + 2] * 2 ** 8 + + this.buffer[offset + 3] + ); + } + + writeUInt32BE(value: number, offset: number): void { + this.buffer[offset + 3] = value; + value = value >>> 8; + this.buffer[offset + 2] = value; + value = value >>> 8; + this.buffer[offset + 1] = value; + value = value >>> 8; + this.buffer[offset] = value; + } + + readUInt32LE(offset: number): number { + return ( + ((this.buffer[offset + 0] << 0) >>> 0) | + ((this.buffer[offset + 1] << 8) >>> 0) | + ((this.buffer[offset + 2] << 16) >>> 0) | + ((this.buffer[offset + 3] << 24) >>> 0) + ); + } + + writeUInt32LE(value: number, offset: number): void { + this.buffer[offset + 0] = (value & 0b11111111); + value = value >>> 8; + this.buffer[offset + 1] = (value & 0b11111111); + value = value >>> 8; + this.buffer[offset + 2] = (value & 0b11111111); + value = value >>> 8; + this.buffer[offset + 3] = (value & 0b11111111); + } + + readUInt8(offset: number): number { + return this.buffer[offset]; + } + + writeUInt8(value: number, offset: number): void { + this.buffer[offset] = value; + } + +} + +export interface BinaryBufferReadable extends streams.Readable { } +export namespace BinaryBufferReadable { + export function toBuffer(readable: BinaryBufferReadable): BinaryBuffer { + return streams.consumeReadable(readable, chunks => BinaryBuffer.concat(chunks)); + } + export function fromBuffer(buffer: BinaryBuffer): BinaryBufferReadable { + return streams.toReadable(buffer); + } +} + +export interface BinaryBufferReadableStream extends streams.ReadableStream { } +export namespace BinaryBufferReadableStream { + export function toBuffer(stream: BinaryBufferReadableStream): Promise { + return streams.consumeStream(stream, chunks => BinaryBuffer.concat(chunks)); + } + export function fromBuffer(buffer: BinaryBuffer): BinaryBufferReadableStream { + return streams.toStream(buffer, chunks => BinaryBuffer.concat(chunks)); + } +} + +export interface BinaryBufferWriteableStream extends streams.WriteableStream { } +export namespace BinaryBufferWriteableStream { + export function create(): BinaryBufferWriteableStream { + return streams.newWriteableStream(chunks => BinaryBuffer.concat(chunks)); + } +} diff --git a/packages/callhierarchy/src/common/char-code.ts b/packages/core/src/common/char-code.ts similarity index 100% rename from packages/callhierarchy/src/common/char-code.ts rename to packages/core/src/common/char-code.ts diff --git a/packages/core/src/common/encoding-service.ts b/packages/core/src/common/encoding-service.ts new file mode 100644 index 0000000000000..c50148007f048 --- /dev/null +++ b/packages/core/src/common/encoding-service.ts @@ -0,0 +1,224 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/services/textfile/common/encoding.ts + +import * as iconv from 'iconv-lite'; +import { Buffer } from 'safer-buffer'; +import { injectable } from 'inversify'; +import { BinaryBuffer } from './buffer'; +import { UTF8, UTF8_with_bom, UTF16be, UTF16le, UTF16be_BOM, UTF16le_BOM, UTF8_BOM } from './encodings'; + +const ZERO_BYTE_DETECTION_BUFFER_MAX_LEN = 512; // number of bytes to look at to decide about a file being binary or not +const AUTO_ENCODING_GUESS_MAX_BYTES = 512 * 128; // set an upper limit for the number of bytes we pass on to jschardet + +// we explicitly ignore a specific set of encodings from auto guessing +// - ASCII: we never want this encoding (most UTF-8 files would happily detect as +// ASCII files and then you could not type non-ASCII characters anymore) +// - UTF-16: we have our own detection logic for UTF-16 +// - UTF-32: we do not support this encoding in VSCode +const IGNORE_ENCODINGS = ['ascii', 'utf-16', 'utf-32']; + +export interface ResourceEncoding { + encoding: string + hasBOM: boolean +} + +export interface DetectedEncoding { + encoding?: string + seemsBinary?: boolean +} + +@injectable() +export class EncodingService { + + encode(value: string, options?: ResourceEncoding): BinaryBuffer { + let encoding = options?.encoding; + const addBOM = options?.hasBOM; + encoding = this.toIconvEncoding(encoding); + if (encoding === UTF8 && !addBOM) { + return BinaryBuffer.fromString(value); + } + const buffer = iconv.encode(value, encoding, { addBOM }); + return BinaryBuffer.wrap(buffer); + } + + decode(value: BinaryBuffer, encoding?: string): string { + const buffer = Buffer.from(value.buffer); + encoding = this.toIconvEncoding(encoding); + return iconv.decode(buffer, encoding); + } + + exists(encoding: string): boolean { + encoding = this.toIconvEncoding(encoding); + return iconv.encodingExists(encoding); + } + + toIconvEncoding(encoding?: string): string { + if (encoding === UTF8_with_bom || !encoding) { + return UTF8; // iconv does not distinguish UTF 8 with or without BOM, so we need to help it + } + return encoding; + } + + async toResourceEncoding(encoding: string, options: { + overwriteEncoding?: boolean, + read: (length: number) => Promise + }): Promise { + // Some encodings come with a BOM automatically + if (encoding === UTF16be || encoding === UTF16le || encoding === UTF8_with_bom) { + return { encoding, hasBOM: true }; + } + + // Ensure that we preserve an existing BOM if found for UTF8 + // unless we are instructed to overwrite the encoding + const overwriteEncoding = options?.overwriteEncoding; + if (!overwriteEncoding && encoding === UTF8) { + try { + // stream here to avoid fetching the whole content on write + const buffer = await options.read(UTF8_BOM.length); + if (this.detectEncodingByBOMFromBuffer(Buffer.from(buffer), buffer.byteLength) === UTF8_with_bom) { + return { encoding, hasBOM: true }; + } + } catch (error) { + // ignore - file might not exist + } + } + + return { encoding, hasBOM: false }; + } + + async detectEncoding(data: BinaryBuffer, autoGuessEncoding?: boolean): Promise { + const buffer = Buffer.from(data.buffer); + const bytesRead = data.byteLength; + // Always first check for BOM to find out abouÏt encoding + let encoding = this.detectEncodingByBOMFromBuffer(buffer, bytesRead); + + // Detect 0 bytes to see if file is binary or UTF-16 LE/BEÏ + // unless we already know that this file has a UTF-16 encoding + let seemsBinary = false; + if (encoding !== UTF16be && encoding !== UTF16le && buffer) { + let couldBeUTF16LE = true; // e.g. 0xAA 0x00 + let couldBeUTF16BE = true; // e.g. 0x00 0xAA + let containsZeroByte = false; + + // This is a simplified guess to detect UTF-16 BE or LE by just checking if + // the first 512 bytes have the 0-byte at a specific location. For UTF-16 LE + // this would be the odd byte index and for UTF-16 BE the even one. + // Note: this can produce false positives (a binary file that uses a 2-byte + // encoding of the same format as UTF-16) and false negatives (a UTF-16 file + // that is using 4 bytes to encode a character). + for (let i = 0; i < bytesRead && i < ZERO_BYTE_DETECTION_BUFFER_MAX_LEN; i++) { + const isEndian = (i % 2 === 1); // assume 2-byte sequences typical for UTF-16 + const isZeroByte = (buffer.readUInt8(i) === 0); + + if (isZeroByte) { + containsZeroByte = true; + } + + // UTF-16 LE: expect e.g. 0xAA 0x00 + if (couldBeUTF16LE && (isEndian && !isZeroByte || !isEndian && isZeroByte)) { + couldBeUTF16LE = false; + } + + // UTF-16 BE: expect e.g. 0x00 0xAA + if (couldBeUTF16BE && (isEndian && isZeroByte || !isEndian && !isZeroByte)) { + couldBeUTF16BE = false; + } + + // Return if this is neither UTF16-LE nor UTF16-BE and thus treat as binary + if (isZeroByte && !couldBeUTF16LE && !couldBeUTF16BE) { + break; + } + } + + // Handle case of 0-byte included + if (containsZeroByte) { + if (couldBeUTF16LE) { + encoding = UTF16le; + } else if (couldBeUTF16BE) { + encoding = UTF16be; + } else { + seemsBinary = true; + } + } + } + + // Auto guess encoding if configured + if (autoGuessEncoding && !seemsBinary && !encoding && buffer) { + const guessedEncoding = await this.guessEncodingByBuffer(buffer.slice(0, bytesRead)); + return { + seemsBinary: false, + encoding: guessedEncoding + }; + } + + return { seemsBinary, encoding }; + } + + protected detectEncodingByBOMFromBuffer(buffer: Buffer, bytesRead: number): typeof UTF8_with_bom | typeof UTF16le | typeof UTF16be | undefined { + if (!buffer || bytesRead < UTF16be_BOM.length) { + return undefined; + } + + const b0 = buffer.readUInt8(0); + const b1 = buffer.readUInt8(1); + + // UTF-16 BE + if (b0 === UTF16be_BOM[0] && b1 === UTF16be_BOM[1]) { + return UTF16be; + } + + // UTF-16 LE + if (b0 === UTF16le_BOM[0] && b1 === UTF16le_BOM[1]) { + return UTF16le; + } + + if (bytesRead < UTF8_BOM.length) { + return undefined; + } + + const b2 = buffer.readUInt8(2); + + // UTF-8 + if (b0 === UTF8_BOM[0] && b1 === UTF8_BOM[1] && b2 === UTF8_BOM[2]) { + return UTF8_with_bom; + } + + return undefined; + } + + protected async guessEncodingByBuffer(buffer: Buffer): Promise { + const jschardet = await import('jschardet'); + + const guessed = jschardet.detect(buffer.slice(0, AUTO_ENCODING_GUESS_MAX_BYTES)); // ensure to limit buffer for guessing due to https://github.com/aadsm/jschardet/issues/53 + if (!guessed || !guessed.encoding) { + return undefined; + } + + const enc = guessed.encoding.toLowerCase(); + if (0 <= IGNORE_ENCODINGS.indexOf(enc)) { + return undefined; // see comment above why we ignore some encodings + } + + return this.toIconvEncoding(guessed.encoding); + } + +} diff --git a/packages/filesystem/src/node/drivelist.d.ts b/packages/core/src/common/encodings.ts similarity index 72% rename from packages/filesystem/src/node/drivelist.d.ts rename to packages/core/src/common/encodings.ts index 00bd5d06db603..a1c6f1367b5ad 100644 --- a/packages/filesystem/src/node/drivelist.d.ts +++ b/packages/core/src/common/encodings.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018 TypeFox and others. + * Copyright (C) 2020 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -14,13 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -declare module 'drivelist' { +export const UTF8 = 'utf8'; +export const UTF8_with_bom = 'utf8bom'; +export const UTF16be = 'utf16be'; +export const UTF16le = 'utf16le'; - interface Drivelist { - list(cb: (error: Error, drives: ({ readonly mountpoints: { readonly path: string }[] })[]) => void): void; - } - - const drivelist: Drivelist; - - export = drivelist; -} +export const UTF16be_BOM = [0xFE, 0xFF]; +export const UTF16le_BOM = [0xFF, 0xFE]; +export const UTF8_BOM = [0xEF, 0xBB, 0xBF]; diff --git a/packages/core/src/common/env-variables/env-variables-protocol.ts b/packages/core/src/common/env-variables/env-variables-protocol.ts index 167f0b2a2e1e9..62823428f9d71 100644 --- a/packages/core/src/common/env-variables/env-variables-protocol.ts +++ b/packages/core/src/common/env-variables/env-variables-protocol.ts @@ -22,6 +22,14 @@ export interface EnvVariablesServer { getVariables(): Promise getValue(key: string): Promise getConfigDirUri(): Promise; + /** + * Resolves to a URI representing the current user's home directory. + */ + getHomeDirUri(): Promise; + /** + * Resolves to an array of URIs pointing to the available drives on the filesystem. + */ + getDrives(): Promise; } export interface EnvVariable { diff --git a/packages/core/src/common/event.ts b/packages/core/src/common/event.ts index 5ee7f64536a23..b72d94f1fd3be 100644 --- a/packages/core/src/common/event.ts +++ b/packages/core/src/common/event.ts @@ -152,7 +152,7 @@ export class Emitter { private static _noop = function (): void { }; private _event: Event; - private _callbacks: CallbackList | undefined; + protected _callbacks: CallbackList | undefined; private _disposed = false; private _leakingStacks: Map | undefined; @@ -199,8 +199,8 @@ export class Emitter { return result; }, { - maxListeners: Emitter.LEAK_WARNING_THRESHHOLD - } + maxListeners: Emitter.LEAK_WARNING_THRESHHOLD + } ); } return this._event; @@ -296,7 +296,6 @@ export class Emitter { } export interface WaitUntilEvent { - /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Allows to pause the event loop until the provided thenable resolved. * @@ -305,17 +304,20 @@ export interface WaitUntilEvent { * @param thenable A thenable that delays execution. */ waitUntil(thenable: Promise): void; - /* eslint-enable @typescript-eslint/no-explicit-any */ } export namespace WaitUntilEvent { + /** + * Fire all listeners in the same tick. + * + * Use `AsyncEmitter.fire` to fire listeners async one after another. + */ export async function fire( emitter: Emitter, - event: Pick>, + event: Omit, timeout: number | undefined = undefined ): Promise { const waitables: Promise[] = []; const asyncEvent = Object.assign(event, { - // eslint-disable-next-line @typescript-eslint/no-explicit-any waitUntil: (thenable: Promise) => { if (Object.isFrozen(waitables)) { throw new Error('waitUntil cannot be called asynchronously.'); @@ -340,3 +342,65 @@ export namespace WaitUntilEvent { } } } + +import { CancellationToken } from './cancellation'; + +export class AsyncEmitter extends Emitter { + + protected deliveryQueue: Promise | undefined; + + /** + * Fire listeners async one after another. + */ + fire(event: Omit, token: CancellationToken = CancellationToken.None, + promiseJoin?: (p: Promise, listener: Function) => Promise): Promise { + const callbacks = this._callbacks; + if (!callbacks) { + return Promise.resolve(); + } + const listeners = [...callbacks]; + if (this.deliveryQueue) { + return this.deliveryQueue = this.deliveryQueue.then(() => this.deliver(listeners, event, token, promiseJoin)); + } + return this.deliveryQueue = this.deliver(listeners, event, token, promiseJoin); + } + + protected async deliver(listeners: Callback[], event: Omit, token: CancellationToken, + promiseJoin?: (p: Promise, listener: Function) => Promise): Promise { + for (const listener of listeners) { + if (token.isCancellationRequested) { + return; + } + const waitables: Promise[] = []; + const asyncEvent = Object.assign(event, { + waitUntil: (thenable: Promise) => { + if (Object.isFrozen(waitables)) { + throw new Error('waitUntil cannot be called asynchronously.'); + } + if (promiseJoin) { + thenable = promiseJoin(thenable, listener); + } + waitables.push(thenable); + } + }) as T; + try { + listener(event); + // Asynchronous calls to `waitUntil` should fail. + Object.freeze(waitables); + } catch (e) { + console.error(e); + } finally { + delete asyncEvent['waitUntil']; + } + if (!waitables.length) { + return; + } + try { + await Promise.all(waitables); + } catch (e) { + console.error(e); + } + } + } + +} diff --git a/packages/core/src/common/messaging/proxy-factory.ts b/packages/core/src/common/messaging/proxy-factory.ts index 13311b12ace5b..815bde49b952c 100644 --- a/packages/core/src/common/messaging/proxy-factory.ts +++ b/packages/core/src/common/messaging/proxy-factory.ts @@ -41,11 +41,12 @@ export type JsonRpcProxy = T & JsonRpcConnectionEventEmitter; export class JsonRpcConnectionHandler implements ConnectionHandler { constructor( readonly path: string, - readonly targetFactory: (proxy: JsonRpcProxy) => any + readonly targetFactory: (proxy: JsonRpcProxy) => any, + readonly factoryConstructor: new () => JsonRpcProxyFactory = JsonRpcProxyFactory ) { } onConnection(connection: MessageConnection): void { - const factory = new JsonRpcProxyFactory(this.path); + const factory = new this.factoryConstructor(); const proxy = factory.createProxy(); factory.target = this.targetFactory(proxy); factory.listen(connection); diff --git a/packages/core/src/common/preferences/preference-schema.ts b/packages/core/src/common/preferences/preference-schema.ts index 1dcc1dd64effe..0737a5a6e3230 100644 --- a/packages/core/src/common/preferences/preference-schema.ts +++ b/packages/core/src/common/preferences/preference-schema.ts @@ -81,7 +81,7 @@ export interface PreferenceItem { export interface PreferenceSchemaProperty extends PreferenceItem { description?: string; markdownDescription?: string; - scope?: 'application' | 'window' | 'resource' | PreferenceScope; + scope?: 'application' | 'machine' | 'window' | 'resource' | 'language-overridable' | 'machine-overridable' | PreferenceScope; } export interface PreferenceDataProperty extends PreferenceItem { diff --git a/packages/core/src/common/promise-util.ts b/packages/core/src/common/promise-util.ts index 5de09bbb8cc21..fc67564242844 100644 --- a/packages/core/src/common/promise-util.ts +++ b/packages/core/src/common/promise-util.ts @@ -43,3 +43,19 @@ export function timeout(ms: number, token = CancellationToken.None): Promise(task: () => Promise, delay: number, retries: number): Promise { + let lastError: Error | undefined; + + for (let i = 0; i < retries; i++) { + try { + return await task(); + } catch (error) { + lastError = error; + + await timeout(delay); + } + } + + throw lastError; +} diff --git a/packages/core/src/common/resource.ts b/packages/core/src/common/resource.ts index 01e8029ff18da..6530f4c7b8e2e 100644 --- a/packages/core/src/common/resource.ts +++ b/packages/core/src/common/resource.ts @@ -32,8 +32,8 @@ export interface ResourceReadOptions { } export interface ResourceSaveOptions { - encoding?: string, - overwriteEncoding?: string, + encoding?: string + overwriteEncoding?: boolean version?: ResourceVersion } @@ -46,10 +46,18 @@ export interface Resource extends Disposable { * Undefined if a resource did not read content yet. */ readonly version?: ResourceVersion | undefined; + /** + * Latest read encoding of this resource. + * + * Optional if a resource does not support encoding, check with `in` operator`. + * Undefined if a resource did not read content yet. + */ + readonly encoding?: string | undefined; /** * Reads latest content of this resource. * * If a resource supports versioning it updates version to latest. + * If a resource supports encoding it updates encoding to latest. * * @throws `ResourceError.NotFound` if a resource not found */ @@ -60,7 +68,8 @@ export interface Resource extends Disposable { * * If a resource supports versioning clients can pass some version * to check against it, if it is not provided latest version is used. - * It updates version to latest. + * + * It updates version and encoding to latest. * * @throws `ResourceError.OutOfSync` if latest resource version is out of sync with the given */ diff --git a/packages/core/src/common/stream.ts b/packages/core/src/common/stream.ts new file mode 100644 index 0000000000000..8ddc9e2e95387 --- /dev/null +++ b/packages/core/src/common/stream.ts @@ -0,0 +1,504 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/base/common/stream.ts + +/* eslint-disable max-len */ +/* eslint-disable no-null/no-null */ +/* eslint-disable @typescript-eslint/tslint/config */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export interface ReadableStreamEvents { + + /** + * The 'data' event is emitted whenever the stream is + * relinquishing ownership of a chunk of data to a consumer. + */ + on(event: 'data', callback: (data: T) => void): void; + + /** + * Emitted when any error occurs. + */ + on(event: 'error', callback: (err: Error) => void): void; + + /** + * The 'end' event is emitted when there is no more data + * to be consumed from the stream. The 'end' event will + * not be emitted unless the data is completely consumed. + */ + on(event: 'end', callback: () => void): void; +} + +/** + * A interface that emulates the API shape of a node.js readable + * stream for use in desktop and web environments. + */ +export interface ReadableStream extends ReadableStreamEvents { + + /** + * Stops emitting any events until resume() is called. + */ + pause(): void; + + /** + * Starts emitting events again after pause() was called. + */ + resume(): void; + + /** + * Destroys the stream and stops emitting any event. + */ + destroy(): void; +} + +/** + * A interface that emulates the API shape of a node.js readable + * for use in desktop and web environments. + */ +export interface Readable { + + /** + * Read data from the underlying source. Will return + * null to indicate that no more data can be read. + */ + read(): T | null; +} + +/** + * A interface that emulates the API shape of a node.js writeable + * stream for use in desktop and web environments. + */ +export interface WriteableStream extends ReadableStream { + + /** + * Writing data to the stream will trigger the on('data') + * event listener if the stream is flowing and buffer the + * data otherwise until the stream is flowing. + */ + write(data: T): void; + + /** + * Signals an error to the consumer of the stream via the + * on('error') handler if the stream is flowing. + */ + error(error: Error): void; + + /** + * Signals the end of the stream to the consumer. If the + * result is not an error, will trigger the on('data') event + * listener if the stream is flowing and buffer the data + * otherwise until the stream is flowing. + * + * In case of an error, the on('error') event will be used + * if the stream is flowing. + */ + end(result?: T | Error): void; +} + +export function isReadableStream(obj: unknown): obj is ReadableStream { + const candidate = obj as ReadableStream; + + return candidate && [candidate.on, candidate.pause, candidate.resume, candidate.destroy].every(fn => typeof fn === 'function'); +} + +export interface IReducer { + (data: T[]): T; +} + +export interface IDataTransformer { + (data: Original): Transformed; +} + +export interface IErrorTransformer { + (error: Error): Error; +} + +export interface ITransformer { + data: IDataTransformer; + error?: IErrorTransformer; +} + +export function newWriteableStream(reducer: IReducer): WriteableStream { + return new WriteableStreamImpl(reducer); +} + +class WriteableStreamImpl implements WriteableStream { + + private readonly state = { + flowing: false, + ended: false, + destroyed: false + }; + + private readonly buffer = { + data: [] as T[], + error: [] as Error[] + }; + + private readonly listeners = { + data: [] as { (data: T): void }[], + error: [] as { (error: Error): void }[], + end: [] as { (): void }[] + }; + + constructor(private reducer: IReducer) { } + + pause(): void { + if (this.state.destroyed) { + return; + } + + this.state.flowing = false; + } + + resume(): void { + if (this.state.destroyed) { + return; + } + + if (!this.state.flowing) { + this.state.flowing = true; + + // emit buffered events + this.flowData(); + this.flowErrors(); + this.flowEnd(); + } + } + + write(data: T): void { + if (this.state.destroyed) { + return; + } + + // flowing: directly send the data to listeners + if (this.state.flowing) { + this.listeners.data.forEach(listener => listener(data)); + } + + // not yet flowing: buffer data until flowing + else { + this.buffer.data.push(data); + } + } + + error(error: Error): void { + if (this.state.destroyed) { + return; + } + + // flowing: directly send the error to listeners + if (this.state.flowing) { + this.listeners.error.forEach(listener => listener(error)); + } + + // not yet flowing: buffer errors until flowing + else { + this.buffer.error.push(error); + } + } + + end(result?: T | Error): void { + if (this.state.destroyed) { + return; + } + + // end with data or error if provided + if (result instanceof Error) { + this.error(result); + } else if (result) { + this.write(result); + } + + // flowing: send end event to listeners + if (this.state.flowing) { + this.listeners.end.forEach(listener => listener()); + + this.destroy(); + } + + // not yet flowing: remember state + else { + this.state.ended = true; + } + } + + on(event: 'data', callback: (data: T) => void): void; + on(event: 'error', callback: (err: Error) => void): void; + on(event: 'end', callback: () => void): void; + on(event: 'data' | 'error' | 'end', callback: (arg0?: any) => void): void { + if (this.state.destroyed) { + return; + } + + switch (event) { + case 'data': + this.listeners.data.push(callback); + + // switch into flowing mode as soon as the first 'data' + // listener is added and we are not yet in flowing mode + this.resume(); + + break; + + case 'end': + this.listeners.end.push(callback); + + // emit 'end' event directly if we are flowing + // and the end has already been reached + // + // finish() when it went through + if (this.state.flowing && this.flowEnd()) { + this.destroy(); + } + + break; + + case 'error': + this.listeners.error.push(callback); + + // emit buffered 'error' events unless done already + // now that we know that we have at least one listener + if (this.state.flowing) { + this.flowErrors(); + } + + break; + } + } + + private flowData(): void { + if (this.buffer.data.length > 0) { + const fullDataBuffer = this.reducer(this.buffer.data); + + this.listeners.data.forEach(listener => listener(fullDataBuffer)); + + this.buffer.data.length = 0; + } + } + + private flowErrors(): void { + if (this.listeners.error.length > 0) { + for (const error of this.buffer.error) { + this.listeners.error.forEach(listener => listener(error)); + } + + this.buffer.error.length = 0; + } + } + + private flowEnd(): boolean { + if (this.state.ended) { + this.listeners.end.forEach(listener => listener()); + + return this.listeners.end.length > 0; + } + + return false; + } + + destroy(): void { + if (!this.state.destroyed) { + this.state.destroyed = true; + this.state.ended = true; + + this.buffer.data.length = 0; + this.buffer.error.length = 0; + + this.listeners.data.length = 0; + this.listeners.error.length = 0; + this.listeners.end.length = 0; + } + } +} + +/** + * Helper to fully read a T readable into a T. + */ +export function consumeReadable(readable: Readable, reducer: IReducer): T { + const chunks: T[] = []; + + let chunk: T | null; + while ((chunk = readable.read()) !== null) { + chunks.push(chunk); + } + + return reducer(chunks); +} + +/** + * Helper to read a T readable up to a maximum of chunks. If the limit is + * reached, will return a readable instead to ensure all data can still + * be read. + */ +export function consumeReadableWithLimit(readable: Readable, reducer: IReducer, maxChunks: number): T | Readable { + const chunks: T[] = []; + + let chunk: T | null | undefined = undefined; + while ((chunk = readable.read()) !== null && chunks.length < maxChunks) { + chunks.push(chunk); + } + + // If the last chunk is null, it means we reached the end of + // the readable and return all the data at once + if (chunk === null && chunks.length > 0) { + return reducer(chunks); + } + + // Otherwise, we still have a chunk, it means we reached the maxChunks + // value and as such we return a new Readable that first returns + // the existing read chunks and then continues with reading from + // the underlying readable. + return { + read: () => { + + // First consume chunks from our array + if (chunks.length > 0) { + return chunks.shift()!; + } + + // Then ensure to return our last read chunk + if (typeof chunk !== 'undefined') { + const lastReadChunk = chunk; + + // explicitly use undefined here to indicate that we consumed + // the chunk, which could have either been null or valued. + chunk = undefined; + + return lastReadChunk; + } + + // Finally delegate back to the Readable + return readable.read(); + } + }; +} + +/** + * Helper to fully read a T stream into a T. + */ +export function consumeStream(stream: ReadableStream, reducer: IReducer): Promise { + return new Promise((resolve, reject) => { + const chunks: T[] = []; + + stream.on('data', data => chunks.push(data)); + stream.on('error', error => reject(error)); + stream.on('end', () => resolve(reducer(chunks))); + }); +} + +/** + * Helper to read a T stream up to a maximum of chunks. If the limit is + * reached, will return a stream instead to ensure all data can still + * be read. + */ +export function consumeStreamWithLimit(stream: ReadableStream, reducer: IReducer, maxChunks: number): Promise> { + return new Promise((resolve, reject) => { + const chunks: T[] = []; + + let wrapperStream: WriteableStream | undefined = undefined; + + stream.on('data', data => { + + // If we reach maxChunks, we start to return a stream + // and make sure that any data we have already read + // is in it as well + if (!wrapperStream && chunks.length === maxChunks) { + wrapperStream = newWriteableStream(reducer); + while (chunks.length) { + wrapperStream.write(chunks.shift()!); + } + + wrapperStream.write(data); + + return resolve(wrapperStream); + } + + if (wrapperStream) { + wrapperStream.write(data); + } else { + chunks.push(data); + } + }); + + stream.on('error', error => { + if (wrapperStream) { + wrapperStream.error(error); + } else { + return reject(error); + } + }); + + stream.on('end', () => { + if (wrapperStream) { + while (chunks.length) { + wrapperStream.write(chunks.shift()!); + } + + wrapperStream.end(); + } else { + return resolve(reducer(chunks)); + } + }); + }); +} + +/** + * Helper to create a readable stream from an existing T. + */ +export function toStream(t: T, reducer: IReducer): ReadableStream { + const stream = newWriteableStream(reducer); + + stream.end(t); + + return stream; +} + +/** + * Helper to convert a T into a Readable. + */ +export function toReadable(t: T): Readable { + let consumed = false; + + return { + read: () => { + if (consumed) { + return null; + } + + consumed = true; + + return t; + } + }; +} + +/** + * Helper to transform a readable stream into another stream. + */ +export function transform(stream: ReadableStreamEvents, transformer: ITransformer, reducer: IReducer): ReadableStream { + const target = newWriteableStream(reducer); + + stream.on('data', data => target.write(transformer.data(data))); + stream.on('end', () => target.end()); + stream.on('error', error => target.error(transformer.error ? transformer.error(error) : error)); + + return target; +} diff --git a/packages/core/src/common/strings.ts b/packages/core/src/common/strings.ts index 5f3db25ea6275..6f694090c0455 100644 --- a/packages/core/src/common/strings.ts +++ b/packages/core/src/common/strings.ts @@ -13,6 +13,115 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/base/common/strings.ts + +import { CharCode } from './char-code'; + +/** + * Determines if haystack ends with needle. + */ +export function endsWith(haystack: string, needle: string): boolean { + const diff = haystack.length - needle.length; + if (diff > 0) { + return haystack.indexOf(needle, diff) === diff; + } else if (diff === 0) { + return haystack === needle; + } else { + return false; + } +} +export function isLowerAsciiLetter(code: number): boolean { + return code >= CharCode.a && code <= CharCode.z; +} + +export function isUpperAsciiLetter(code: number): boolean { + return code >= CharCode.A && code <= CharCode.Z; +} + +function isAsciiLetter(code: number): boolean { + return isLowerAsciiLetter(code) || isUpperAsciiLetter(code); +} +export function equalsIgnoreCase(a: string, b: string): boolean { + const len1 = a ? a.length : 0; + const len2 = b ? b.length : 0; + + if (len1 !== len2) { + return false; + } + + return doEqualsIgnoreCase(a, b); +} + +function doEqualsIgnoreCase(a: string, b: string, stopAt = a.length): boolean { + if (typeof a !== 'string' || typeof b !== 'string') { + return false; + } + + for (let i = 0; i < stopAt; i++) { + const codeA = a.charCodeAt(i); + const codeB = b.charCodeAt(i); + + if (codeA === codeB) { + continue; + } + + // a-z A-Z + if (isAsciiLetter(codeA) && isAsciiLetter(codeB)) { + const diff = Math.abs(codeA - codeB); + if (diff !== 0 && diff !== 32) { + return false; + } + } + + // Any other charcode + // tslint:disable-next-line:one-line + else { + if (String.fromCharCode(codeA).toLowerCase() !== String.fromCharCode(codeB).toLowerCase()) { + return false; + } + } + } + + return true; +} + +/** + * @returns the length of the common prefix of the two strings. + */ +export function commonPrefixLength(a: string, b: string): number { + + let i: number; + const len = Math.min(a.length, b.length); + + for (i = 0; i < len; i++) { + if (a.charCodeAt(i) !== b.charCodeAt(i)) { + return i; + } + } + + return len; +} + +/** + * Escapes regular expression characters in a given string + */ +export function escapeRegExpCharacters(value: string): string { + return value.replace(/[\-\\\{\}\*\+\?\|\^\$\.\[\]\(\)\#]/g, '\\$&'); +} + +export function startsWithIgnoreCase(str: string, candidate: string): boolean { + const candidateLength = candidate.length; + if (candidate.length > str.length) { + return false; + } + + return doEqualsIgnoreCase(str, candidate, candidateLength); +} export function* split(s: string, splitter: string): IterableIterator { let start = 0; @@ -35,6 +144,77 @@ export function unescapeInvisibleChars(value: string): string { return value.replace(/\\n/g, '\n').replace(/\\r/g, '\r'); } -export function escapeRegExpCharacters(value: string): string { - return value.replace(/[\-\\\{\}\*\+\?\|\^\$\.\[\]\(\)\#]/g, '\\$&'); +export function compare(a: string, b: string): number { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } else { + return 0; + } +} + +export function compareSubstring(a: string, b: string, aStart: number = 0, aEnd: number = a.length, bStart: number = 0, bEnd: number = b.length): number { + for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) { + const codeA = a.charCodeAt(aStart); + const codeB = b.charCodeAt(bStart); + if (codeA < codeB) { + return -1; + } else if (codeA > codeB) { + return 1; + } + } + const aLen = aEnd - aStart; + const bLen = bEnd - bStart; + if (aLen < bLen) { + return -1; + } else if (aLen > bLen) { + return 1; + } + return 0; +} + +export function compareIgnoreCase(a: string, b: string): number { + return compareSubstringIgnoreCase(a, b, 0, a.length, 0, b.length); +} + +export function compareSubstringIgnoreCase(a: string, b: string, aStart: number = 0, aEnd: number = a.length, bStart: number = 0, bEnd: number = b.length): number { + + for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) { + + const codeA = a.charCodeAt(aStart); + const codeB = b.charCodeAt(bStart); + + if (codeA === codeB) { + // equal + continue; + } + + const diff = codeA - codeB; + if (diff === 32 && isUpperAsciiLetter(codeB)) { // codeB =[65-90] && codeA =[97-122] + continue; + + } else if (diff === -32 && isUpperAsciiLetter(codeA)) { // codeB =[97-122] && codeA =[65-90] + continue; + } + + if (isLowerAsciiLetter(codeA) && isLowerAsciiLetter(codeB)) { + // + return diff; + + } else { + return compareSubstring(a.toLowerCase(), b.toLowerCase(), aStart, aEnd, bStart, bEnd); + } + } + + const aLen = aEnd - aStart; + const bLen = bEnd - bStart; + + if (aLen < bLen) { + return -1; + } else if (aLen > bLen) { + return 1; + } + + return 0; } diff --git a/packages/core/src/common/ternary-search-tree.ts b/packages/core/src/common/ternary-search-tree.ts new file mode 100644 index 0000000000000..499f4b1d22ace --- /dev/null +++ b/packages/core/src/common/ternary-search-tree.ts @@ -0,0 +1,417 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/base/common/map.ts#L251 + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/tslint/config */ + +import URI from './uri'; +import { CharCode } from './char-code'; +import { compareSubstringIgnoreCase, compare, compareSubstring } from './strings'; + +export interface IKeyIterator { + reset(key: K): this; + next(): this; + + hasNext(): boolean; + cmp(a: string): number; + value(): string; +} + +export class PathIterator implements IKeyIterator { + + private _value!: string; + private _from!: number; + private _to!: number; + + constructor( + private readonly _splitOnBackslash: boolean = true, + private readonly _caseSensitive: boolean = true + ) { } + + reset(key: string): this { + this._value = key.replace(/\\$|\/$/, ''); + this._from = 0; + this._to = 0; + return this.next(); + } + + hasNext(): boolean { + return this._to < this._value.length; + } + + next(): this { + // this._data = key.split(/[\\/]/).filter(s => !!s); + this._from = this._to; + let justSeps = true; + for (; this._to < this._value.length; this._to++) { + const ch = this._value.charCodeAt(this._to); + if (ch === CharCode.Slash || this._splitOnBackslash && ch === CharCode.Backslash) { + if (justSeps) { + this._from++; + } else { + break; + } + } else { + justSeps = false; + } + } + return this; + } + + cmp(a: string): number { + return this._caseSensitive + ? compareSubstring(a, this._value, 0, a.length, this._from, this._to) + : compareSubstringIgnoreCase(a, this._value, 0, a.length, this._from, this._to); + } + + value(): string { + return this._value.substring(this._from, this._to); + } +} + +const enum UriIteratorState { + Scheme = 1, Authority = 2, Path = 3, Query = 4, Fragment = 5 +} + +export class UriIterator implements IKeyIterator { + + private _pathIterator!: PathIterator; + private _value!: URI; + private _states: UriIteratorState[] = []; + private _stateIdx: number = 0; + + constructor( + protected readonly caseSensitive: boolean + ) { } + + reset(key: URI): this { + this._value = key; + this._states = []; + if (this._value.scheme) { + this._states.push(UriIteratorState.Scheme); + } + if (this._value.authority) { + this._states.push(UriIteratorState.Authority); + } + if (this._value.path) { + this._pathIterator = new PathIterator(false, this.caseSensitive); + this._pathIterator.reset(key.path.toString()); + if (this._pathIterator.value()) { + this._states.push(UriIteratorState.Path); + } + } + if (this._value.query) { + this._states.push(UriIteratorState.Query); + } + if (this._value.fragment) { + this._states.push(UriIteratorState.Fragment); + } + this._stateIdx = 0; + return this; + } + + next(): this { + if (this._states[this._stateIdx] === UriIteratorState.Path && this._pathIterator.hasNext()) { + this._pathIterator.next(); + } else { + this._stateIdx += 1; + } + return this; + } + + hasNext(): boolean { + return (this._states[this._stateIdx] === UriIteratorState.Path && this._pathIterator.hasNext()) + || this._stateIdx < this._states.length - 1; + } + + cmp(a: string): number { + if (this._states[this._stateIdx] === UriIteratorState.Scheme) { + return compareSubstringIgnoreCase(a, this._value.scheme); + } else if (this._states[this._stateIdx] === UriIteratorState.Authority) { + return compareSubstringIgnoreCase(a, this._value.authority); + } else if (this._states[this._stateIdx] === UriIteratorState.Path) { + return this._pathIterator.cmp(a); + } else if (this._states[this._stateIdx] === UriIteratorState.Query) { + return compare(a, this._value.query); + } else if (this._states[this._stateIdx] === UriIteratorState.Fragment) { + return compare(a, this._value.fragment); + } + throw new Error(); + } + + value(): string { + if (this._states[this._stateIdx] === UriIteratorState.Scheme) { + return this._value.scheme; + } else if (this._states[this._stateIdx] === UriIteratorState.Authority) { + return this._value.authority; + } else if (this._states[this._stateIdx] === UriIteratorState.Path) { + return this._pathIterator.value(); + } else if (this._states[this._stateIdx] === UriIteratorState.Query) { + return this._value.query; + } else if (this._states[this._stateIdx] === UriIteratorState.Fragment) { + return this._value.fragment; + } + throw new Error(); + } +} + +class TernarySearchTreeNode { + segment!: string; + value: V | undefined; + key!: K; + left: TernarySearchTreeNode | undefined; + mid: TernarySearchTreeNode | undefined; + right: TernarySearchTreeNode | undefined; + + isEmpty(): boolean { + return !this.left && !this.mid && !this.right && !this.value; + } +} + +export class TernarySearchTree { + + static forUris(caseSensitive: boolean): TernarySearchTree { + return new TernarySearchTree(new UriIterator(caseSensitive)); + } + + static forPaths(): TernarySearchTree { + return new TernarySearchTree(new PathIterator()); + } + + private _iter: IKeyIterator; + private _root: TernarySearchTreeNode | undefined; + + constructor(segments: IKeyIterator) { + this._iter = segments; + } + + clear(): void { + this._root = undefined; + } + + set(key: K, element: V): V | undefined { + const iter = this._iter.reset(key); + let node: TernarySearchTreeNode; + + if (!this._root) { + this._root = new TernarySearchTreeNode(); + this._root.segment = iter.value(); + } + + node = this._root; + while (true) { + const val = iter.cmp(node.segment); + if (val > 0) { + // left + if (!node.left) { + node.left = new TernarySearchTreeNode(); + node.left.segment = iter.value(); + } + node = node.left; + + } else if (val < 0) { + // right + if (!node.right) { + node.right = new TernarySearchTreeNode(); + node.right.segment = iter.value(); + } + node = node.right; + + } else if (iter.hasNext()) { + // mid + iter.next(); + if (!node.mid) { + node.mid = new TernarySearchTreeNode(); + node.mid.segment = iter.value(); + } + node = node.mid; + } else { + break; + } + } + const oldElement = node.value; + node.value = element; + node.key = key; + return oldElement; + } + + get(key: K): V | undefined { + const iter = this._iter.reset(key); + let node = this._root; + while (node) { + const val = iter.cmp(node.segment); + if (val > 0) { + // left + node = node.left; + } else if (val < 0) { + // right + node = node.right; + } else if (iter.hasNext()) { + // mid + iter.next(); + node = node.mid; + } else { + break; + } + } + return node ? node.value : undefined; + } + + delete(key: K): void { + + const iter = this._iter.reset(key); + const stack: [-1 | 0 | 1, TernarySearchTreeNode][] = []; + let node = this._root; + + // find and unset node + while (node) { + const val = iter.cmp(node.segment); + if (val > 0) { + // left + stack.push([1, node]); + node = node.left; + } else if (val < 0) { + // right + stack.push([-1, node]); + node = node.right; + } else if (iter.hasNext()) { + // mid + iter.next(); + stack.push([0, node]); + node = node.mid; + } else { + // remove element + node.value = undefined; + + // clean up empty nodes + while (stack.length > 0 && node.isEmpty()) { + const [dir, parent] = stack.pop()!; + switch (dir) { + case 1: parent.left = undefined; break; + case 0: parent.mid = undefined; break; + case -1: parent.right = undefined; break; + } + node = parent; + } + break; + } + } + } + + findSubstr(key: K): V | undefined { + const iter = this._iter.reset(key); + let node = this._root; + let candidate: V | undefined = undefined; + while (node) { + const val = iter.cmp(node.segment); + if (val > 0) { + // left + node = node.left; + } else if (val < 0) { + // right + node = node.right; + } else if (iter.hasNext()) { + // mid + iter.next(); + candidate = node.value || candidate; + node = node.mid; + } else { + break; + } + } + return node && node.value || candidate; + } + + findSuperstr(key: K): Iterator | undefined { + const iter = this._iter.reset(key); + let node = this._root; + while (node) { + const val = iter.cmp(node.segment); + if (val > 0) { + // left + node = node.left; + } else if (val < 0) { + // right + node = node.right; + } else if (iter.hasNext()) { + // mid + iter.next(); + node = node.mid; + } else { + // collect + if (!node.mid) { + return undefined; + } else { + return this._nodeIterator(node.mid); + } + } + } + return undefined; + } + + private _nodeIterator(node: TernarySearchTreeNode): Iterator { + let res: { done: false; value: V; }; + let idx: number; + let data: V[]; + const next = (): IteratorResult => { + if (!data) { + // lazy till first invocation + data = []; + idx = 0; + this._forEach(node, value => data.push(value)); + } + if (idx >= data.length) { + return { done: true, value: undefined }; + } + + if (!res) { + res = { done: false, value: data[idx++] }; + } else { + res.value = data[idx++]; + } + return res; + }; + return { next }; + } + + forEach(callback: (value: V, index: K) => any) { + this._forEach(this._root, callback); + } + + private _forEach(node: TernarySearchTreeNode | undefined, callback: (value: V, index: K) => any) { + if (node) { + // left + this._forEach(node.left, callback); + + // node + if (node.value) { + // callback(node.value, this._iter.join(parts)); + callback(node.value, node.key); + } + // mid + this._forEach(node.mid, callback); + + // right + this._forEach(node.right, callback); + } + } +} diff --git a/packages/core/src/common/uri.ts b/packages/core/src/common/uri.ts index acc5ac2bb3662..e60fce5462b22 100644 --- a/packages/core/src/common/uri.ts +++ b/packages/core/src/common/uri.ts @@ -199,8 +199,17 @@ export default class URI { return this.codeUri.toString(skipEncoding); } - isEqualOrParent(uri: URI): boolean { - return this.authority === uri.authority && this.scheme === uri.scheme && this.path.isEqualOrParent(uri.path); + isEqualOrParent(uri: URI, caseSensitive: boolean = true): boolean { + if (this.authority !== uri.authority || this.scheme !== uri.scheme) { + return false; + } + let left = this.path; + let right = uri.path; + if (!caseSensitive) { + left = new Path(left.toString().toLowerCase()); + right = new Path(right.toString().toLowerCase()); + } + return left.isEqualOrParent(right); } static getDistinctParents(uris: URI[]): URI[] { diff --git a/packages/core/src/node/env-variables/env-variables-server.ts b/packages/core/src/node/env-variables/env-variables-server.ts index 650dd8a703868..92644e0a53625 100644 --- a/packages/core/src/node/env-variables/env-variables-server.ts +++ b/packages/core/src/node/env-variables/env-variables-server.ts @@ -17,6 +17,7 @@ import { join } from 'path'; import { homedir } from 'os'; import { injectable } from 'inversify'; +import * as drivelist from 'drivelist'; import { EnvVariable, EnvVariablesServer } from '../../common/env-variables'; import { isWindows } from '../../common/os'; import { FileUri } from '../file-uri'; @@ -25,6 +26,7 @@ import { FileUri } from '../file-uri'; export class EnvVariablesServerImpl implements EnvVariablesServer { protected readonly envs: { [key: string]: EnvVariable } = {}; + protected readonly homeDirUri = FileUri.create(homedir()).toString(); protected readonly configDirUri: Promise; constructor() { @@ -63,4 +65,36 @@ export class EnvVariablesServerImpl implements EnvVariablesServer { return this.configDirUri; } + async getHomeDirUri(): Promise { + return this.homeDirUri; + } + + async getDrives(): Promise { + const uris: string[] = []; + const drives = await drivelist.list(); + for (const drive of drives) { + for (const mounpoint of drive.mountpoints) { + if (this.filterHiddenPartitions(mounpoint.path)) { + uris.push(FileUri.create(mounpoint.path).toString()); + } + } + } + return uris; + } + + /** + * Filters hidden and system partitions. + */ + protected filterHiddenPartitions(path: string): boolean { + // OS X: This is your sleep-image. When your Mac goes to sleep it writes the contents of its memory to the hard disk. (https://bit.ly/2R6cztl) + if (path === '/private/var/vm') { + return false; + } + // Ubuntu: This system partition is simply the boot partition created when the computers mother board runs UEFI rather than BIOS. (https://bit.ly/2N5duHr) + if (path === '/boot/efi') { + return false; + } + return true; + } + } diff --git a/packages/debug/src/browser/debug-configuration-manager.ts b/packages/debug/src/browser/debug-configuration-manager.ts index ce25e327a44ac..9121f1ad85764 100644 --- a/packages/debug/src/browser/debug-configuration-manager.ts +++ b/packages/debug/src/browser/debug-configuration-manager.ts @@ -35,8 +35,8 @@ import { DebugService } from '../common/debug-service'; import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { DebugConfiguration } from '../common/debug-common'; import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution'; -import { FileSystem, FileSystemError } from '@theia/filesystem/lib/common'; import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export interface WillProvideDebugConfiguration extends WaitUntilEvent { } @@ -56,8 +56,8 @@ export class DebugConfigurationManager { @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; - @inject(FileSystem) - protected readonly filesystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; @inject(PreferenceService) protected readonly preferences: PreferenceService; @@ -93,7 +93,7 @@ export class DebugConfigurationManager { const roots = await this.workspaceService.roots; const toDelete = new Set(this.models.keys()); for (const rootStat of roots) { - const key = rootStat.uri; + const key = rootStat.resource.toString(); toDelete.delete(key); if (!this.models.has(key)) { const model = new DebugConfigurationModel(key, this.preferences); @@ -273,17 +273,7 @@ export class DebugConfigurationManager { const debugType = await this.selectDebugType(); const configurations = debugType ? await this.provideDebugConfigurations(debugType, model.workspaceFolderUri) : []; const content = this.getInitialConfigurationContent(configurations); - const fileStat = await this.filesystem.getFileStat(uri.toString()); - if (!fileStat) { - throw new Error(`file not found: ${uri.toString()}`); - } - try { - await this.filesystem.setContent(fileStat, content); - } catch (e) { - if (!FileSystemError.FileExists.is(e)) { - throw e; - } - } + await this.fileService.write(uri, content); return uri; } diff --git a/packages/debug/src/browser/debug-session-contribution.ts b/packages/debug/src/browser/debug-session-contribution.ts index 5c9c4569b9c26..9b00a1786d3aa 100644 --- a/packages/debug/src/browser/debug-session-contribution.ts +++ b/packages/debug/src/browser/debug-session-contribution.ts @@ -29,7 +29,7 @@ import { DebugSessionConnection } from './debug-session-connection'; import { IWebSocket } from 'vscode-ws-jsonrpc/lib/socket/socket'; import { DebugAdapterPath } from '../common/debug-service'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; -import { FileSystem } from '@theia/filesystem/lib/common'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; /** * DebugSessionContribution symbol for DI. @@ -112,8 +112,8 @@ export class DefaultDebugSessionFactory implements DebugSessionFactory { protected readonly outputChannelManager: OutputChannelManager; @inject(DebugPreferences) protected readonly debugPreferences: DebugPreferences; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; get(sessionId: string, options: DebugSessionOptions): DebugSession { const connection = new DebugSessionConnection( @@ -133,7 +133,7 @@ export class DefaultDebugSessionFactory implements DebugSessionFactory { this.breakpoints, this.labelProvider, this.messages, - this.fileSystem); + this.fileService); } protected getTraceOutputChannel(): OutputChannel | undefined { diff --git a/packages/debug/src/browser/debug-session.tsx b/packages/debug/src/browser/debug-session.tsx index 695be24359c31..b466c35c54009 100644 --- a/packages/debug/src/browser/debug-session.tsx +++ b/packages/debug/src/browser/debug-session.tsx @@ -36,9 +36,9 @@ import { BreakpointManager } from './breakpoint/breakpoint-manager'; import { DebugSessionOptions, InternalDebugSessionOptions } from './debug-session-options'; import { DebugConfiguration } from '../common/debug-common'; import { SourceBreakpoint, ExceptionBreakpoint } from './breakpoint/breakpoint-marker'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { TerminalWidgetOptions, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { DebugFunctionBreakpoint } from './model/debug-function-breakpoint'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export enum DebugState { Inactive, @@ -73,7 +73,7 @@ export class DebugSession implements CompositeTreeElement { protected readonly breakpoints: BreakpointManager, protected readonly labelProvider: LabelProvider, protected readonly messages: MessageClient, - protected readonly fileSystem: FileSystem) { + protected readonly fileService: FileService) { this.connection.onRequest('runInTerminal', (request: DebugProtocol.RunInTerminalRequest) => this.runInTerminal(request)); this.toDispose.pushAll([ this.onDidChangeEmitter, @@ -153,9 +153,12 @@ export class DebugSession implements CompositeTreeElement { }; } const name = uri.displayName; - let path: string | undefined = uri.toString(); - if (uri.scheme === 'file') { - path = await this.fileSystem.getFsPath(path); + let path; + const underlying = await this.fileService.toUnderlyingResource(uri); + if (underlying.scheme === 'file') { + path = await this.fileService.fsPath(underlying); + } else { + path = uri.toString(); } return { name, path }; } diff --git a/packages/editor/src/browser/editor-command.ts b/packages/editor/src/browser/editor-command.ts index 5434961cb462e..51d52ec98cff8 100644 --- a/packages/editor/src/browser/editor-command.ts +++ b/packages/editor/src/browser/editor-command.ts @@ -21,9 +21,9 @@ import { CommonCommands, PreferenceService, QuickPickItem, QuickPickService, Lab import { EditorManager } from './editor-manager'; import { EncodingMode } from './editor'; import { EditorPreferences } from './editor-preferences'; -import { SUPPORTED_ENCODINGS } from './supported-encodings'; import { ResourceProvider, MessageService } from '@theia/core'; import { LanguageService, Language } from '@theia/core/lib/browser/language-service'; +import { SUPPORTED_ENCODINGS } from '@theia/core/lib/browser/supported-encodings'; export namespace EditorCommands { @@ -260,7 +260,7 @@ export class EditorCommandContribution implements CommandContribution { } const isReopenWithEncoding = (action === reopenWithEncodingPick.value); - const configuredEncoding = this.editorPreferences.get('files.encoding'); + const configuredEncoding = this.preferencesService.get('files.encoding', 'utf8', editor.uri.toString()); const resource = await this.resourceProvider(editor.uri); const guessedEncoding = resource.guessEncoding ? await resource.guessEncoding() : undefined; diff --git a/packages/editor/src/browser/editor-contribution.ts b/packages/editor/src/browser/editor-contribution.ts index 2f0e848cef924..7f2466e960902 100644 --- a/packages/editor/src/browser/editor-contribution.ts +++ b/packages/editor/src/browser/editor-contribution.ts @@ -25,8 +25,8 @@ import { EditorCommands } from './editor-command'; import { EditorQuickOpenService } from './editor-quick-open-service'; import { CommandRegistry, CommandContribution } from '@theia/core/lib/common'; import { KeybindingRegistry, KeybindingContribution, QuickOpenContribution, QuickOpenHandlerRegistry } from '@theia/core/lib/browser'; -import { SUPPORTED_ENCODINGS } from './supported-encodings'; import { LanguageService } from '@theia/core/lib/browser/language-service'; +import { SUPPORTED_ENCODINGS } from '@theia/core/lib/browser/supported-encodings'; @injectable() export class EditorContribution implements FrontendApplicationContribution, CommandContribution, KeybindingContribution, QuickOpenContribution { diff --git a/packages/editor/src/browser/editor-preferences.ts b/packages/editor/src/browser/editor-preferences.ts index cf2169a2f485e..971aa6f8d0d08 100644 --- a/packages/editor/src/browser/editor-preferences.ts +++ b/packages/editor/src/browser/editor-preferences.ts @@ -25,7 +25,6 @@ import { PreferenceSchemaProperties } from '@theia/core/lib/browser/preferences'; import { isWindows, isOSX, OS } from '@theia/core/lib/common/os'; -import { SUPPORTED_ENCODINGS } from './supported-encodings'; const DEFAULT_WINDOWS_FONT_FAMILY = 'Consolas, \'Courier New\', monospace'; const DEFAULT_MAC_FONT_FAMILY = 'Menlo, Monaco, \'Courier New\', monospace'; @@ -1225,11 +1224,6 @@ export const editorPreferenceSchema: PreferenceSchema = { ], 'default': 'auto', 'description': 'The default end of line character.' - }, - 'files.encoding': { - 'enum': Object.keys(SUPPORTED_ENCODINGS).sort(), - 'default': 'utf8', - 'description': 'The default character set encoding to use when reading and writing files.' } } }; @@ -1248,7 +1242,6 @@ export interface EditorConfiguration extends CodeEditorConfiguration { 'editor.formatOnSave': boolean 'editor.formatOnSaveTimeout': number 'files.eol': EndOfLinePreference - 'files.encoding': string } export type EndOfLinePreference = '\n' | '\r\n' | 'auto'; diff --git a/packages/file-search/src/browser/quick-file-open.ts b/packages/file-search/src/browser/quick-file-open.ts index 741d49cf93037..38887a3409d8c 100644 --- a/packages/file-search/src/browser/quick-file-open.ts +++ b/packages/file-search/src/browser/quick-file-open.ts @@ -20,7 +20,6 @@ import { OpenerService, KeybindingRegistry, QuickOpenGroupItem, QuickOpenGroupItemOptions, QuickOpenItemOptions, QuickOpenHandler, QuickOpenOptions } from '@theia/core/lib/browser'; -import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import URI from '@theia/core/lib/common/uri'; import { FileSearchService } from '../common/file-search-service'; @@ -42,8 +41,6 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler { @inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(OpenerService) @@ -197,7 +194,7 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler { }; this.fileSearchService.find(lookFor, { - rootUris: roots.map(r => r.uri), + rootUris: roots.map(r => r.resource.toString()), fuzzyMatch: true, limit: 200, useGitIgnore: this.hideIgnoredFiles, diff --git a/packages/filesystem/package.json b/packages/filesystem/package.json index 6938b6fd36a8b..60d7eac6cc4a5 100644 --- a/packages/filesystem/package.json +++ b/packages/filesystem/package.json @@ -8,21 +8,15 @@ "@types/body-parser": "^1.17.0", "@types/rimraf": "^2.0.2", "@types/tar-fs": "^1.16.1", - "@types/touch": "0.0.1", "@types/uuid": "^7.0.3", "body-parser": "^1.18.3", - "drivelist": "^6.4.3", "http-status-codes": "^1.3.0", - "iconv-lite": "0.4.23", - "jschardet": "1.6.0", "minimatch": "^3.0.4", - "mv": "^2.1.1", "rimraf": "^2.6.2", "tar-fs": "^1.16.2", - "touch": "^3.1.0", - "trash": "^4.0.1", + "trash": "^6.1.1", "uuid": "^8.0.0", - "zip-dir": "^1.0.2" + "vscode-languageserver-textdocument": "^1.0.1" }, "publishConfig": { "access": "public" diff --git a/packages/filesystem/src/browser/download/file-download-service.ts b/packages/filesystem/src/browser/download/file-download-service.ts index a5141626c8e56..cfc801ad244bd 100644 --- a/packages/filesystem/src/browser/download/file-download-service.ts +++ b/packages/filesystem/src/browser/download/file-download-service.ts @@ -18,7 +18,6 @@ import { inject, injectable } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { ILogger } from '@theia/core/lib/common/logger'; import { Endpoint } from '@theia/core/lib/browser/endpoint'; -import { FileSystem } from '../../common/filesystem'; import { FileDownloadData } from '../../common/download/file-download-data'; import { MessageService } from '@theia/core/lib/common/message-service'; import { addClipboardListener } from '@theia/core/lib/browser/widgets'; @@ -32,9 +31,6 @@ export class FileDownloadService { @inject(ILogger) protected readonly logger: ILogger; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; - @inject(MessageService) protected readonly messageService: MessageService; diff --git a/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts b/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts index ef8dc048af9de..a813ed8cd6d22 100644 --- a/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts +++ b/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts @@ -18,9 +18,11 @@ import { injectable, inject } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { MaybeArray } from '@theia/core/lib/common'; import { LabelProvider } from '@theia/core/lib/browser'; -import { FileSystem, FileStat } from '../../common'; +import { FileStat } from '../../common/files'; import { DirNode } from '../file-tree'; import { OpenFileDialogFactory, OpenFileDialogProps, SaveFileDialogFactory, SaveFileDialogProps } from './file-dialog'; +import { FileService } from '../file-service'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; export const FileDialogService = Symbol('FileDialogService'); export interface FileDialogService { @@ -34,9 +36,14 @@ export interface FileDialogService { } @injectable() -export class DefaultFileDialogService { +export class DefaultFileDialogService implements FileDialogService { + + @inject(EnvVariablesServer) + protected readonly environments: EnvVariablesServer; + + @inject(FileService) + protected readonly fileService: FileService; - @inject(FileSystem) protected readonly fileSystem: FileSystem; @inject(OpenFileDialogFactory) protected readonly openFileDialogFactory: OpenFileDialogFactory; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @inject(SaveFileDialogFactory) protected readonly saveFileDialogFactory: SaveFileDialogFactory; @@ -72,14 +79,17 @@ export class DefaultFileDialogService { } protected async getRootNode(folderToOpen?: FileStat): Promise { - const folder = folderToOpen || await this.fileSystem.getCurrentUserHome(); + const folder = folderToOpen || { + resource: new URI(await this.environments.getHomeDirUri()), + isDirectory: true + }; if (folder) { - const folderUri = new URI(folder.uri); + const folderUri = folder.resource; const rootUri = folder.isDirectory ? folderUri : folderUri.parent; - const rootStat = await this.fileSystem.getFileStat(rootUri.toString()); - if (rootStat) { + try { + const rootStat = await this.fileService.resolve(rootUri); return DirNode.createRoot(rootStat); - } + } catch { } } return undefined; } diff --git a/packages/filesystem/src/browser/file-dialog/file-dialog-tree.ts b/packages/filesystem/src/browser/file-dialog/file-dialog-tree.ts index fb0abb48d0d42..dbbc70d39d748 100644 --- a/packages/filesystem/src/browser/file-dialog/file-dialog-tree.ts +++ b/packages/filesystem/src/browser/file-dialog/file-dialog-tree.ts @@ -17,7 +17,7 @@ import { injectable } from 'inversify'; import { DirNode, FileTree } from '../file-tree'; import { TreeNode, CompositeTreeNode } from '@theia/core/lib/browser/tree/tree'; -import { FileStat } from '../../common'; +import { FileStat } from '../../common/files'; @injectable() export class FileDialogTree extends FileTree { @@ -65,7 +65,7 @@ export class FileDialogTree extends FileTree { return true; } - return !this.fileExtensions.every(value => !fileStat.uri.endsWith('.' + value)); + return !this.fileExtensions.every(value => fileStat.resource.path.ext !== '.' + value); } } diff --git a/packages/filesystem/src/browser/file-resource.ts b/packages/filesystem/src/browser/file-resource.ts index 43a5c5024fae3..51024639811e2 100644 --- a/packages/filesystem/src/browser/file-resource.ts +++ b/packages/filesystem/src/browser/file-resource.ts @@ -15,23 +15,30 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; import { Resource, ResourceVersion, ResourceResolver, ResourceError, ResourceSaveOptions } from '@theia/core/lib/common/resource'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { Emitter, Event } from '@theia/core/lib/common/event'; import URI from '@theia/core/lib/common/uri'; -import { FileSystem, FileStat, FileSystemError } from '../common/filesystem'; -import { FileSystemWatcher, FileChangeEvent } from './filesystem-watcher'; +import { FileOperation, FileOperationError, FileOperationResult, ETAG_DISABLED, FileSystemProviderCapabilities } from '../common/files'; +import { FileService } from './file-service'; +import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; +import { LabelProvider } from '@theia/core/lib/browser/label-provider'; export interface FileResourceVersion extends ResourceVersion { - readonly stat: FileStat + readonly encoding: string; + readonly mtime: number; + readonly etag: string; } export namespace FileResourceVersion { export function is(version: ResourceVersion | undefined): version is FileResourceVersion { - return !!version && 'stat' in version && FileStat.is(version['stat']); + return !!version && 'encoding' in version && 'mtime' in version && 'etag' in version; } } +export interface FileResourceOptions { + shouldOverwrite: () => Promise +} + export class FileResource implements Resource { protected readonly toDispose = new DisposableCollection(); @@ -42,44 +49,37 @@ export class FileResource implements Resource { get version(): FileResourceVersion | undefined { return this._version; } - - protected uriString: string; + get encoding(): string | undefined { + return this._version?.encoding; + } constructor( readonly uri: URI, - protected readonly fileSystem: FileSystem, - protected readonly fileSystemWatcher: FileSystemWatcher + protected readonly fileService: FileService, + protected readonly options: FileResourceOptions ) { - this.uriString = this.uri.toString(); this.toDispose.push(this.onDidChangeContentsEmitter); - } - - async init(): Promise { - const stat = await this.getFileStat(); - if (stat && stat.isDirectory) { - throw new Error('The given uri is a directory: ' + this.uriString); - } - - this.toDispose.push(this.fileSystemWatcher.onFilesChanged(event => { - if (FileChangeEvent.isAffected(event, this.uri)) { + this.toDispose.push(this.fileService.onDidFilesChange(event => { + if (event.contains(this.uri)) { this.sync(); } })); - this.toDispose.push(this.fileSystemWatcher.onDidDelete(event => { - if (event.uri.isEqualOrParent(this.uri)) { - this.sync(); - } - })); - this.toDispose.push(this.fileSystemWatcher.onDidMove(event => { - if (event.sourceUri.isEqualOrParent(this.uri) || event.targetUri.isEqualOrParent(this.uri)) { + this.toDispose.push(this.fileService.onDidRunOperation(e => { + if ((e.isOperation(FileOperation.DELETE) || e.isOperation(FileOperation.MOVE)) && e.resource.isEqualOrParent(this.uri)) { this.sync(); } })); try { - this.toDispose.push(await this.fileSystemWatcher.watchFileChanges(this.uri)); + this.toDispose.push(this.fileService.watch(this.uri)); } catch (e) { console.error(e); } + this.updateSavingContentChanges(); + this.toDispose.push(this.fileService.onDidChangeFileSystemProviderCapabilities(e => { + if (e.scheme === this.uri.scheme) { + this.updateSavingContentChanges(); + } + })); } dispose(): void { @@ -88,14 +88,20 @@ export class FileResource implements Resource { async readContents(options?: { encoding?: string }): Promise { try { - const { stat, content } = await this.fileSystem.resolveContent(this.uriString, options); - this._version = { stat }; - return content; + const encoding = options?.encoding || this.version?.encoding; + const stat = await this.fileService.read(this.uri, { encoding, etag: ETAG_DISABLED }); + this._version = { + encoding: stat.encoding, + etag: stat.etag, + mtime: stat.mtime + }; + return stat.value; } catch (e) { - if (FileSystemError.FileNotFound.is(e)) { + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { this._version = undefined; + const { message, stack } = e; throw ResourceError.NotFound({ - ...e.toJson(), + message, stack, data: { uri: this.uri } @@ -106,61 +112,77 @@ export class FileResource implements Resource { } async saveContents(content: string, options?: ResourceSaveOptions): Promise { + const version = options?.version || this._version; + const current = FileResourceVersion.is(version) ? version : undefined; + const etag = current?.etag; try { - let resolvedOptions = options; - if (options && options.overwriteEncoding) { - resolvedOptions = { - ...options, - encoding: options.overwriteEncoding - }; - delete resolvedOptions.overwriteEncoding; - } - const stat = await this.doSaveContents(content, resolvedOptions); - this._version = { stat }; + const stat = await this.fileService.write(this.uri, content, { + encoding: options?.encoding, + overwriteEncoding: options?.overwriteEncoding, + etag, + mtime: current?.mtime + }); + this._version = { + etag: stat.etag, + mtime: stat.mtime, + encoding: stat.encoding + }; } catch (e) { - if (FileSystemError.FileIsOutOfSync.is(e)) { - throw ResourceError.OutOfSync({ ...e.toJson(), data: { uri: this.uri } }); + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { + if (etag !== ETAG_DISABLED && await this.shouldOverwrite()) { + return this.saveContents(content, { ...options, version: { stat: { ...current, etag: ETAG_DISABLED } } }); + } + const { message, stack } = e; + throw ResourceError.OutOfSync({ message, stack, data: { uri: this.uri } }); } throw e; } } - protected async doSaveContents(content: string, options?: { encoding?: string, version?: ResourceVersion }): Promise { - const version = options && options.version || this._version; - const stat = FileResourceVersion.is(version) && version.stat || await this.getFileStat(); - if (stat) { - try { - return await this.fileSystem.setContent(stat, content, options); - } catch (e) { - if (!FileSystemError.FileNotFound.is(e)) { - throw e; - } - } + + saveContentChanges?: Resource['saveContentChanges']; + protected updateSavingContentChanges(): void { + if (this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Update)) { + this.saveContentChanges = this.doSaveContentChanges; + } else { + delete this.saveContentChanges; } - return this.fileSystem.createFile(this.uriString, { content, ...options }); } - - async saveContentChanges(changes: TextDocumentContentChangeEvent[], options?: ResourceSaveOptions): Promise { - const version = options && options.version || this._version; - const currentStat = FileResourceVersion.is(version) && version.stat; - if (!currentStat) { + protected doSaveContentChanges: Resource['saveContentChanges'] = async (changes, options) => { + const version = options?.version || this._version; + const current = FileResourceVersion.is(version) ? version : undefined; + if (!current) { throw ResourceError.NotFound({ message: 'has not been read yet', data: { uri: this.uri } }); } + const etag = current?.etag; try { - const stat = await this.fileSystem.updateContent(currentStat, changes, options); - this._version = { stat }; + const stat = await this.fileService.update(this.uri, changes, { + readEncoding: current.encoding, + encoding: options?.encoding, + overwriteEncoding: options?.overwriteEncoding, + etag, + mtime: current?.mtime + }); + this._version = { + etag: stat.etag, + mtime: stat.mtime, + encoding: stat.encoding + }; } catch (e) { - if (FileSystemError.FileNotFound.is(e)) { - throw ResourceError.NotFound({ ...e.toJson(), data: { uri: this.uri } }); + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + const { message, stack } = e; + throw ResourceError.NotFound({ message, stack, data: { uri: this.uri } }); } - if (FileSystemError.FileIsOutOfSync.is(e)) { - throw ResourceError.OutOfSync({ ...e.toJson(), data: { uri: this.uri } }); + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { + const { message, stack } = e; + throw ResourceError.OutOfSync({ message, stack, data: { uri: this.uri } }); } throw e; } - } + }; - async guessEncoding(): Promise { - return this.fileSystem.guessEncoding(this.uriString); + async guessEncoding(): Promise { + const content = await this.fileService.read(this.uri, { autoGuessEncoding: true }); + return content.encoding; } protected async sync(): Promise { @@ -170,43 +192,54 @@ export class FileResource implements Resource { this.onDidChangeContentsEmitter.fire(undefined); } protected async isInSync(): Promise { - const stat = await this.getFileStat(); - const current = this.version && this.version.stat; - if (!current) { - return !stat; - } - return !!stat && current.lastModification >= stat.lastModification; - } - - protected async getFileStat(): Promise { - if (!await this.fileSystem.exists(this.uriString)) { - return undefined; - } try { - return this.fileSystem.getFileStat(this.uriString); + const stat = await this.fileService.resolve(this.uri, { resolveMetadata: true }); + return !!this.version && this.version.mtime >= stat.mtime; } catch { - return undefined; + return !this.version; } } + protected async shouldOverwrite(): Promise { + return this.options.shouldOverwrite(); + } + } @injectable() export class FileResourceResolver implements ResourceResolver { - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; - @inject(FileSystemWatcher) - protected readonly fileSystemWatcher: FileSystemWatcher; + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; async resolve(uri: URI): Promise { - if (uri.scheme !== 'file') { - throw new Error('The given uri is not file uri: ' + uri); + let stat; + try { + stat = await this.fileService.resolve(uri); + } catch (e) { + if (!(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { + throw e; + } } - const resource = new FileResource(uri, this.fileSystem, this.fileSystemWatcher); - await resource.init(); - return resource; + if (stat && stat.isDirectory) { + throw new Error('The given uri is a directory: ' + this.labelProvider.getLongName(uri)); + } + return new FileResource(uri, this.fileService, { + shouldOverwrite: () => this.shouldOverwrite(uri) + }); + } + + protected async shouldOverwrite(uri: URI): Promise { + const dialog = new ConfirmDialog({ + title: `The file '${this.labelProvider.getName(uri)}' has been changed on the file system.`, + msg: `Do you want to overwrite the changes made to '${this.labelProvider.getLongName(uri)}' on the file system?`, + ok: 'Yes', + cancel: 'No' + }); + return !!await dialog.open(); } } diff --git a/packages/filesystem/src/browser/file-selection.ts b/packages/filesystem/src/browser/file-selection.ts index d26fff13eedbd..7f1a3da575e43 100644 --- a/packages/filesystem/src/browser/file-selection.ts +++ b/packages/filesystem/src/browser/file-selection.ts @@ -16,7 +16,7 @@ import { SelectionService } from '@theia/core/lib/common/selection-service'; import { SelectionCommandHandler } from '@theia/core/lib/common/selection-command-handler'; -import { FileStat } from '../common/filesystem'; +import { FileStat } from '../common/files'; export interface FileSelection { fileStat: FileStat diff --git a/packages/filesystem/src/browser/file-service.ts b/packages/filesystem/src/browser/file-service.ts new file mode 100644 index 0000000000000..e83fb62e82f94 --- /dev/null +++ b/packages/filesystem/src/browser/file-service.ts @@ -0,0 +1,1516 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/platform/files/common/fileService.ts +// and https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/services/textfile/browser/textFileService.ts +// and https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +// and https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts +// and https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts + +/* eslint-disable max-len */ +/* eslint-disable no-shadow */ +/* eslint-disable no-null/no-null */ +/* eslint-disable @typescript-eslint/tslint/config */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { injectable, inject, named, postConstruct } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { timeout, Deferred } from '@theia/core/lib/common/promise-util'; +import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { WaitUntilEvent, Emitter, AsyncEmitter } from '@theia/core/lib/common/event'; +import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; +import { TernarySearchTree } from '@theia/core/lib/common/ternary-search-tree'; +import { + ensureFileSystemProviderError, etag, ETAG_DISABLED, + FileChangesEvent, + FileOperation, FileOperationError, + FileOperationEvent, FileOperationResult, FileSystemProviderCapabilities, + FileSystemProviderErrorCode, FileType, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, + CreateFileOptions, FileContent, FileStat, FileStatWithMetadata, + FileStreamContent, FileSystemProvider, + FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, + ReadFileOptions, ResolveFileOptions, ResolveMetadataFileOptions, + Stat, WatchOptions, WriteFileOptions, + toFileOperationResult, toFileSystemProviderErrorCode, + ResolveFileResult, ResolveFileResultWithMetadata, + MoveFileOptions, CopyFileOptions, BaseStatWithMetadata, FileDeleteOptions, FileOperationOptions, hasAccessCapability, hasUpdateCapability +} from '../common/files'; +import { createReadStream } from '../common/io'; +import { BinaryBuffer, BinaryBufferReadable, BinaryBufferReadableStream } from '@theia/core/lib/common/buffer'; +import { isReadableStream, ReadableStreamEvents, transform, consumeStreamWithLimit, consumeReadableWithLimit } from '@theia/core/lib/common/stream'; +import { LabelProvider } from '@theia/core/lib/browser/label-provider'; +import { FileSystemPreferences } from './filesystem-preferences'; +import { ProgressService } from '@theia/core/lib/common/progress-service'; +import { DelegatingFileSystemProvider } from '../common/delegating-file-system-provider'; +import type { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; +import { EncodingRegistry } from '@theia/core/lib/browser/encoding-registry'; +import { UTF8, UTF8_with_bom } from '@theia/core/lib/common/encodings'; +import { EncodingService, ResourceEncoding } from '@theia/core/lib/common/encoding-service'; + +export interface FileOperationParticipant { + + /** + * Participate in a file operation of a working copy. Allows to + * change the working copy before it is being saved to disk. + */ + participate( + target: URI, + source: URI | undefined, + operation: FileOperation, + timeout: number, + token: CancellationToken + ): Promise; +} + +export interface ReadEncodingOptions { + + /** + * The optional encoding parameter allows to specify the desired encoding when resolving + * the contents of the file. + */ + encoding?: string; + + /** + * The optional guessEncoding parameter allows to guess encoding from content of the file. + */ + autoGuessEncoding?: boolean; +} + +export interface WriteEncodingOptions { + + /** + * The encoding to use when updating a file. + */ + encoding?: string; + + /** + * If set to true, will enforce the selected encoding and not perform any detection using BOMs. + */ + overwriteEncoding?: boolean; +} + +export interface ReadTextFileOptions extends ReadEncodingOptions, ReadFileOptions { } + +export interface TextFileContent extends BaseStatWithMetadata { + + /** + * The encoding of the content if known. + */ + encoding: string; + + /** + * The content of a text file. + */ + value: string; +} + +export interface CreateTextFileOptions extends WriteEncodingOptions, CreateFileOptions { } + +export interface WriteTextFileOptions extends WriteEncodingOptions, WriteFileOptions { } + +export interface UpdateTextFileOptions extends WriteEncodingOptions, WriteFileOptions { + readEncoding: string +} + +export interface UserFileOperationEvent extends WaitUntilEvent { + + /** + * An identifier to correlate the operation through the + * different event types (before, after, error). + */ + readonly correlationId: number; + + /** + * The file operation that is taking place. + */ + readonly operation: FileOperation; + + /** + * The resource the event is about. + */ + readonly target: URI; + + /** + * A property that is defined for move operations. + */ + readonly source?: URI; +} + +export const FileServiceContribution = Symbol('FileServiceContribution'); +export interface FileServiceContribution { + /** + * ```ts + * service.onWillActivateFileSystemProvider(event => { + * if (event.scheme === 'mySyncProviderScheme') { + * service.registerProvider('mySyncProviderScheme', this.mySyncProvider); + * } + * if (event.scheme === 'myAsyncProviderScheme') { + * event.waitUntil((async () => { + * const myAsyncProvider = await this.createAsyncProvider(); + * service.registerProvider('myAsyncProviderScheme', myAsyncProvider); + * })()); + * } + * }); + * ``` + */ + registerFileSystemProviders(service: FileService): void; +} + +export interface FileSystemProviderRegistrationEvent { + added: boolean; + scheme: string; + provider?: FileSystemProvider; +} + +export interface FileSystemProviderCapabilitiesChangeEvent { + provider: FileSystemProvider; + scheme: string; +} + +export interface FileSystemProviderActivationEvent extends WaitUntilEvent { + scheme: string; +} + +@injectable() +export class FileService { + + private readonly BUFFER_SIZE = 64 * 1024; + + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + @inject(FileSystemPreferences) + protected readonly preferences: FileSystemPreferences; + + @inject(ProgressService) + protected readonly progressService: ProgressService; + + @inject(EncodingRegistry) + protected readonly encodingRegistry: EncodingRegistry; + + @inject(EncodingService) + protected readonly encodingService: EncodingService; + + @inject(ContributionProvider) @named(FileServiceContribution) + protected readonly contributions: ContributionProvider; + + @postConstruct() + protected init(): void { + for (const contribution of this.contributions.getContributions()) { + contribution.registerFileSystemProviders(this); + } + } + + // #region Events + + private correlationIds = 0; + + private readonly onWillRunUserOperationEmitter = new AsyncEmitter(); + /** + * An event that is emitted when file operation is being performed. + * This event is triggered by user gestures. + */ + readonly onWillRunUserOperation = this.onWillRunUserOperationEmitter.event; + + private readonly onDidFailUserOperationEmitter = new AsyncEmitter(); + /** + * An event that is emitted when file operation is failed. + * This event is triggered by user gestures. + */ + readonly onDidFailUserOperation = this.onDidFailUserOperationEmitter.event; + + private readonly onDidRunUserOperationEmitter = new AsyncEmitter(); + /** + * An event that is emitted when file operation is finished. + * This event is triggered by user gestures. + */ + readonly onDidRunUserOperation = this.onDidRunUserOperationEmitter.event; + + // #endregion + + // #region File System Provider + + private onDidChangeFileSystemProviderRegistrationsEmitter = new Emitter(); + readonly onDidChangeFileSystemProviderRegistrations = this.onDidChangeFileSystemProviderRegistrationsEmitter.event; + + private onWillActivateFileSystemProviderEmitter = new Emitter(); + /** + * See `FileServiceContribution.registerProviders`. + */ + readonly onWillActivateFileSystemProvider = this.onWillActivateFileSystemProviderEmitter.event; + + private onDidChangeFileSystemProviderCapabilitiesEmitter = new Emitter(); + readonly onDidChangeFileSystemProviderCapabilities = this.onDidChangeFileSystemProviderCapabilitiesEmitter.event; + + private readonly providers = new Map(); + private readonly activations = new Map>(); + + registerProvider(scheme: string, provider: FileSystemProvider): Disposable { + if (this.providers.has(scheme)) { + throw new Error(`A filesystem provider for the scheme '${scheme}' is already registered.`); + } + + this.providers.set(scheme, provider); + this.onDidChangeFileSystemProviderRegistrationsEmitter.fire({ added: true, scheme, provider }); + + const providerDisposables = new DisposableCollection(); + providerDisposables.push(provider.onDidChangeFile(changes => this.onDidFilesChangeEmitter.fire(new FileChangesEvent(changes)))); + providerDisposables.push(provider.onDidChangeCapabilities(() => this.onDidChangeFileSystemProviderCapabilitiesEmitter.fire({ provider, scheme }))); + + return Disposable.create(() => { + this.onDidChangeFileSystemProviderRegistrationsEmitter.fire({ added: false, scheme, provider }); + this.providers.delete(scheme); + + providerDisposables.dispose(); + }); + } + + async activateProvider(scheme: string): Promise { + let provider = this.providers.get(scheme); + if (provider) { + return provider; + } + let activation = this.activations.get(scheme); + if (!activation) { + const deferredActivation = new Deferred(); + this.activations.set(scheme, activation = deferredActivation.promise); + WaitUntilEvent.fire(this.onWillActivateFileSystemProviderEmitter, { scheme }).then(() => { + provider = this.providers.get(scheme); + if (!provider) { + const error = new Error(); + error.name = 'ENOPRO'; + error.message = `No file system provider found for scheme ${scheme}`; + throw error; + } else { + deferredActivation.resolve(provider); + } + }).catch(e => deferredActivation.reject(e)); + } + return activation; + } + + canHandleResource(resource: URI): boolean { + return this.providers.has(resource.scheme); + } + + hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean { + const provider = this.providers.get(resource.scheme); + + return !!(provider && (provider.capabilities & capability)); + } + + protected async withProvider(resource: URI): Promise { + // Assert path is absolute + if (!resource.path.isAbsolute) { + throw new FileOperationError(`Unable to resolve filesystem provider with relative file path ${this.resourceForError(resource)}`, FileOperationResult.FILE_INVALID_PATH); + } + + return this.activateProvider(resource.scheme); + } + + private async withReadProvider(resource: URI): Promise { + const provider = await this.withProvider(resource); + + if (hasOpenReadWriteCloseCapability(provider) || hasReadWriteCapability(provider)) { + return provider; + } + + throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.`); + } + + private async withWriteProvider(resource: URI): Promise { + const provider = await this.withProvider(resource); + if (hasOpenReadWriteCloseCapability(provider) || hasReadWriteCapability(provider)) { + return provider; + } + + throw new Error(`Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the write operation.`); + } + + // #endregion + + private onDidRunOperationEmitter = new Emitter(); + /** + * An event that is emitted when operation is finished. + * This event is triggered by user gestures and programmatically. + */ + readonly onDidRunOperation = this.onDidRunOperationEmitter.event; + + resolve(resource: URI, options: ResolveMetadataFileOptions): Promise; + resolve(resource: URI, options?: ResolveFileOptions | undefined): Promise; + async resolve(resource: any, options?: any) { + try { + return await this.doResolveFile(resource, options); + } catch (error) { + + // Specially handle file not found case as file operation result + if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileNotFound) { + throw new FileOperationError(`Unable to resolve non-existing file '${this.resourceForError(resource)}'`, FileOperationResult.FILE_NOT_FOUND); + } + + // Bubble up any other error as is + throw ensureFileSystemProviderError(error); + } + } + + private async doResolveFile(resource: URI, options: ResolveMetadataFileOptions): Promise; + private async doResolveFile(resource: URI, options?: ResolveFileOptions): Promise; + private async doResolveFile(resource: URI, options?: ResolveFileOptions): Promise { + const provider = await this.withProvider(resource); + + const resolveTo = options?.resolveTo; + const resolveSingleChildDescendants = options?.resolveSingleChildDescendants; + const resolveMetadata = options?.resolveMetadata; + + const stat = await provider.stat(resource); + + let trie: TernarySearchTree | undefined; + + return this.toFileStat(provider, resource, stat, undefined, !!resolveMetadata, (stat, siblings) => { + + // lazy trie to check for recursive resolving + if (!trie) { + trie = TernarySearchTree.forUris(!!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive)); + trie.set(resource, true); + if (Array.isArray(resolveTo) && resolveTo.length) { + resolveTo.forEach(uri => trie!.set(uri, true)); + } + } + + // check for recursive resolving + if (Boolean(trie.findSuperstr(stat.resource) || trie.get(stat.resource))) { + return true; + } + + // check for resolving single child folders + if (stat.isDirectory && resolveSingleChildDescendants) { + return siblings === 1; + } + + return false; + }); + } + + private async toFileStat(provider: FileSystemProvider, resource: URI, stat: Stat | { type: FileType } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: FileStat, siblings?: number) => boolean): Promise; + private async toFileStat(provider: FileSystemProvider, resource: URI, stat: Stat, siblings: number | undefined, resolveMetadata: true, recurse: (stat: FileStat, siblings?: number) => boolean): Promise; + private async toFileStat(provider: FileSystemProvider, resource: URI, stat: Stat | { type: FileType } & Partial, siblings: number | undefined, resolveMetadata: boolean, recurse: (stat: FileStat, siblings?: number) => boolean): Promise { + const fileStat = FileStat.fromStat(resource, stat); + + // check to recurse for directories + if (fileStat.isDirectory && recurse(fileStat, siblings)) { + try { + const entries = await provider.readdir(resource); + const resolvedEntries = await Promise.all(entries.map(async ([name, type]) => { + try { + const childResource = resource.resolve(name); + const childStat = resolveMetadata ? await provider.stat(childResource) : { type }; + + return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse); + } catch (error) { + console.trace(error); + + return null; // can happen e.g. due to permission errors + } + })); + + // make sure to get rid of null values that signal a failure to resolve a particular entry + fileStat.children = resolvedEntries.filter(e => !!e) as FileStat[]; + } catch (error) { + console.trace(error); + + fileStat.children = []; // gracefully handle errors, we may not have permissions to read + } + + return fileStat; + } + + return fileStat; + } + + async resolveAll(toResolve: { resource: URI, options?: ResolveFileOptions }[]): Promise; + async resolveAll(toResolve: { resource: URI, options: ResolveMetadataFileOptions }[]): Promise; + async resolveAll(toResolve: { resource: URI; options?: ResolveFileOptions; }[]): Promise { + return Promise.all(toResolve.map(async entry => { + try { + return { stat: await this.doResolveFile(entry.resource, entry.options), success: true }; + } catch (error) { + console.trace(error); + + return { stat: undefined, success: false }; + } + })); + } + + async exists(resource: URI): Promise { + const provider = await this.withProvider(resource); + + try { + const stat = await provider.stat(resource); + + return !!stat; + } catch (error) { + return false; + } + } + + /** + * Tests a user's permissions for the given resource. + */ + async access(resource: URI, mode?: number): Promise { + const provider = await this.withProvider(resource); + + if (!hasAccessCapability(provider)) { + return false; + } + try { + await provider.access(resource, mode); + return true; + } catch (error) { + return false; + } + } + + /** + * Resolves the fs path of the given URI. + * + * USE WITH CAUTION: You should always prefer URIs to paths if possible, as they are + * portable and platform independent. Paths should only be used in cases you directly + * interact with the OS, e.g. when running a command on the shell. + * + * If you need to display human readable simple or long names then use `LabelProvider` instead. + */ + async fsPath(resource: URI): Promise { + const provider = await this.withProvider(resource); + + if (!hasAccessCapability(provider)) { + return resource.path.toString(); + } + return provider.fsPath(resource); + } + + // #region Text File Reading/Writing + + async create(resource: URI, value?: string, options?: CreateTextFileOptions): Promise { + if (options?.fromUserGesture === false) { + return this.doCreate(resource, value, options); + } + await this.runFileOperationParticipants(resource, undefined, FileOperation.CREATE); + + const event = { correlationId: this.correlationIds++, operation: FileOperation.CREATE, target: resource }; + await this.onWillRunUserOperationEmitter.fire(event); + + let stat: FileStatWithMetadata; + try { + stat = await this.doCreate(resource, value, options); + } catch (error) { + await this.onDidFailUserOperationEmitter.fire(event); + throw error; + } + + await this.onDidRunUserOperationEmitter.fire(event); + + return stat; + } + + protected async doCreate(resource: URI, value?: string, options?: CreateTextFileOptions): Promise { + const encoding = await this.getWriteEncoding(resource, options); + const encoded = this.encodingService.encode(value || '', encoding); + return this.createFile(resource, encoded, options); + } + + async write(resource: URI, value: string, options?: WriteTextFileOptions): Promise { + const encoding = await this.getWriteEncoding(resource, options); + const encoded = this.encodingService.encode(value, encoding); + return Object.assign(await this.writeFile(resource, encoded, options), { encoding: encoding.encoding }); + } + + async read(resource: URI, options?: ReadTextFileOptions): Promise { + options = { + ...options, + autoGuessEncoding: typeof options?.autoGuessEncoding === 'boolean' ? options.autoGuessEncoding : this.preferences['files.autoGuessEncoding'] + }; + const content = await this.readFile(resource, options); + const detected = await this.encodingService.detectEncoding(content.value, options.autoGuessEncoding); + const encoding = await this.getReadEncoding(resource, options, detected.encoding); + const value = this.encodingService.decode(content.value, encoding); + return { ...content, encoding, value }; + } + + async update(resource: URI, changes: TextDocumentContentChangeEvent[], options: UpdateTextFileOptions): Promise { + const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); + try { + await this.validateWriteFile(provider, resource, options); + if (hasUpdateCapability(provider)) { + const encoding = await this.getEncodingForResource(resource, options ? options.encoding : undefined);; + const stat = await provider.updateFile(resource, changes, { + readEncoding: options.readEncoding, + writeEncoding: encoding, + overwriteEncoding: options.overwriteEncoding || false + }); + return Object.assign(FileStat.fromStat(resource, stat), { encoding: stat.encoding }); + } else { + throw new Error('incremental file update is not supported'); + } + } catch (error) { + this.rethrowAsFileOperationError('Unable to write file', resource, error, options); + } + } + + // #endregion + + // #region File Reading/Writing + + async createFile(resource: URI, bufferOrReadableOrStream: BinaryBuffer | BinaryBufferReadable | BinaryBufferReadableStream = BinaryBuffer.fromString(''), options?: CreateFileOptions): Promise { + + // validate overwrite + if (!options?.overwrite && await this.exists(resource)) { + throw new FileOperationError(`Unable to create file '${this.resourceForError(resource)}' that already exists when overwrite flag is not set`, FileOperationResult.FILE_MODIFIED_SINCE, options); + } + + // do write into file (this will create it too) + const fileStat = await this.writeFile(resource, bufferOrReadableOrStream); + + // events + this.onDidRunOperationEmitter.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); + + return fileStat; + } + + async writeFile(resource: URI, bufferOrReadableOrStream: BinaryBuffer | BinaryBufferReadable | BinaryBufferReadableStream, options?: WriteFileOptions): Promise { + const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); + + try { + + // validate write + const stat = await this.validateWriteFile(provider, resource, options); + + // mkdir recursively as needed + if (!stat) { + await this.mkdirp(provider, resource.parent); + } + + // optimization: if the provider has unbuffered write capability and the data + // to write is a Readable, we consume up to 3 chunks and try to write the data + // unbuffered to reduce the overhead. If the Readable has more data to provide + // we continue to write buffered. + if (hasReadWriteCapability(provider) && !(bufferOrReadableOrStream instanceof BinaryBuffer)) { + if (isReadableStream(bufferOrReadableOrStream)) { + bufferOrReadableOrStream = await consumeStreamWithLimit(bufferOrReadableOrStream, data => BinaryBuffer.concat(data), 3); + } else { + bufferOrReadableOrStream = consumeReadableWithLimit(bufferOrReadableOrStream, data => BinaryBuffer.concat(data), 3); + } + } + + // write file: unbuffered (only if data to write is a buffer, or the provider has no buffered write capability) + if (!hasOpenReadWriteCloseCapability(provider) || (hasReadWriteCapability(provider) && bufferOrReadableOrStream instanceof BinaryBuffer)) { + await this.doWriteUnbuffered(provider, resource, bufferOrReadableOrStream); + } + + // write file: buffered + else { + await this.doWriteBuffered(provider, resource, bufferOrReadableOrStream instanceof BinaryBuffer ? BinaryBufferReadable.fromBuffer(bufferOrReadableOrStream) : bufferOrReadableOrStream); + } + } catch (error) { + this.rethrowAsFileOperationError('Unable to write file', resource, error, options); + } + + return this.resolve(resource, { resolveMetadata: true }); + } + + private async validateWriteFile(provider: FileSystemProvider, resource: URI, options?: WriteFileOptions): Promise { + let stat: Stat | undefined = undefined; + try { + stat = await provider.stat(resource); + } catch (error) { + return undefined; // file might not exist + } + + // file cannot be directory + if ((stat.type & FileType.Directory) !== 0) { + throw new FileOperationError(`Unable to write file ${this.resourceForError(resource)} that is actually a directory`, FileOperationResult.FILE_IS_DIRECTORY, options); + } + + if (this.modifiedSince(stat, options)) { + throw new FileOperationError('File Modified Since', FileOperationResult.FILE_MODIFIED_SINCE, options); + } + + return stat; + } + + /** + * Dirty write prevention: if the file on disk has been changed and does not match our expected + * mtime and etag, we bail out to prevent dirty writing. + * + * First, we check for a mtime that is in the future before we do more checks. The assumption is + * that only the mtime is an indicator for a file that has changed on disk. + * + * Second, if the mtime has advanced, we compare the size of the file on disk with our previous + * one using the etag() function. Relying only on the mtime check has prooven to produce false + * positives due to file system weirdness (especially around remote file systems). As such, the + * check for size is a weaker check because it can return a false negative if the file has changed + * but to the same length. This is a compromise we take to avoid having to produce checksums of + * the file content for comparison which would be much slower to compute. + */ + protected modifiedSince(stat: Stat, options?: WriteFileOptions): boolean { + return !!options && typeof options.mtime === 'number' && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED && + typeof stat.mtime === 'number' && typeof stat.size === 'number' && + options.mtime < stat.mtime && options.etag !== etag({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size }); + } + + async readFile(resource: URI, options?: ReadFileOptions): Promise { + const provider = await this.withReadProvider(resource); + + const stream = await this.doReadAsFileStream(provider, resource, { + ...options, + // optimization: since we know that the caller does not + // care about buffering, we indicate this to the reader. + // this reduces all the overhead the buffered reading + // has (open, read, close) if the provider supports + // unbuffered reading. + preferUnbuffered: true + }); + + return { + ...stream, + value: await BinaryBufferReadableStream.toBuffer(stream.value) + }; + } + + async readFileStream(resource: URI, options?: ReadFileOptions): Promise { + const provider = await this.withReadProvider(resource); + + return this.doReadAsFileStream(provider, resource, options); + } + + private async doReadAsFileStream(provider: FileSystemProviderWithFileReadWriteCapability | FileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options?: ReadFileOptions & { preferUnbuffered?: boolean }): Promise { + + // install a cancellation token that gets cancelled + // when any error occurs. this allows us to resolve + // the content of the file while resolving metadata + // but still cancel the operation in certain cases. + const cancellableSource = new CancellationTokenSource(); + + // validate read operation + const statPromise = this.validateReadFile(resource, options).then(stat => stat, error => { + cancellableSource.cancel(); + + throw error; + }); + + try { + + // if the etag is provided, we await the result of the validation + // due to the likelyhood of hitting a NOT_MODIFIED_SINCE result. + // otherwise, we let it run in parallel to the file reading for + // optimal startup performance. + if (options && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED) { + await statPromise; + } + + let fileStreamPromise: Promise; + + // read unbuffered (only if either preferred, or the provider has no buffered read capability) + if (!hasOpenReadWriteCloseCapability(provider) || (hasReadWriteCapability(provider) && options?.preferUnbuffered)) { + fileStreamPromise = this.readFileUnbuffered(provider, resource, options); + } + // read buffered + else { + fileStreamPromise = Promise.resolve(this.readFileBuffered(provider, resource, cancellableSource.token, options)); + } + + const [fileStat, fileStream] = await Promise.all([statPromise, fileStreamPromise]); + + return { + ...fileStat, + value: fileStream + }; + } catch (error) { + this.rethrowAsFileOperationError('Unable to read file', resource, error, options); + } + } + + private readFileBuffered(provider: FileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: ReadFileOptions = Object.create(null)): BinaryBufferReadableStream { + const fileStream = createReadStream(provider, resource, { + ...options, + bufferSize: this.BUFFER_SIZE + }, token); + + return this.transformFileReadStream(resource, fileStream, options); + } + + private transformFileReadStream(resource: URI, stream: ReadableStreamEvents, options: ReadFileOptions): BinaryBufferReadableStream { + return transform(stream, { + data: data => data instanceof BinaryBuffer ? data : BinaryBuffer.wrap(data), + error: error => this.asFileOperationError('Unable to read file', resource, error, options) + }, data => BinaryBuffer.concat(data)); + } + + protected rethrowAsFileOperationError(message: string, resource: URI, error: Error, options?: ReadFileOptions & WriteFileOptions & CreateFileOptions): never { + throw this.asFileOperationError(message, resource, error, options); + } + protected asFileOperationError(message: string, resource: URI, error: Error, options?: ReadFileOptions & WriteFileOptions & CreateFileOptions): FileOperationError { + const fileOperationError = new FileOperationError(`${message} '${this.resourceForError(resource)}' (${ensureFileSystemProviderError(error).toString()})`, + toFileOperationResult(error), options); + fileOperationError.stack = `${fileOperationError.stack}\nCaused by: ${error.stack}`; + return fileOperationError; + } + + private async readFileUnbuffered(provider: FileSystemProviderWithFileReadWriteCapability, resource: URI, options?: ReadFileOptions): Promise { + let buffer = await provider.readFile(resource); + + // respect position option + if (options && typeof options.position === 'number') { + buffer = buffer.slice(options.position); + } + + // respect length option + if (options && typeof options.length === 'number') { + buffer = buffer.slice(0, options.length); + } + + return BinaryBufferReadableStream.fromBuffer(BinaryBuffer.wrap(buffer)); + } + + private async validateReadFile(resource: URI, options?: ReadFileOptions): Promise { + const stat = await this.resolve(resource, { resolveMetadata: true }); + + // Throw if resource is a directory + if (stat.isDirectory) { + throw new FileOperationError(`Unable to read file '${this.resourceForError(resource)}' that is actually a directory`, FileOperationResult.FILE_IS_DIRECTORY, options); + } + + // Throw if file not modified since (unless disabled) + if (options && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED && options.etag === stat.etag) { + throw new FileOperationError('File not modified since', FileOperationResult.FILE_NOT_MODIFIED_SINCE, options); + } + + return stat; + } + + // #endregion + + // #region Move/Copy/Delete/Create Folder + + async move(source: URI, target: URI, options?: MoveFileOptions): Promise { + if (options?.fromUserGesture === false) { + return this.doMove(source, target, options.overwrite); + } + await this.runFileOperationParticipants(target, source, FileOperation.MOVE); + + const event = { correlationId: this.correlationIds++, operation: FileOperation.MOVE, target, source }; + await this.onWillRunUserOperationEmitter.fire(event); + let stat: FileStatWithMetadata; + try { + stat = await this.doMove(source, target, options?.overwrite); + } catch (error) { + await this.onDidFailUserOperationEmitter.fire(event); + throw error; + } + + await this.onDidRunUserOperationEmitter.fire(event); + return stat; + } + + protected async doMove(source: URI, target: URI, overwrite?: boolean): Promise { + const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source); + const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target); + + // move + const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'move', !!overwrite); + + // resolve and send events + const fileStat = await this.resolve(target, { resolveMetadata: true }); + this.onDidRunOperationEmitter.fire(new FileOperationEvent(source, mode === 'move' ? FileOperation.MOVE : FileOperation.COPY, fileStat)); + + return fileStat; + } + + async copy(source: URI, target: URI, options?: CopyFileOptions): Promise { + if (options?.fromUserGesture === false) { + return this.doCopy(source, target, options.overwrite); + } + await this.runFileOperationParticipants(target, source, FileOperation.COPY); + + const event = { correlationId: this.correlationIds++, operation: FileOperation.COPY, target, source }; + await this.onWillRunUserOperationEmitter.fire(event); + let stat: FileStatWithMetadata; + try { + stat = await this.doCopy(source, target, options?.overwrite); + } catch (error) { + await this.onDidFailUserOperationEmitter.fire(event); + throw error; + } + + await this.onDidRunUserOperationEmitter.fire(event); + return stat; + } + + protected async doCopy(source: URI, target: URI, overwrite?: boolean): Promise { + const sourceProvider = await this.withReadProvider(source); + const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target); + + // copy + const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', !!overwrite); + + // resolve and send events + const fileStat = await this.resolve(target, { resolveMetadata: true }); + this.onDidRunOperationEmitter.fire(new FileOperationEvent(source, mode === 'copy' ? FileOperation.COPY : FileOperation.MOVE, fileStat)); + + return fileStat; + } + + private async doMoveCopy(sourceProvider: FileSystemProvider, source: URI, targetProvider: FileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite: boolean): Promise<'move' | 'copy'> { + if (source.toString() === target.toString()) { + return mode; // simulate node.js behaviour here and do a no-op if paths match + } + + // validation + const { exists, isSameResourceWithDifferentPathCase } = await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite); + + // delete as needed (unless target is same resurce with different path case) + if (exists && !isSameResourceWithDifferentPathCase && overwrite) { + await this.delete(target, { recursive: true }); + } + + // create parent folders + await this.mkdirp(targetProvider, target.parent); + + // copy source => target + if (mode === 'copy') { + + // same provider with fast copy: leverage copy() functionality + if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) { + await sourceProvider.copy(source, target, { overwrite }); + } + + // when copying via buffer/unbuffered, we have to manually + // traverse the source if it is a folder and not a file + else { + const sourceFile = await this.resolve(source); + if (sourceFile.isDirectory) { + await this.doCopyFolder(sourceProvider, sourceFile, targetProvider, target); + } else { + await this.doCopyFile(sourceProvider, source, targetProvider, target); + } + } + + return mode; + } + + // move source => target + else { + + // same provider: leverage rename() functionality + if (sourceProvider === targetProvider) { + await sourceProvider.rename(source, target, { overwrite }); + + return mode; + } + + // across providers: copy to target & delete at source + else { + await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', overwrite); + + await this.delete(source, { recursive: true }); + + return 'copy'; + } + } + } + + private async doCopyFile(sourceProvider: FileSystemProvider, source: URI, targetProvider: FileSystemProvider, target: URI): Promise { + + // copy: source (buffered) => target (buffered) + if (hasOpenReadWriteCloseCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) { + return this.doPipeBuffered(sourceProvider, source, targetProvider, target); + } + + // copy: source (buffered) => target (unbuffered) + if (hasOpenReadWriteCloseCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) { + return this.doPipeBufferedToUnbuffered(sourceProvider, source, targetProvider, target); + } + + // copy: source (unbuffered) => target (buffered) + if (hasReadWriteCapability(sourceProvider) && hasOpenReadWriteCloseCapability(targetProvider)) { + return this.doPipeUnbufferedToBuffered(sourceProvider, source, targetProvider, target); + } + + // copy: source (unbuffered) => target (unbuffered) + if (hasReadWriteCapability(sourceProvider) && hasReadWriteCapability(targetProvider)) { + return this.doPipeUnbuffered(sourceProvider, source, targetProvider, target); + } + } + + private async doCopyFolder(sourceProvider: FileSystemProvider, sourceFolder: FileStat, targetProvider: FileSystemProvider, targetFolder: URI): Promise { + + // create folder in target + await targetProvider.mkdir(targetFolder); + + // create children in target + if (Array.isArray(sourceFolder.children)) { + await Promise.all(sourceFolder.children.map(async sourceChild => { + const targetChild = targetFolder.resolve(sourceChild.name); + if (sourceChild.isDirectory) { + return this.doCopyFolder(sourceProvider, await this.resolve(sourceChild.resource), targetProvider, targetChild); + } else { + return this.doCopyFile(sourceProvider, sourceChild.resource, targetProvider, targetChild); + } + })); + } + } + + private async doValidateMoveCopy(sourceProvider: FileSystemProvider, source: URI, targetProvider: FileSystemProvider, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<{ exists: boolean, isSameResourceWithDifferentPathCase: boolean }> { + let isSameResourceWithDifferentPathCase = false; + + // Check if source is equal or parent to target (requires providers to be the same) + if (sourceProvider === targetProvider) { + const isPathCaseSensitive = !!(sourceProvider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + if (!isPathCaseSensitive) { + isSameResourceWithDifferentPathCase = source.toString().toLowerCase() === target.toString().toLowerCase(); + } + + if (isSameResourceWithDifferentPathCase && mode === 'copy') { + throw new Error(`Unable to copy when source '${this.resourceForError(source)}' is same as target '${this.resourceForError(target)}' with different path case on a case insensitive file system`); + } + + if (!isSameResourceWithDifferentPathCase && target.isEqualOrParent(source, isPathCaseSensitive)) { + throw new Error(`Unable to move/copy when source '${this.resourceForError(source)}' is parent of target '${this.resourceForError(target)}'.`); + } + } + + // Extra checks if target exists and this is not a rename + const exists = await this.exists(target); + if (exists && !isSameResourceWithDifferentPathCase) { + + // Bail out if target exists and we are not about to overwrite + if (!overwrite) { + throw new FileOperationError(`Unable to move/copy '${this.resourceForError(source)}' because target '${this.resourceForError(target)}' already exists at destination.`, FileOperationResult.FILE_MOVE_CONFLICT); + } + + // Special case: if the target is a parent of the source, we cannot delete + // it as it would delete the source as well. In this case we have to throw + if (sourceProvider === targetProvider) { + const isPathCaseSensitive = !!(sourceProvider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + if (source.isEqualOrParent(target, isPathCaseSensitive)) { + throw new Error(`Unable to move/copy '${this.resourceForError(source)}' into '${this.resourceForError(target)}' since a file would replace the folder it is contained in.`); + } + } + } + + return { exists, isSameResourceWithDifferentPathCase }; + } + + async createFolder(resource: URI): Promise { + const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource); + + // mkdir recursively + await this.mkdirp(provider, resource); + + // events + const fileStat = await this.resolve(resource, { resolveMetadata: true }); + this.onDidRunOperationEmitter.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat)); + + return fileStat; + } + + private async mkdirp(provider: FileSystemProvider, directory: URI): Promise { + const directoriesToCreate: string[] = []; + + // mkdir until we reach root + while (!directory.path.isRoot) { + try { + const stat = await provider.stat(directory); + if ((stat.type & FileType.Directory) === 0) { + throw new Error(`Unable to create folder ${this.resourceForError(directory)} that already exists but is not a directory`); + } + + break; // we have hit a directory that exists -> good + } catch (error) { + + // Bubble up any other error that is not file not found + if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileNotFound) { + throw error; + } + + // Upon error, remember directories that need to be created + directoriesToCreate.push(directory.path.base); + + // Continue up + directory = directory.parent; + } + } + + // Create directories as needed + for (let i = directoriesToCreate.length - 1; i >= 0; i--) { + directory = directory.resolve(directoriesToCreate[i]); + + try { + await provider.mkdir(directory); + } catch (error) { + if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileExists) { + // For mkdirp() we tolerate that the mkdir() call fails + // in case the folder already exists. This follows node.js + // own implementation of fs.mkdir({ recursive: true }) and + // reduces the chances of race conditions leading to errors + // if multiple calls try to create the same folders + // As such, we only throw an error here if it is other than + // the fact that the file already exists. + // (see also https://github.com/microsoft/vscode/issues/89834) + throw error; + } + } + } + } + + async delete(resource: URI, options?: FileOperationOptions & Partial): Promise { + if (options?.fromUserGesture === false) { + return this.doDelete(resource, options); + } + await this.runFileOperationParticipants(resource, undefined, FileOperation.DELETE); + + const event = { correlationId: this.correlationIds++, operation: FileOperation.DELETE, target: resource }; + await this.onWillRunUserOperationEmitter.fire(event); + try { + await this.doDelete(resource, options); + } catch (error) { + await this.onDidFailUserOperationEmitter.fire(event); + throw error; + } + + await this.onDidRunUserOperationEmitter.fire(event); + } + + protected async doDelete(resource: URI, options?: Partial): Promise { + const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource); + + // Validate trash support + const useTrash = !!options?.useTrash; + if (useTrash && !(provider.capabilities & FileSystemProviderCapabilities.Trash)) { + throw new Error(`Unable to delete file '${this.resourceForError(resource)}' via trash because provider does not support it.`); + } + + // Validate delete + const exists = await this.exists(resource); + if (!exists) { + throw new FileOperationError(`Unable to delete non-existing file '${this.resourceForError(resource)}'`, FileOperationResult.FILE_NOT_FOUND); + } + + // Validate recursive + const recursive = !!options?.recursive; + if (!recursive && exists) { + const stat = await this.resolve(resource); + if (stat.isDirectory && Array.isArray(stat.children) && stat.children.length > 0) { + throw new Error(`Unable to delete non-empty folder '${this.resourceForError(resource)}'.`); + } + } + + // Delete through provider + await provider.delete(resource, { recursive, useTrash }); + + // Events + this.onDidRunOperationEmitter.fire(new FileOperationEvent(resource, FileOperation.DELETE)); + } + + // #endregion + + // #region File Watching + + private onDidFilesChangeEmitter = new Emitter(); + /** + * An event that is emitted when files are changed on the disk. + */ + readonly onDidFilesChange = this.onDidFilesChangeEmitter.event; + + private activeWatchers = new Map(); + + watch(resource: URI, options: WatchOptions = { recursive: false, excludes: [] }): Disposable { + const resolvedOptions: WatchOptions = { + ...options, + // always ignore temporary upload files + excludes: options.excludes.concat('**/theia_upload_*') + }; + + let watchDisposed = false; + let watchDisposable = Disposable.create(() => watchDisposed = true); + + // Watch and wire in disposable which is async but + // check if we got disposed meanwhile and forward + this.doWatch(resource, resolvedOptions).then(disposable => { + if (watchDisposed) { + disposable.dispose(); + } else { + watchDisposable = disposable; + } + }, error => console.error(error)); + + return Disposable.create(() => watchDisposable.dispose()); + } + + async doWatch(resource: URI, options: WatchOptions): Promise { + const provider = await this.withProvider(resource); + const key = this.toWatchKey(provider, resource, options); + + // Only start watching if we are the first for the given key + const watcher = this.activeWatchers.get(key) || { count: 0, disposable: provider.watch(resource, options) }; + if (!this.activeWatchers.has(key)) { + this.activeWatchers.set(key, watcher); + } + + // Increment usage counter + watcher.count += 1; + + return Disposable.create(() => { + + // Unref + watcher.count--; + + // Dispose only when last user is reached + if (watcher.count === 0) { + watcher.disposable.dispose(); + this.activeWatchers.delete(key); + } + }); + } + + private toWatchKey(provider: FileSystemProvider, resource: URI, options: WatchOptions): string { + return [ + this.toMapKey(provider, resource), // lowercase path if the provider is case insensitive + String(options.recursive), // use recursive: true | false as part of the key + options.excludes.join() // use excludes as part of the key + ].join(); + } + + // #endregion + + // #region Helpers + + private writeQueues: Map> = new Map(); + + private ensureWriteQueue(provider: FileSystemProvider, resource: URI, task: () => Promise): Promise { + // ensure to never write to the same resource without finishing + // the one write. this ensures a write finishes consistently + // (even with error) before another write is done. + const queueKey = this.toMapKey(provider, resource); + const writeQueue = (this.writeQueues.get(queueKey) || Promise.resolve()).then(task, task); + this.writeQueues.set(queueKey, writeQueue); + return writeQueue; + } + + private toMapKey(provider: FileSystemProvider, resource: URI): string { + const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + + return isPathCaseSensitive ? resource.toString() : resource.toString().toLowerCase(); + } + + private async doWriteBuffered(provider: FileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readableOrStream: BinaryBufferReadable | BinaryBufferReadableStream): Promise { + return this.ensureWriteQueue(provider, resource, async () => { + + // open handle + const handle = await provider.open(resource, { create: true }); + + // write into handle until all bytes from buffer have been written + try { + if (isReadableStream(readableOrStream)) { + await this.doWriteStreamBufferedQueued(provider, handle, readableOrStream); + } else { + await this.doWriteReadableBufferedQueued(provider, handle, readableOrStream); + } + } catch (error) { + throw ensureFileSystemProviderError(error); + } finally { + + // close handle always + await provider.close(handle); + } + }); + } + + private doWriteStreamBufferedQueued(provider: FileSystemProviderWithOpenReadWriteCloseCapability, handle: number, stream: BinaryBufferReadableStream): Promise { + return new Promise((resolve, reject) => { + let posInFile = 0; + + stream.on('data', async chunk => { + + // pause stream to perform async write operation + stream.pause(); + + try { + await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0); + } catch (error) { + return reject(error); + } + + posInFile += chunk.byteLength; + + // resume stream now that we have successfully written + // run this on the next tick to prevent increasing the + // execution stack because resume() may call the event + // handler again before finishing. + setTimeout(() => stream.resume()); + }); + + stream.on('error', error => reject(error)); + stream.on('end', () => resolve()); + }); + } + + private async doWriteReadableBufferedQueued(provider: FileSystemProviderWithOpenReadWriteCloseCapability, handle: number, readable: BinaryBufferReadable): Promise { + let posInFile = 0; + + let chunk: BinaryBuffer | null; + while ((chunk = readable.read()) !== null) { + await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0); + + posInFile += chunk.byteLength; + } + } + + private async doWriteBuffer(provider: FileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: BinaryBuffer, length: number, posInFile: number, posInBuffer: number): Promise { + let totalBytesWritten = 0; + while (totalBytesWritten < length) { + const bytesWritten = await provider.write(handle, posInFile + totalBytesWritten, buffer.buffer, posInBuffer + totalBytesWritten, length - totalBytesWritten); + totalBytesWritten += bytesWritten; + } + } + + private async doWriteUnbuffered(provider: FileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStream: BinaryBuffer | BinaryBufferReadable | BinaryBufferReadableStream): Promise { + return this.ensureWriteQueue(provider, resource, () => this.doWriteUnbufferedQueued(provider, resource, bufferOrReadableOrStream)); + } + + private async doWriteUnbufferedQueued(provider: FileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStream: BinaryBuffer | BinaryBufferReadable | BinaryBufferReadableStream): Promise { + let buffer: BinaryBuffer; + if (bufferOrReadableOrStream instanceof BinaryBuffer) { + buffer = bufferOrReadableOrStream; + } else if (isReadableStream(bufferOrReadableOrStream)) { + buffer = await BinaryBufferReadableStream.toBuffer(bufferOrReadableOrStream); + } else { + buffer = BinaryBufferReadable.toBuffer(bufferOrReadableOrStream); + } + + return provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true }); + } + + private async doPipeBuffered(sourceProvider: FileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: FileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { + return this.ensureWriteQueue(targetProvider, target, () => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target)); + } + + private async doPipeBufferedQueued(sourceProvider: FileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: FileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { + let sourceHandle: number | undefined = undefined; + let targetHandle: number | undefined = undefined; + + try { + + // Open handles + sourceHandle = await sourceProvider.open(source, { create: false }); + targetHandle = await targetProvider.open(target, { create: true }); + + const buffer = BinaryBuffer.alloc(this.BUFFER_SIZE); + + let posInFile = 0; + let posInBuffer = 0; + let bytesRead = 0; + do { + // read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at + // buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength). + bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer); + + // write into target (targetHandle) at current position (posInFile) from buffer (buffer) at + // buffer position (posInBuffer) all bytes we read (bytesRead). + await this.doWriteBuffer(targetProvider, targetHandle, buffer, bytesRead, posInFile, posInBuffer); + + posInFile += bytesRead; + posInBuffer += bytesRead; + + // when buffer full, fill it again from the beginning + if (posInBuffer === buffer.byteLength) { + posInBuffer = 0; + } + } while (bytesRead > 0); + } catch (error) { + throw ensureFileSystemProviderError(error); + } finally { + await Promise.all([ + typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(), + typeof targetHandle === 'number' ? targetProvider.close(targetHandle) : Promise.resolve(), + ]); + } + } + + private async doPipeUnbuffered(sourceProvider: FileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: FileSystemProviderWithFileReadWriteCapability, target: URI): Promise { + return this.ensureWriteQueue(targetProvider, target, () => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target)); + } + + private async doPipeUnbufferedQueued(sourceProvider: FileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: FileSystemProviderWithFileReadWriteCapability, target: URI): Promise { + return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true }); + } + + private async doPipeUnbufferedToBuffered(sourceProvider: FileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: FileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { + return this.ensureWriteQueue(targetProvider, target, () => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target)); + } + + private async doPipeUnbufferedToBufferedQueued(sourceProvider: FileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: FileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { + + // Open handle + const targetHandle = await targetProvider.open(target, { create: true }); + + // Read entire buffer from source and write buffered + try { + const buffer = await sourceProvider.readFile(source); + await this.doWriteBuffer(targetProvider, targetHandle, BinaryBuffer.wrap(buffer), buffer.byteLength, 0, 0); + } catch (error) { + throw ensureFileSystemProviderError(error); + } finally { + await targetProvider.close(targetHandle); + } + } + + private async doPipeBufferedToUnbuffered(sourceProvider: FileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: FileSystemProviderWithFileReadWriteCapability, target: URI): Promise { + + // Read buffer via stream buffered + const buffer = await BinaryBufferReadableStream.toBuffer(this.readFileBuffered(sourceProvider, source, CancellationToken.None)); + + // Write buffer into target at once + await this.doWriteUnbuffered(targetProvider, target, buffer); + } + + protected throwIfFileSystemIsReadonly(provider: T, resource: URI): T { + if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { + throw new FileOperationError(`Unable to modify readonly file ${this.resourceForError(resource)}`, FileOperationResult.FILE_PERMISSION_DENIED); + } + + return provider; + } + + private resourceForError(resource: URI): string { + return this.labelProvider.getLongName(resource); + } + + // #endregion + + // #region File operation participants + + private readonly participants: FileOperationParticipant[] = []; + + addFileOperationParticipant(participant: FileOperationParticipant): Disposable { + this.participants.push(participant); + + return Disposable.create(() => { + const index = this.participants.indexOf(participant); + if (index > -1) { + this.participants.splice(index, 1); + } + }); + } + + async runFileOperationParticipants(target: URI, source: URI | undefined, operation: FileOperation): Promise { + const participantsTimeout = this.preferences['files.participants.timeout']; + if (participantsTimeout <= 0) { + return; + } + + const cancellationTokenSource = new CancellationTokenSource(); + + return this.progressService.withProgress(this.progressLabel(operation), 'window', async () => { + for (const participant of this.participants) { + if (cancellationTokenSource.token.isCancellationRequested) { + break; + } + + try { + const promise = participant.participate(target, source, operation, participantsTimeout, cancellationTokenSource.token); + await Promise.race([ + promise, + timeout(participantsTimeout, cancellationTokenSource.token).then(() => cancellationTokenSource.dispose(), () => { /* no-op if cancelled */ }) + ]); + } catch (err) { + console.warn(err); + } + } + }); + } + + private progressLabel(operation: FileOperation): string { + switch (operation) { + case FileOperation.CREATE: + return "Running 'File Create' participants..."; + case FileOperation.MOVE: + return "Running 'File Rename' participants..."; + case FileOperation.COPY: + return "Running 'File Copy' participants..."; + case FileOperation.DELETE: + return "Running 'File Delete' participants..."; + } + } + + // #endregion + + // #region encoding + + protected async getWriteEncoding(resource: URI, options?: WriteEncodingOptions): Promise { + const encoding = await this.getEncodingForResource(resource, options ? options.encoding : undefined); + return this.encodingService.toResourceEncoding(encoding, { + overwriteEncoding: options?.overwriteEncoding, + read: async length => { + const buffer = await BinaryBufferReadableStream.toBuffer((await this.readFileStream(resource, { length })).value); + return buffer.buffer; + } + }); + } + + protected getReadEncoding(resource: URI, options?: ReadEncodingOptions, detectedEncoding?: string): Promise { + let preferredEncoding: string | undefined; + + // Encoding passed in as option + if (options?.encoding) { + if (detectedEncoding === UTF8_with_bom && options.encoding === UTF8) { + preferredEncoding = UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8 + } else { + preferredEncoding = options.encoding; // give passed in encoding highest priority + } + } else if (detectedEncoding) { + preferredEncoding = detectedEncoding; + } + + return this.getEncodingForResource(resource, preferredEncoding); + } + + protected async getEncodingForResource(resource: URI, preferredEncoding?: string): Promise { + resource = await this.toUnderlyingResource(resource); + return this.encodingRegistry.getEncodingForResource(resource, preferredEncoding); + } + + /** + * Converts to an underlying fs provider resource format. + * + * For example converting `user-storage` resources to `file` resources under a user home: + * user-storage:/settings.json => file://home/.theia/settings.json + */ + async toUnderlyingResource(resource: URI): Promise { + let provider = await this.withProvider(resource); + while (provider instanceof DelegatingFileSystemProvider) { + resource = provider.toUnderlyingResource(resource); + provider = await this.withProvider(resource); + } + return resource; + } + + // #endregion + +} diff --git a/packages/filesystem/src/browser/file-tree/file-tree-model.ts b/packages/filesystem/src/browser/file-tree/file-tree-model.ts index 9896969dfa890..1f8440e41157d 100644 --- a/packages/filesystem/src/browser/file-tree/file-tree-model.ts +++ b/packages/filesystem/src/browser/file-tree/file-tree-model.ts @@ -17,24 +17,33 @@ import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { CompositeTreeNode, TreeModelImpl, TreeNode, ConfirmDialog } from '@theia/core/lib/browser'; -import { FileSystem } from '../../common'; -import { FileSystemWatcher, FileChangeType, FileChange, FileMoveEvent } from '../filesystem-watcher'; import { FileStatNode, DirNode, FileNode } from './file-tree'; import { LocationService } from '../location'; import { LabelProvider } from '@theia/core/lib/browser/label-provider'; +import { FileService } from '../file-service'; +import { FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, FileChange, FileOperation, FileOperationEvent } from '../../common/files'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; @injectable() export class FileTreeModel extends TreeModelImpl implements LocationService { @inject(LabelProvider) protected readonly labelProvider: LabelProvider; - @inject(FileSystem) protected readonly fileSystem: FileSystem; - @inject(FileSystemWatcher) protected readonly watcher: FileSystemWatcher; + + @inject(FileService) + protected readonly fileService: FileService; + + @inject(MessageService) + protected readonly messageService: MessageService; + + @inject(EnvVariablesServer) + protected readonly environments: EnvVariablesServer; @postConstruct() protected init(): void { super.init(); - this.toDispose.push(this.watcher.onFilesChanged(changes => this.onFilesChanged(changes))); - this.toDispose.push(this.watcher.onDidMove(move => this.onDidMove(move))); + this.toDispose.push(this.fileService.onDidFilesChange(changes => this.onFilesChanged(changes))); + this.toDispose.push(this.fileService.onDidRunOperation(event => this.onDidMove(event))); } get location(): URI | undefined { @@ -47,7 +56,7 @@ export class FileTreeModel extends TreeModelImpl implements LocationService { set location(uri: URI | undefined) { if (uri) { - this.fileSystem.getFileStat(uri.toString()).then(async fileStat => { + this.fileService.resolve(uri).then(fileStat => { if (fileStat) { const node = DirNode.createRoot(fileStat); this.navigateTo(node); @@ -60,7 +69,7 @@ export class FileTreeModel extends TreeModelImpl implements LocationService { async drives(): Promise { try { - const drives = await this.fileSystem.getDrives(); + const drives = await this.environments.getDrives(); return drives.map(uri => new URI(uri)); } catch (e) { this.logger.error('Error when loading drives.', e); @@ -82,38 +91,40 @@ export class FileTreeModel extends TreeModelImpl implements LocationService { /** * to workaround https://github.com/Axosoft/nsfw/issues/42 */ - protected onDidMove(move: FileMoveEvent): void { - if (FileMoveEvent.isRename(move)) { + protected onDidMove(event: FileOperationEvent): void { + if (!event.isOperation(FileOperation.MOVE)) { + return; + } + if (event.resource.parent.toString() === event.target.resource.parent.toString()) { + // file rename return; } this.refreshAffectedNodes([ - move.sourceUri, - move.targetUri + event.resource, + event.target.resource ]); } - protected onFilesChanged(changes: FileChange[]): void { + protected onFilesChanged(changes: FileChangesEvent): void { if (!this.refreshAffectedNodes(this.getAffectedUris(changes)) && this.isRootAffected(changes)) { this.refresh(); } } - protected isRootAffected(changes: FileChange[]): boolean { + protected isRootAffected(changes: FileChangesEvent): boolean { const root = this.root; if (FileStatNode.is(root)) { - return changes.some(change => - change.type < FileChangeType.DELETED && change.uri.toString() === root.uri.toString() - ); + return changes.contains(root.uri, FileChangeType.ADDED) || changes.contains(root.uri, FileChangeType.UPDATED); } return false; } - protected getAffectedUris(changes: FileChange[]): URI[] { - return changes.filter(change => !this.isFileContentChanged(change)).map(change => change.uri); + protected getAffectedUris(changes: FileChangesEvent): URI[] { + return changes.changes.filter(change => !this.isFileContentChanged(change)).map(change => change.resource); } protected isFileContentChanged(change: FileChange): boolean { - return change.type === FileChangeType.UPDATED && FileNode.is(this.getNodesByUri(change.uri).next().value); + return change.type === FileChangeType.UPDATED && FileNode.is(this.getNodesByUri(change.resource).next().value); } protected refreshAffectedNodes(uris: URI[]): boolean { @@ -136,44 +147,43 @@ export class FileTreeModel extends TreeModelImpl implements LocationService { return nodes; } - copy(uri: URI): boolean { - if (uri.scheme !== 'file') { - return false; - } - const node = this.selectedFileStatNodes[0]; - if (!node) { - return false; - } - const targetUri = node.uri.resolve(uri.path.base); - /* Check if the folder is copied on itself */ - const sourcePath = uri.path.toString(); - const targetPath = node.uri.path.toString(); - if (sourcePath === targetPath) { - return false; + async copy(source: URI, target: Readonly): Promise { + const targetUri = target.uri.resolve(source.path.base); + try { + await this.fileService.copy(source, targetUri); + } catch (e) { + this.messageService.error(e.message); } - - this.fileSystem.copy(uri.toString(), targetUri.toString()); - return true; + return targetUri; } /** * Move the given source file or directory to the given target directory. */ - async move(source: TreeNode, target: TreeNode): Promise { + async move(source: TreeNode, target: TreeNode): Promise { if (DirNode.is(target) && FileStatNode.is(source)) { - const sourceUri = source.uri.toString(); - if (target.uri.toString() === sourceUri) { /* Folder on itself */ - return; - } - const name = source.uri.displayName; - const targetUri = target.uri.resolve(name).toString(); - if (sourceUri !== targetUri) { /* File not on itself */ - const fileExistsInTarget = await this.fileSystem.exists(targetUri); - if (!fileExistsInTarget || await this.shouldReplace(name)) { - await this.fileSystem.move(sourceUri, targetUri, { overwrite: true }); + const name = source.fileStat.name; + const targetUri = target.uri.resolve(name); + try { + await this.fileService.move(source.uri, targetUri); + return targetUri; + } catch (e) { + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { + const fileName = this.labelProvider.getName(source); + if (await this.shouldReplace(fileName)) { + try { + await this.fileService.move(source.uri, targetUri, { overwrite: true }); + return targetUri; + } catch (e2) { + this.messageService.error(e2.message); + } + } + } else { + this.messageService.error(e.message); } } } + return undefined; } protected async shouldReplace(fileName: string): Promise { diff --git a/packages/filesystem/src/browser/file-tree/file-tree-widget.tsx b/packages/filesystem/src/browser/file-tree/file-tree-widget.tsx index 4f527a1fab064..6b20084da1a6f 100644 --- a/packages/filesystem/src/browser/file-tree/file-tree-widget.tsx +++ b/packages/filesystem/src/browser/file-tree/file-tree-widget.tsx @@ -16,14 +16,16 @@ import * as React from 'react'; import { injectable, inject } from 'inversify'; -import { DisposableCollection, Disposable } from '@theia/core/lib/common'; +import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; +import URI from '@theia/core/lib/common/uri'; import { UriSelection } from '@theia/core/lib/common/selection'; import { isCancelled } from '@theia/core/lib/common/cancellation'; import { ContextMenuRenderer, NodeProps, TreeProps, TreeNode, TreeWidget, CompositeTreeNode } from '@theia/core/lib/browser'; import { FileUploadService } from '../file-upload-service'; -import { DirNode, FileStatNode } from './file-tree'; +import { DirNode, FileStatNode, FileStatNodeData } from './file-tree'; import { FileTreeModel } from './file-tree-model'; import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service'; +import { FileStat, FileType } from '../../common/files'; export const FILE_TREE_CLASS = 'theia-FileTree'; export const FILE_STAT_NODE_CLASS = 'theia-FileStatNode'; @@ -243,4 +245,50 @@ export class FileTreeWidget extends TreeWidget { return super.needsExpansionTogglePadding(node); } + protected deflateForStorage(node: TreeNode): object { + const deflated = super.deflateForStorage(node); + if (FileStatNode.is(node) && FileStatNodeData.is(deflated)) { + deflated.uri = node.uri.toString(); + delete deflated['fileStat']; + deflated.stat = FileStat.toStat(node.fileStat); + } + return deflated; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected inflateFromStorage(node: any, parent?: TreeNode): TreeNode { + if (FileStatNodeData.is(node)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fileStatNode: FileStatNode = node as any; + const resource = new URI(node.uri); + fileStatNode.uri = resource; + let stat: typeof node['stat']; + // in order to support deprecated FileStat + if (node.fileStat) { + stat = { + type: node.fileStat.isDirectory ? FileType.Directory : FileType.File, + mtime: node.fileStat.lastModification, + size: node.fileStat.size + }; + delete node['fileStat']; + } else if (node.stat) { + stat = node.stat; + delete node['stat']; + } + if (stat) { + fileStatNode.fileStat = FileStat.fromStat(resource, stat); + } + } + const inflated = super.inflateFromStorage(node, parent); + if (DirNode.is(inflated)) { + inflated.fileStat.children = []; + for (const child of inflated.children) { + if (FileStatNode.is(child)) { + inflated.fileStat.children.push(child.fileStat); + } + } + } + return inflated; + } + } diff --git a/packages/filesystem/src/browser/file-tree/file-tree.ts b/packages/filesystem/src/browser/file-tree/file-tree.ts index 0a3d990d09f48..654668d149449 100644 --- a/packages/filesystem/src/browser/file-tree/file-tree.ts +++ b/packages/filesystem/src/browser/file-tree/file-tree.ts @@ -17,14 +17,22 @@ import { injectable, inject } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { TreeNode, CompositeTreeNode, SelectableTreeNode, ExpandableTreeNode, TreeImpl } from '@theia/core/lib/browser'; -import { FileSystem, FileStat } from '../../common'; +import { Mutable } from '@theia/core/lib/common/types'; +import { FileStat, Stat, FileType, FileOperationError, FileOperationResult } from '../../common/files'; +import { FileStat as DeprecatedFileStat } from '../../common/filesystem'; import { UriSelection } from '@theia/core/lib/common/selection'; +import { MessageService } from '@theia/core/lib/common/message-service'; import { FileSelection } from '../file-selection'; +import { FileService } from '../file-service'; @injectable() export class FileTree extends TreeImpl { - @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; + + @inject(MessageService) + protected readonly messagingService: MessageService; async resolveChildren(parent: CompositeTreeNode): Promise { if (FileStatNode.is(parent)) { @@ -37,14 +45,17 @@ export class FileTree extends TreeImpl { return super.resolveChildren(parent); } - protected resolveFileStat(node: FileStatNode): Promise { - return this.fileSystem.getFileStat(node.fileStat.uri).then(fileStat => { - if (fileStat) { - node.fileStat = fileStat; - return fileStat; + protected async resolveFileStat(node: FileStatNode): Promise { + try { + const fileStat = await this.fileService.resolve(node.uri); + node.fileStat = fileStat; + return fileStat; + } catch (e) { + if (!(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { + this.messagingService.error(e.message); } return undefined; - }); + } } protected async toNodes(fileStat: FileStat, parent: CompositeTreeNode): Promise { @@ -58,7 +69,7 @@ export class FileTree extends TreeImpl { } protected toNode(fileStat: FileStat, parent: CompositeTreeNode): FileNode | DirNode { - const uri = new URI(fileStat.uri); + const uri = fileStat.resource; const id = this.toNodeId(uri, parent); const node = this.getNode(id); if (fileStat.isDirectory) { @@ -88,7 +99,7 @@ export class FileTree extends TreeImpl { } } -export interface FileStatNode extends SelectableTreeNode, UriSelection, FileSelection { +export interface FileStatNode extends SelectableTreeNode, Mutable, FileSelection { } export namespace FileStatNode { export function is(node: object | undefined): node is FileStatNode { @@ -97,12 +108,23 @@ export namespace FileStatNode { export function getUri(node: TreeNode | undefined): string | undefined { if (is(node)) { - return node.fileStat.uri; + return node.fileStat.resource.toString(); } return undefined; } } +export type FileStatNodeData = Omit & { + uri: string + stat?: Stat | { type: FileType } & Partial + fileStat?: DeprecatedFileStat +}; +export namespace FileStatNodeData { + export function is(node: object | undefined): node is FileStatNodeData { + return !!node && 'uri' in node && ('fileStat' in node || 'stat' in node); + } +} + export type FileNode = FileStatNode; export namespace FileNode { export function is(node: Object | undefined): node is FileNode { @@ -140,8 +162,8 @@ export namespace DirNode { } export function createRoot(fileStat: FileStat): DirNode { - const uri = new URI(fileStat.uri); - const id = fileStat.uri; + const uri = fileStat.resource; + const id = uri.toString(); return { id, uri, fileStat, visible: true, diff --git a/packages/filesystem/src/browser/filesystem-frontend-contribution.ts b/packages/filesystem/src/browser/filesystem-frontend-contribution.ts index 6848657d63437..ddaba126bdae1 100644 --- a/packages/filesystem/src/browser/filesystem-frontend-contribution.ts +++ b/packages/filesystem/src/browser/filesystem-frontend-contribution.ts @@ -24,12 +24,14 @@ import { NavigatableWidget, NavigatableWidgetOptions, Saveable, WidgetManager, StatefulWidget, FrontendApplication, ExpandableTreeNode, waitForClosed } from '@theia/core/lib/browser'; -import { FileSystemWatcher, FileChangeEvent, FileMoveEvent, FileChangeType } from './filesystem-watcher'; import { MimeService } from '@theia/core/lib/browser/mime-service'; import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection'; import { FileSystemPreferences } from './filesystem-preferences'; import { FileSelection } from './file-selection'; import { FileUploadService } from './file-upload-service'; +import { FileService, UserFileOperationEvent } from './file-service'; +import { FileChangesEvent, FileChangeType, FileOperation } from '../common/files'; +import { Deferred } from '@theia/core/lib/common/promise-util'; export namespace FileSystemCommands { @@ -55,9 +57,6 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri @inject(WidgetManager) protected readonly widgetManager: WidgetManager; - @inject(FileSystemWatcher) - protected readonly fileSystemWatcher: FileSystemWatcher; - @inject(MimeService) protected readonly mimeService: MimeService; @@ -70,11 +69,37 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri @inject(FileUploadService) protected readonly uploadService: FileUploadService; + @inject(FileService) + protected readonly fileService: FileService; + + protected readonly userOperations = new Map>(); + protected queueUserOperation(event: UserFileOperationEvent): void { + const moveOperation = new Deferred(); + this.userOperations.set(event.correlationId, moveOperation); + this.run(() => moveOperation.promise); + } + protected resolveUserOperation(event: UserFileOperationEvent): void { + const operation = this.userOperations.get(event.correlationId); + if (operation) { + this.userOperations.delete(event.correlationId); + operation.resolve(); + } + } + initialize(): void { - this.fileSystemWatcher.onFilesChanged(event => this.run(() => this.updateWidgets(event))); - this.fileSystemWatcher.onWillMove(event => event.waitUntil(this.runEach((uri, widget) => this.pushMove(uri, widget, event)))); - this.fileSystemWatcher.onDidFailMove(event => event.waitUntil(this.runEach((uri, widget) => this.revertMove(uri, widget, event)))); - this.fileSystemWatcher.onDidMove(event => event.waitUntil(this.runEach((uri, widget) => this.applyMove(uri, widget, event)))); + this.fileService.onDidFilesChange(event => this.run(() => this.updateWidgets(event))); + this.fileService.onWillRunUserOperation(event => { + this.queueUserOperation(event); + event.waitUntil(this.runEach((uri, widget) => this.pushMove(uri, widget, event))); + }); + this.fileService.onDidFailUserOperation(event => event.waitUntil((async () => { + await this.runEach((uri, widget) => this.revertMove(uri, widget, event)); + this.resolveUserOperation(event); + })())); + this.fileService.onDidRunUserOperation(event => event.waitUntil((async () => { + await this.runEach((uri, widget) => this.applyMove(uri, widget, event)); + this.resolveUserOperation(event); + })())); } onStart?(app: FrontendApplication): MaybePromise { @@ -102,7 +127,7 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri protected async upload(selection: FileSelection): Promise { try { const source = TreeWidgetSelection.getSource(this.selectionService.selection); - await this.uploadService.upload(selection.fileStat.uri); + await this.uploadService.upload(selection.fileStat.resource); if (ExpandableTreeNode.is(selection) && source) { await source.model.expandNode(selection); } @@ -124,14 +149,12 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri }); } - protected runEach(participant: (resourceUri: URI, widget: NavigatableWidget) => Promise): Promise { - return this.run(async () => { - const promises: Promise[] = []; - for (const [resourceUri, widget] of NavigatableWidget.get(this.shell.widgets)) { - promises.push(participant(resourceUri, widget)); - } - await Promise.all(promises); - }); + protected async runEach(participant: (resourceUri: URI, widget: NavigatableWidget) => Promise): Promise { + const promises: Promise[] = []; + for (const [resourceUri, widget] of NavigatableWidget.get(this.shell.widgets)) { + promises.push(participant(resourceUri, widget)); + } + await Promise.all(promises); } protected readonly moveSnapshots = new Map(); @@ -160,7 +183,7 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri } } - protected async pushMove(resourceUri: URI, widget: NavigatableWidget, event: FileMoveEvent): Promise { + protected async pushMove(resourceUri: URI, widget: NavigatableWidget, event: UserFileOperationEvent): Promise { const newResourceUri = this.createMoveToUri(resourceUri, widget, event); if (!newResourceUri) { return; @@ -181,7 +204,7 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri this.moveSnapshots.set(newResourceUri.toString(), snapshot); } - protected async revertMove(resourceUri: URI, widget: NavigatableWidget, event: FileMoveEvent): Promise { + protected async revertMove(resourceUri: URI, widget: NavigatableWidget, event: UserFileOperationEvent): Promise { const newResourceUri = this.createMoveToUri(resourceUri, widget, event); if (!newResourceUri) { return; @@ -190,7 +213,7 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri this.applyMoveSnapshot(widget, snapshot); } - protected async applyMove(resourceUri: URI, widget: NavigatableWidget, event: FileMoveEvent): Promise { + protected async applyMove(resourceUri: URI, widget: NavigatableWidget, event: UserFileOperationEvent): Promise { const newResourceUri = this.createMoveToUri(resourceUri, widget, event); if (!newResourceUri) { return; @@ -225,20 +248,21 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri pending.push(this.shell.closeWidget(widget.id, { save: false })); await Promise.all(pending); } - protected createMoveToUri(resourceUri: URI, widget: NavigatableWidget, event: FileMoveEvent): URI | undefined { - const path = event.sourceUri.relative(resourceUri); - const targetUri = path && event.targetUri.resolve(path); + + protected createMoveToUri(resourceUri: URI, widget: NavigatableWidget, event: UserFileOperationEvent): URI | undefined { + if (event.operation !== FileOperation.MOVE) { + return undefined; + } + const path = event.source?.relative(resourceUri); + const targetUri = path && event.target.resolve(path); return targetUri && widget.createMoveToUri(targetUri); } protected readonly deletedSuffix = ' (deleted from disk)'; - protected async updateWidgets(event: FileChangeEvent): Promise { - const relevantEvent = event.filter(({ type }) => type !== FileChangeType.UPDATED); - if (relevantEvent.length) { - return this.doUpdateWidgets(relevantEvent); + protected async updateWidgets(event: FileChangesEvent): Promise { + if (!event.gotDeleted() && !event.gotAdded()) { + return; } - } - protected async doUpdateWidgets(event: FileChangeEvent): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any const pending: Promise[] = []; @@ -258,13 +282,13 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri await Promise.all(pending); } - protected updateWidget(uri: URI, widget: NavigatableWidget, event: FileChangeEvent, { dirty, toClose }: { + protected updateWidget(uri: URI, widget: NavigatableWidget, event: FileChangesEvent, { dirty, toClose }: { dirty: Set; toClose: Map }): void { const label = widget.title.label; const deleted = label.endsWith(this.deletedSuffix); - if (FileChangeEvent.isDeleted(event, uri)) { + if (event.contains(uri, FileChangeType.DELETED)) { const uriString = uri.toString(); if (Saveable.isDirty(widget)) { if (!deleted) { @@ -275,7 +299,7 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri const widgets = toClose.get(uriString) || []; widgets.push(widget); toClose.set(uriString, widgets); - } else if (FileChangeEvent.isAdded(event, uri)) { + } else if (event.contains(uri, FileChangeType.ADDED)) { if (deleted) { widget.title.label = widget.title.label.substr(0, label.length - this.deletedSuffix.length); } diff --git a/packages/filesystem/src/browser/filesystem-frontend-module.ts b/packages/filesystem/src/browser/filesystem-frontend-module.ts index 351c873b4e599..6b5695db471d5 100644 --- a/packages/filesystem/src/browser/filesystem-frontend-module.ts +++ b/packages/filesystem/src/browser/filesystem-frontend-module.ts @@ -18,44 +18,190 @@ import '../../src/browser/style/index.css'; import { ContainerModule, interfaces } from 'inversify'; import { ResourceResolver, CommandContribution } from '@theia/core/lib/common'; -import { WebSocketConnectionProvider, FrontendApplicationContribution, ConfirmDialog, LabelProviderContribution, LabelProvider } from '@theia/core/lib/browser'; -import { FileSystem, fileSystemPath, FileShouldOverwrite, FileStat } from '../common'; -import { - fileSystemWatcherPath, FileSystemWatcherServer, - FileSystemWatcherServerProxy, ReconnectingFileSystemWatcherServer -} from '../common/filesystem-watcher-protocol'; +import { WebSocketConnectionProvider, FrontendApplicationContribution, LabelProviderContribution } from '@theia/core/lib/browser'; import { FileResourceResolver } from './file-resource'; import { bindFileSystemPreferences } from './filesystem-preferences'; import { FileSystemWatcher } from './filesystem-watcher'; import { FileSystemFrontendContribution } from './filesystem-frontend-contribution'; -import { FileSystemProxyFactory } from './filesystem-proxy-factory'; import { FileUploadService } from './file-upload-service'; import { FileTreeLabelProvider } from './file-tree/file-tree-label-provider'; +import { FileService, FileServiceContribution } from './file-service'; +import { RemoteFileSystemProvider, RemoteFileSystemServer, remoteFileSystemPath, RemoteFileSystemProxyFactory } from '../common/remote-file-system-provider'; +import { FileSystem, FileStat, FileMoveOptions, FileDeleteOptions, FileSystemError } from '../common/filesystem'; import URI from '@theia/core/lib/common/uri'; +import { FileOperationError, FileOperationResult, BaseStatWithMetadata, FileStatWithMetadata, etag } from '../common/files'; +import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider'; +import { RemoteFileServiceContribution } from './remote-file-service-contribution'; +import { UTF8 } from '@theia/core/lib/common/encodings'; export default new ContainerModule(bind => { bindFileSystemPreferences(bind); - bind(FileSystemWatcherServerProxy).toDynamicValue(ctx => - WebSocketConnectionProvider.createProxy(ctx.container, fileSystemWatcherPath) + bindContributionProvider(bind, FileServiceContribution); + bind(FileService).toSelf().inSingletonScope(); + + bind(RemoteFileSystemServer).toDynamicValue(ctx => + WebSocketConnectionProvider.createProxy(ctx.container, remoteFileSystemPath, new RemoteFileSystemProxyFactory()) ); - bind(FileSystemWatcherServer).to(ReconnectingFileSystemWatcherServer); + bind(RemoteFileSystemProvider).toSelf().inSingletonScope(); + bind(RemoteFileServiceContribution).toSelf().inSingletonScope(); + bind(FileServiceContribution).toService(RemoteFileServiceContribution); + bind(FileSystemWatcher).toSelf().inSingletonScope(); - bind(FileShouldOverwrite).toDynamicValue(context => async (file: FileStat, stat: FileStat): Promise => { - const labelProvider = context.container.get(LabelProvider); - const dialog = new ConfirmDialog({ - title: `The file '${labelProvider.getName(new URI(file.uri))}' has been changed on the file system.`, - msg: `Do you want to overwrite the changes made to '${labelProvider.getLongName(new URI(file.uri))}' on the file system?`, - ok: 'Yes', - cancel: 'No' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bind(FileSystem).toDynamicValue(({ container }) => { + const fileService = container.get(FileService); + const environments = container.get(EnvVariablesServer); + const convertStat: (stat: BaseStatWithMetadata | FileStatWithMetadata) => FileStat = stat => ({ + uri: stat.resource.toString(), + lastModification: stat.mtime, + size: stat.size, + isDirectory: 'isDirectory' in stat && stat.isDirectory, + children: 'children' in stat ? stat.children?.map(convertStat) : undefined }); - return !!await dialog.open(); - }).inSingletonScope(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rethrowError: (uri: string, error: any) => never = (uri, error) => { + if (error instanceof FileOperationError) { + if (error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + throw FileSystemError.FileNotFound(uri); + } + if (error.fileOperationResult === FileOperationResult.FILE_IS_DIRECTORY) { + throw FileSystemError.FileIsDirectory(uri); + } + if (error.fileOperationResult === FileOperationResult.FILE_NOT_DIRECTORY) { + throw FileSystemError.FileNotDirectory(uri); + } + if (error.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) { + throw FileSystemError.FileIsOutOfSync(uri); + } + } + throw error; + }; + return class implements FileSystem { + async getFileStat(uri: string): Promise { + try { + const stat = await fileService.resolve(new URI(uri), { resolveMetadata: true }); + return convertStat(stat); + } catch (e) { + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return undefined; + } + rethrowError(uri, e); + } + } + exists(uri: string): Promise { + return fileService.exists(new URI(uri)); + } + async resolveContent(uri: string, options?: { encoding?: string | undefined; } | undefined): Promise<{ stat: FileStat; content: string; }> { + try { + const content = await fileService.read(new URI(uri), options); + return { + stat: convertStat(content), + content: content.value + }; + } catch (e) { + rethrowError(uri, e); + } + } + async setContent(file: FileStat, content: string, options?: { encoding?: string | undefined; } | undefined): Promise { + try { + const result = await fileService.write(new URI(file.uri), content, { + ...options, + mtime: file.lastModification + }); + return convertStat(result); + } catch (e) { + rethrowError(file.uri, e); + } + } + async updateContent(file: FileStat, contentChanges: TextDocumentContentChangeEvent[], options?: { + encoding?: string | undefined; + overwriteEncoding?: string | undefined; + } | undefined): Promise { + try { + const result = await fileService.update(new URI(file.uri), contentChanges, { + mtime: file.lastModification, + etag: etag({ size: file.size, mtime: file.lastModification }), + readEncoding: options?.encoding || UTF8, + encoding: options?.overwriteEncoding, + overwriteEncoding: !!options?.overwriteEncoding + }); + return convertStat(result); + } catch (e) { + rethrowError(file.uri, e); + } + } + async move(sourceUri: string, targetUri: string, options?: FileMoveOptions | undefined): Promise { + try { + const result = await fileService.move(new URI(sourceUri), new URI(targetUri), options); + return convertStat(result); + } catch (e) { + rethrowError(sourceUri, e); + } + } + async copy(sourceUri: string, targetUri: string, options?: { overwrite?: boolean | undefined; recursive?: boolean | undefined; } | undefined): Promise { + try { + const result = await fileService.copy(new URI(sourceUri), new URI(targetUri), options); + return convertStat(result); + } catch (e) { + rethrowError(sourceUri, e); + } + } + async createFile(uri: string, options?: { content?: string | undefined; encoding?: string | undefined; } | undefined): Promise { + try { + const result = await fileService.create(new URI(uri), options?.content, { encoding: options?.encoding }); + return convertStat(result); + } catch (e) { + rethrowError(uri, e); + } + } + async createFolder(uri: string): Promise { + try { + const result = await fileService.createFolder(new URI(uri)); + return convertStat(result); + } catch (e) { + rethrowError(uri, e); + } + } + touchFile(uri: string): Promise { + throw new Error('Method not implemented.'); + } + async delete(uri: string, options?: FileDeleteOptions | undefined): Promise { + try { + return await fileService.delete(new URI(uri), { useTrash: options?.moveToTrash, recursive: true }); + } catch (e) { + rethrowError(uri, e); + } + } + async getEncoding(uri: string): Promise { + const { encoding } = await fileService.read(new URI(uri)); + return encoding; + } + async guessEncoding(uri: string): Promise { + const { encoding } = await fileService.read(new URI(uri), { autoGuessEncoding: true }); + return encoding; + } + async getRoots(): Promise { + const drives = await environments.getDrives(); + const roots = await Promise.all(drives.map(uri => this.getFileStat(uri))); + return roots.filter(root => !!root) as FileStat[]; + } + async getCurrentUserHome(): Promise { + return this.getFileStat(await environments.getHomeDirUri()); + } + getDrives(): Promise { + return environments.getDrives(); + } + access(uri: string, mode?: number | undefined): Promise { + return fileService.access(new URI(uri), mode); + } + getFsPath(uri: string): Promise { + return fileService.fsPath(new URI(uri)); + } - bind(FileSystemProxyFactory).toSelf(); - bind(FileSystem).toDynamicValue(ctx => { - const proxyFactory = ctx.container.get(FileSystemProxyFactory); - return WebSocketConnectionProvider.createProxy(ctx.container, fileSystemPath, proxyFactory); + }; }).inSingletonScope(); bindFileResource(bind); diff --git a/packages/filesystem/src/browser/filesystem-preferences.ts b/packages/filesystem/src/browser/filesystem-preferences.ts index 9d6bf6b147ba0..57044a67fd0c9 100644 --- a/packages/filesystem/src/browser/filesystem-preferences.ts +++ b/packages/filesystem/src/browser/filesystem-preferences.ts @@ -22,6 +22,7 @@ import { PreferenceSchema, PreferenceContribution } from '@theia/core/lib/browser/preferences'; +import { SUPPORTED_ENCODINGS } from '@theia/core/lib/browser/supported-encodings'; export const filesystemPreferenceSchema: PreferenceSchema = { 'type': 'object', @@ -53,6 +54,18 @@ export const filesystemPreferenceSchema: PreferenceSchema = { 'type': 'object', 'description': 'Configure file associations to languages (e.g. \"*.extension\": \"html\"). \ These have precedence over the default associations of the languages installed.' + }, + 'files.autoGuessEncoding': { + 'type': 'boolean', + 'default': false, + 'description': 'When enabled, the editor will attempt to guess the character set encoding when opening files. This setting can also be configured per language.', + 'scope': 'language-overridable', + 'included': Object.keys(SUPPORTED_ENCODINGS).length > 1 + }, + 'files.participants.timeout': { + type: 'number', + default: 5000, + markdownDescription: 'Timeout in milliseconds after which file participants for create, rename, and delete are cancelled. Use `0` to disable participants.' } } }; @@ -62,6 +75,9 @@ export interface FileSystemConfiguration { 'files.exclude': { [key: string]: boolean }; 'files.enableTrash': boolean; 'files.associations': { [filepattern: string]: string }; + 'files.encoding': string; + 'files.autoGuessEncoding': boolean; + 'files.participants.timeout': number; } export const FileSystemPreferences = Symbol('FileSystemPreferences'); diff --git a/packages/filesystem/src/browser/filesystem-proxy-factory.ts b/packages/filesystem/src/browser/filesystem-proxy-factory.ts deleted file mode 100644 index 978ac265036e0..0000000000000 --- a/packages/filesystem/src/browser/filesystem-proxy-factory.ts +++ /dev/null @@ -1,45 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2019 TypeFox and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { injectable, inject } from 'inversify'; -import { JsonRpcProxyFactory } from '@theia/core/lib/common/messaging/proxy-factory'; -import { FileSystem, FileDeleteOptions } from '../common/filesystem'; -import { FileSystemPreferences } from './filesystem-preferences'; - -@injectable() -export class FileSystemProxyFactory extends JsonRpcProxyFactory { - - @inject(FileSystemPreferences) - protected readonly preferences: FileSystemPreferences; - - get(target: FileSystem, propertyKey: PropertyKey, receiver: any): any { - const property = super.get(target, propertyKey, receiver); - if (propertyKey !== 'delete') { - return property; - } - const deleteFn: FileSystem['delete'] = (uri, options) => { - const opt: FileDeleteOptions = { ...options }; - if (opt.moveToTrash === undefined) { - opt.moveToTrash = this.preferences['files.enableTrash']; - } - return property(uri, opt); - }; - return deleteFn; - } - -} diff --git a/packages/filesystem/src/browser/filesystem-watcher.ts b/packages/filesystem/src/browser/filesystem-watcher.ts index 92fb6b42e5bea..d76ac94ba8cfe 100644 --- a/packages/filesystem/src/browser/filesystem-watcher.ts +++ b/packages/filesystem/src/browser/filesystem-watcher.ts @@ -18,9 +18,8 @@ import { injectable, inject, postConstruct } from 'inversify'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Emitter, WaitUntilEvent } from '@theia/core/lib/common/event'; import URI from '@theia/core/lib/common/uri'; -import { FileSystem, FileShouldOverwrite } from '../common/filesystem'; -import { DidFilesChangedParams, FileChangeType, FileSystemWatcherServer, WatchOptions } from '../common/filesystem-watcher-protocol'; -import { FileSystemPreferences } from './filesystem-preferences'; +import { FileChangeType, FileOperation } from '../common/files'; +import { FileService } from './file-service'; export { FileChangeType @@ -48,6 +47,9 @@ export namespace FileChange { } } +/** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileChangesEvent` instead + */ export type FileChangeEvent = FileChange[]; export namespace FileChangeEvent { export function isUpdated(event: FileChangeEvent, uri: URI): boolean { @@ -67,6 +69,9 @@ export namespace FileChangeEvent { } } +/** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `UserFileOperationEvent` instead + */ export interface FileMoveEvent extends WaitUntilEvent { sourceUri: URI targetUri: URI @@ -77,6 +82,9 @@ export namespace FileMoveEvent { } } +/** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `UserFileOperationEvent` instead + */ export interface FileEvent extends WaitUntilEvent { uri: URI } @@ -120,6 +128,8 @@ export class FileOperationEmitter implements Disposabl * * `on(will|did)(create|rename|delete)` events solely come from application * usage, not from actual filesystem. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.watch` instead */ @injectable() export class FileSystemWatcher implements Disposable { @@ -128,36 +138,61 @@ export class FileSystemWatcher implements Disposable { protected readonly toRestartAll = new DisposableCollection(); protected readonly onFileChangedEmitter = new Emitter(); + /** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.onDidFilesChange` instead + */ readonly onFilesChanged = this.onFileChangedEmitter.event; protected readonly fileCreateEmitter = new FileOperationEmitter(); + /** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.onWillRunUserOperation` instead + */ readonly onWillCreate = this.fileCreateEmitter.onWill; + /** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.onDidFailUserOperation` instead + */ readonly onDidFailCreate = this.fileCreateEmitter.onDidFail; + /** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), + * insead use `FileService.onDidRunUserOperation` for events triggered by user gestures + * or `FileService.onDidRunOperation` triggered by user gestures and programmatically + */ readonly onDidCreate = this.fileCreateEmitter.onDid; protected readonly fileDeleteEmitter = new FileOperationEmitter(); + /** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.onWillRunUserOperation` instead + */ readonly onWillDelete = this.fileDeleteEmitter.onWill; + /** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.onDidFailUserOperation` instead + */ readonly onDidFailDelete = this.fileDeleteEmitter.onDidFail; + /** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), + * insead use `FileService.onDidRunUserOperation` for events triggered by user gestures + * or `FileService.onDidRunOperation` triggered by user gestures and programmatically + */ readonly onDidDelete = this.fileDeleteEmitter.onDid; protected readonly fileMoveEmitter = new FileOperationEmitter(); + /** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.onWillRunUserOperation` instead + */ readonly onWillMove = this.fileMoveEmitter.onWill; + /** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.onDidFailUserOperation` instead + */ readonly onDidFailMove = this.fileMoveEmitter.onDidFail; + /** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), + * insead use `FileService.onDidRunUserOperation` for events triggered by user gestures + * or `FileService.onDidRunOperation` triggered by user gestures and programmatically + */ readonly onDidMove = this.fileMoveEmitter.onDid; - @inject(FileSystemWatcherServer) - protected readonly server: FileSystemWatcherServer; - - @inject(FileSystemPreferences) - protected readonly preferences: FileSystemPreferences; - - @inject(FileSystem) - protected readonly filesystem: FileSystem; - - // This is injected so we can avoid including UI stuff and make this class - // unit-testable. - @inject(FileShouldOverwrite) - protected readonly shouldOverwrite: FileShouldOverwrite; + @inject(FileService) + protected readonly fileService: FileService; @postConstruct() protected init(): void { @@ -165,28 +200,33 @@ export class FileSystemWatcher implements Disposable { this.toDispose.push(this.fileDeleteEmitter); this.toDispose.push(this.fileMoveEmitter); - this.toDispose.push(this.server); - this.server.setClient({ - onDidFilesChanged: e => this.onDidFilesChanged(e) - }); - - this.toDispose.push(this.preferences.onPreferenceChanged(e => { - if (e.preferenceName === 'files.watcherExclude') { - this.toRestartAll.dispose(); + this.toDispose.push(this.fileService.onWillRunUserOperation(event => { + if (event.operation === FileOperation.CREATE) { + this.fileCreateEmitter.fireWill({ uri: event.target }); + } else if (event.operation === FileOperation.DELETE) { + this.fileDeleteEmitter.fireWill({ uri: event.target }); + } else if (event.operation === FileOperation.MOVE && event.source) { + this.fileMoveEmitter.fireWill({ sourceUri: event.source, targetUri: event.target }); + } + })); + this.toDispose.push(this.fileService.onDidFailUserOperation(event => { + if (event.operation === FileOperation.CREATE) { + this.fileCreateEmitter.fireDid(true, { uri: event.target }); + } else if (event.operation === FileOperation.DELETE) { + this.fileDeleteEmitter.fireDid(true, { uri: event.target }); + } else if (event.operation === FileOperation.MOVE && event.source) { + this.fileMoveEmitter.fireDid(true, { sourceUri: event.source, targetUri: event.target }); + } + })); + this.toDispose.push(this.fileService.onDidRunUserOperation(event => { + if (event.operation === FileOperation.CREATE) { + this.fileCreateEmitter.fireDid(false, { uri: event.target }); + } else if (event.operation === FileOperation.DELETE) { + this.fileDeleteEmitter.fireDid(false, { uri: event.target }); + } else if (event.operation === FileOperation.MOVE && event.source) { + this.fileMoveEmitter.fireDid(false, { sourceUri: event.source, targetUri: event.target }); } })); - - this.filesystem.setClient({ - /* eslint-disable no-void */ - shouldOverwrite: this.shouldOverwrite.bind(this), - willCreate: async uri => void await this.fileCreateEmitter.fireWill({ uri: new URI(uri) }), - didCreate: async (uri, failed) => void await this.fileCreateEmitter.fireDid(failed, { uri: new URI(uri) }), - willDelete: async uri => void await this.fileDeleteEmitter.fireWill({ uri: new URI(uri) }), - didDelete: async (uri, failed) => void await this.fileDeleteEmitter.fireDid(failed, { uri: new URI(uri) }), - willMove: async (sourceUri, targetUri) => void await this.fileMoveEmitter.fireWill({ sourceUri: new URI(sourceUri), targetUri: new URI(targetUri) }), - didMove: async (sourceUri, targetUri, failed) => void await this.fileMoveEmitter.fireDid(failed, { sourceUri: new URI(sourceUri), targetUri: new URI(targetUri) }), - /* eslint-enable no-void */ - }); } /** @@ -196,52 +236,14 @@ export class FileSystemWatcher implements Disposable { this.toDispose.dispose(); } - protected onDidFilesChanged(event: DidFilesChangedParams): void { - const changes = event.changes.map(change => { - uri: new URI(change.uri), - type: change.type - }); - this.onFileChangedEmitter.fire(changes); - } - /** * Start file watching under the given uri. * * Resolve when watching is started. * Return a disposable to stop file watching under the given uri. */ - watchFileChanges(uri: URI): Promise { - return this.createWatchOptions(uri.toString()) - .then(options => - this.server.watchFileChanges(uri.toString(), options) - ) - .then(watcher => { - const toDispose = new DisposableCollection(); - const toStop = Disposable.create(() => - this.server.unwatchFileChanges(watcher) - ); - const toRestart = toDispose.push(toStop); - this.toRestartAll.push(Disposable.create(() => { - toRestart.dispose(); - toStop.dispose(); - this.watchFileChanges(uri).then(disposable => - toDispose.push(disposable) - ); - })); - return toDispose; - }); - } - - protected createWatchOptions(uri: string): Promise { - return this.getIgnored(uri).then(ignored => ({ - // always ignore temporary upload files - ignored: ignored.concat('**/theia_upload_*') - })); - } - - protected async getIgnored(uri: string): Promise { - const patterns = this.preferences.get('files.watcherExclude', undefined, uri); - return Object.keys(patterns).filter(pattern => patterns[pattern]); + async watchFileChanges(uri: URI): Promise { + return this.fileService.watch(uri); } } diff --git a/packages/filesystem/src/common/test/mock-filesystem-watcher-server.ts b/packages/filesystem/src/browser/remote-file-service-contribution.ts similarity index 50% rename from packages/filesystem/src/common/test/mock-filesystem-watcher-server.ts rename to packages/filesystem/src/browser/remote-file-service-contribution.ts index 8dcbbd582385a..9c6ada265b903 100644 --- a/packages/filesystem/src/common/test/mock-filesystem-watcher-server.ts +++ b/packages/filesystem/src/browser/remote-file-service-contribution.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2017 Ericsson and others. + * Copyright (C) 2020 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -14,22 +14,25 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable } from 'inversify'; -import { FileSystemWatcherServer, FileSystemWatcherClient, WatchOptions } from '../filesystem-watcher-protocol'; +import { inject, injectable } from 'inversify'; +import { FileServiceContribution, FileService } from './file-service'; +import { RemoteFileSystemProvider } from '../common/remote-file-system-provider'; @injectable() -export class MockFilesystemWatcherServer implements FileSystemWatcherServer { +export class RemoteFileServiceContribution implements FileServiceContribution { - dispose(): void { } + @inject(RemoteFileSystemProvider) + protected readonly provider: RemoteFileSystemProvider; - watchFileChanges(uri: string, options?: WatchOptions): Promise { - return Promise.resolve(0); + registerFileSystemProviders(service: FileService): void { + const registering = this.provider.ready.then(() => + service.registerProvider('file', this.provider) + ); + service.onWillActivateFileSystemProvider(event => { + if (event.scheme === 'file') { + event.waitUntil(registering); + } + }); } - unwatchFileChanges(watcher: number): Promise { - return Promise.resolve(); - } - - setClient(client: FileSystemWatcherClient): void { } - } diff --git a/packages/filesystem/src/common/delegating-file-system-provider.ts b/packages/filesystem/src/common/delegating-file-system-provider.ts new file mode 100644 index 0000000000000..eb0f58b613fce --- /dev/null +++ b/packages/filesystem/src/common/delegating-file-system-provider.ts @@ -0,0 +1,204 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import URI from '@theia/core/lib/common/uri'; +import { Event, Emitter } from '@theia/core/lib/common'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { + FileSystemProvider, FileSystemProviderCapabilities, WatchOptions, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileChange, Stat, FileType, + hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, hasAccessCapability, FileUpdateOptions, hasUpdateCapability, FileUpdateResult +} from './files'; +import type { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; + +export class DelegatingFileSystemProvider implements Required, Disposable { + + private readonly onDidChangeFileEmitter = new Emitter(); + readonly onDidChangeFile = this.onDidChangeFileEmitter.event; + + constructor( + protected readonly delegate: FileSystemProvider, + protected readonly options: DelegatingFileSystemProvider.Options, + protected readonly toDispose = new DisposableCollection() + ) { + this.toDispose.push(this.onDidChangeFileEmitter); + } + + dispose(): void { + this.toDispose.dispose(); + } + + get capabilities(): FileSystemProviderCapabilities { + return this.delegate.capabilities; + } + + get onDidChangeCapabilities(): Event { + return this.delegate.onDidChangeCapabilities; + } + + watch(resource: URI, opts: WatchOptions): Disposable { + return this.delegate.watch(this.options.uriConverter.to(resource), opts); + } + + stat(resource: URI): Promise { + return this.delegate.stat(this.options.uriConverter.to(resource)); + } + + access(resource: URI, mode?: number): Promise { + if (hasAccessCapability(this.delegate)) { + return this.delegate.access(this.options.uriConverter.to(resource), mode); + } + throw new Error('not supported'); + } + + fsPath(resource: URI): Promise { + if (hasAccessCapability(this.delegate)) { + return this.delegate.fsPath(this.options.uriConverter.to(resource)); + } + throw new Error('not supported'); + } + + mkdir(resource: URI): Promise { + return this.delegate.mkdir(this.options.uriConverter.to(resource)); + } + + rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + return this.delegate.rename(this.options.uriConverter.to(from), this.options.uriConverter.to(to), opts); + } + + copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + if (hasFileFolderCopyCapability(this.delegate)) { + return this.delegate.copy(this.options.uriConverter.to(from), this.options.uriConverter.to(to), opts); + } + throw new Error('not supported'); + } + + readFile(resource: URI): Promise { + if (hasReadWriteCapability(this.delegate)) { + return this.delegate.readFile(this.options.uriConverter.to(resource)); + } + throw new Error('not supported'); + } + + readdir(resource: URI): Promise<[string, FileType][]> { + return this.delegate.readdir(this.options.uriConverter.to(resource)); + } + + writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + if (hasReadWriteCapability(this.delegate)) { + return this.delegate.writeFile(this.options.uriConverter.to(resource), content, opts); + } + throw new Error('not supported'); + } + + open(resource: URI, opts: FileOpenOptions): Promise { + if (hasOpenReadWriteCloseCapability(this.delegate)) { + return this.delegate.open(this.options.uriConverter.to(resource), opts); + } + throw new Error('not supported'); + } + + close(fd: number): Promise { + if (hasOpenReadWriteCloseCapability(this.delegate)) { + return this.delegate.close(fd); + } + throw new Error('not supported'); + } + + read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + if (hasOpenReadWriteCloseCapability(this.delegate)) { + return this.delegate.read(fd, pos, data, offset, length); + } + throw new Error('not supported'); + } + + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + if (hasOpenReadWriteCloseCapability(this.delegate)) { + return this.delegate.write(fd, pos, data, offset, length); + } + throw new Error('not supported'); + } + + delete(resource: URI, opts: FileDeleteOptions): Promise { + return this.delegate.delete(this.options.uriConverter.to(resource), opts); + } + + updateFile(resource: URI, changes: TextDocumentContentChangeEvent[], opts: FileUpdateOptions): Promise { + if (hasUpdateCapability(this.delegate)) { + return this.delegate.updateFile(resource, changes, opts); + } + throw new Error('not supported'); + } + + protected handleFileChanges(changes: readonly FileChange[]): void { + const delegatingChanges: FileChange[] = []; + for (const change of changes) { + const delegatingResource = this.options.uriConverter.from(change.resource); + if (delegatingResource) { + delegatingChanges.push({ + resource: delegatingResource, + type: change.type + }); + } + } + if (delegatingChanges.length) { + this.onDidChangeFileEmitter.fire(delegatingChanges); + } + } + + /** + * Converts to an underlying fs provider resource format. + * + * For example converting `user-storage` resources to `file` resources under a user home: + * user-storage:/settings.json => file://home/.theia/settings.json + */ + toUnderlyingResource(resource: URI): URI { + return this.options.uriConverter.to(resource); + } + + /** + * Converts from an underlying fs provider resource format. + * + * For example converting `file` resources unser a user home to `user-storage` resource: + * - file://home/.theia/settings.json => user-storage:/settings.json + * - file://documents/some-document.txt => undefined + */ + fromUnderlyingResource(resource: URI): URI { + return this.options.uriConverter.to(resource); + } + +} +export namespace DelegatingFileSystemProvider { + export interface Options { + uriConverter: URIConverter + } + export interface URIConverter { + /** + * Converts to an underlying fs provider resource format. + * + * For example converting `user-storage` resources to `file` resources under a user home: + * user-storage:/settings.json => file://home/.theia/settings.json + */ + to(resource: URI): URI; + /** + * Converts from an underlying fs provider resource format. + * + * For example converting `file` resources unser a user home to `user-storage` resource: + * - file://home/.theia/settings.json => user-storage:/settings.json + * - file://documents/some-document.txt => undefined + */ + from(resource: URI): URI | undefined; + } +} diff --git a/packages/filesystem/src/common/files.ts b/packages/filesystem/src/common/files.ts new file mode 100644 index 0000000000000..193c2c66526cf --- /dev/null +++ b/packages/filesystem/src/common/files.ts @@ -0,0 +1,697 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/platform/files/common/files.ts + +import URI from '@theia/core/lib/common/uri'; +import { Event } from '@theia/core/lib/common/event'; +import { Disposable as IDisposable } from '@theia/core/lib/common/disposable'; +import { BinaryBuffer, BinaryBufferReadableStream } from '@theia/core/lib/common/buffer'; +import type { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; + +export const enum FileOperation { + CREATE, + DELETE, + MOVE, + COPY +} + +export class FileOperationEvent { + + constructor(resource: URI, operation: FileOperation.DELETE); + constructor(resource: URI, operation: FileOperation.CREATE | FileOperation.MOVE | FileOperation.COPY, target: FileStatWithMetadata); + constructor(public readonly resource: URI, public readonly operation: FileOperation, public readonly target?: FileStatWithMetadata) { } + + isOperation(operation: FileOperation.DELETE): boolean; + isOperation(operation: FileOperation.MOVE | FileOperation.COPY | FileOperation.CREATE): this is { readonly target: FileStatWithMetadata }; + isOperation(operation: FileOperation): boolean { + return this.operation === operation; + } +} + +/** + * Possible changes that can occur to a file. + */ +export const enum FileChangeType { + UPDATED = 0, + ADDED = 1, + DELETED = 2 +} + +/** + * Identifies a single change in a file. + */ +export interface FileChange { + + /** + * The type of change that occurred to the file. + */ + readonly type: FileChangeType; + + /** + * The unified resource identifier of the file that changed. + */ + readonly resource: URI; +} + +export class FileChangesEvent { + + constructor(public readonly changes: readonly FileChange[]) { } + + /** + * Returns true if this change event contains the provided file with the given change type (if provided). In case of + * type DELETED, this method will also return true if a folder got deleted that is the parent of the + * provided file path. + */ + contains(resource: URI, type?: FileChangeType): boolean { + if (!resource) { + return false; + } + + const checkForChangeType = typeof type === 'number'; + + return this.changes.some(change => { + if (checkForChangeType && change.type !== type) { + return false; + } + + // For deleted also return true when deleted folder is parent of target path + if (change.type === FileChangeType.DELETED) { + return resource.isEqualOrParent(change.resource); + } + + return resource.toString() === change.resource.toString(); + }); + } + + /** + * Returns the changes that describe added files. + */ + getAdded(): FileChange[] { + return this.getOfType(FileChangeType.ADDED); + } + + /** + * Returns if this event contains added files. + */ + gotAdded(): boolean { + return this.hasType(FileChangeType.ADDED); + } + + /** + * Returns the changes that describe deleted files. + */ + getDeleted(): FileChange[] { + return this.getOfType(FileChangeType.DELETED); + } + + /** + * Returns if this event contains deleted files. + */ + gotDeleted(): boolean { + return this.hasType(FileChangeType.DELETED); + } + + /** + * Returns the changes that describe updated files. + */ + getUpdated(): FileChange[] { + return this.getOfType(FileChangeType.UPDATED); + } + + /** + * Returns if this event contains updated files. + */ + gotUpdated(): boolean { + return this.hasType(FileChangeType.UPDATED); + } + + private getOfType(type: FileChangeType): FileChange[] { + return this.changes.filter(change => change.type === type); + } + + private hasType(type: FileChangeType): boolean { + return this.changes.some(change => change.type === type); + } +} + +export interface BaseStat { + + /** + * The unified resource identifier of this file or folder. + */ + resource: URI; + + /** + * The name which is the last segment + * of the {{path}}. + */ + name: string; + + /** + * The size of the file. + * + * The value may or may not be resolved as + * it is optional. + */ + size?: number; + + /** + * The last modification date represented as millis from unix epoch. + * + * The value may or may not be resolved as + * it is optional. + */ + mtime?: number; + + /** + * The creation date represented as millis from unix epoch. + * + * The value may or may not be resolved as + * it is optional. + */ + ctime?: number; + + /** + * A unique identifier thet represents the + * current state of the file or directory. + * + * The value may or may not be resolved as + * it is optional. + */ + etag?: string; +} +export namespace BaseStat { + export function is(arg: Object | undefined): arg is BaseStat { + return !!arg && typeof arg === 'object' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + && ('resource' in arg && arg['resource'] instanceof URI) + && ('name' in arg && typeof arg['name'] === 'string'); + } +} + +export interface BaseStatWithMetadata extends BaseStat { + mtime: number; + ctime: number; + etag: string; + size: number; +} + +/** + * A file resource with meta information. + */ +export interface FileStat extends BaseStat { + + /** + * The resource is a file. + */ + isFile: boolean; + + /** + * The resource is a directory. + */ + isDirectory: boolean; + + /** + * The resource is a symbolic link. + */ + isSymbolicLink: boolean; + + /** + * The children of the file stat or undefined if none. + */ + children?: FileStat[]; +} +export namespace FileStat { + export function is(arg: Object | undefined): arg is BaseStat { + return BaseStat.is(arg) && + ('isFile' in arg && typeof arg['isFile'] === 'boolean') && + ('isDirectory' in arg && typeof arg['isDirectory'] === 'boolean') && + ('isSymbolicLink' in arg && typeof arg['isSymbolicLink'] === 'boolean'); + } + export function asFileType(stat: FileStat): FileType { + let res = 0; + if (stat.isFile) { + res += FileType.File; + + } else if (stat.isDirectory) { + res += FileType.Directory; + } + if (stat.isSymbolicLink) { + res += FileType.SymbolicLink; + } + return res; + } + export function toStat(stat: FileStat): Stat | { type: FileType } & Partial { + return { + type: asFileType(stat), + ctime: stat.ctime, + mtime: stat.mtime, + size: stat.size + }; + } + export function fromStat(resource: URI, stat: Stat): FileStatWithMetadata; + export function fromStat(resource: URI, stat: { type: FileType } & Partial): FileStat; + export function fromStat(resource: URI, stat: Stat | { type: FileType } & Partial): FileStat { + return { + resource, + name: resource.path.base || resource.path.toString(), + isFile: (stat.type & FileType.File) !== 0, + isDirectory: (stat.type & FileType.Directory) !== 0, + isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0, + mtime: stat.mtime, + ctime: stat.ctime, + size: stat.size, + etag: etag({ mtime: stat.mtime, size: stat.size }) + }; + } + export function dir(resource: string | URI, stat?: Partial>): FileStat { + return fromStat(resource instanceof URI ? resource : new URI(resource), { type: FileType.Directory, ...stat }); + } + export function file(resource: string | URI, stat?: Partial>): FileStat { + return fromStat(resource instanceof URI ? resource : new URI(resource), { type: FileType.File, ...stat }); + } +} + +export interface FileStatWithMetadata extends FileStat, BaseStatWithMetadata { + mtime: number; + ctime: number; + etag: string; + size: number; + children?: FileStatWithMetadata[]; +} + +export interface ResolveFileResult { + stat?: FileStat; + success: boolean; +} + +export interface ResolveFileResultWithMetadata extends ResolveFileResult { + stat?: FileStatWithMetadata; +} + +export interface FileContent extends BaseStatWithMetadata { + + /** + * The content of a file as buffer. + */ + value: BinaryBuffer; +} + +export interface FileStreamContent extends BaseStatWithMetadata { + + /** + * The content of a file as stream. + */ + value: BinaryBufferReadableStream; +} + +export interface WriteFileOptions { + + /** + * The last known modification time of the file. This can be used to prevent dirty writes. + */ + readonly mtime?: number; + + /** + * The etag of the file. This can be used to prevent dirty writes. + */ + readonly etag?: string; +} + +export interface ReadFileOptions extends FileReadStreamOptions { + + /** + * The optional etag parameter allows to return early from resolving the resource if + * the contents on disk match the etag. This prevents accumulated reading of resources + * that have been read already with the same etag. + * It is the task of the caller to makes sure to handle this error case from the promise. + */ + readonly etag?: string; +} + +export interface WriteFileOptions { + + /** + * The last known modification time of the file. This can be used to prevent dirty writes. + */ + readonly mtime?: number; + + /** + * The etag of the file. This can be used to prevent dirty writes. + */ + readonly etag?: string; +} + +export interface ResolveFileOptions { + + /** + * Automatically continue resolving children of a directory until the provided resources + * are found. + */ + readonly resolveTo?: readonly URI[]; + + /** + * Automatically continue resolving children of a directory if the number of children is 1. + */ + readonly resolveSingleChildDescendants?: boolean; + + /** + * Will resolve mtime, ctime, size and etag of files if enabled. This can have a negative impact + * on performance and thus should only be used when these values are required. + */ + readonly resolveMetadata?: boolean; +} + +export interface ResolveMetadataFileOptions extends ResolveFileOptions { + readonly resolveMetadata: true; +} + +export interface FileOperationOptions { + /** + * Indicates that a user action triggered the opening, e.g. + * via mouse or keyboard use. Default is true. + */ + fromUserGesture?: boolean +} + +export interface MoveFileOptions extends FileOperationOptions, Partial { +} + +export interface CopyFileOptions extends FileOperationOptions, Partial { +} + +export interface CreateFileOptions extends FileOperationOptions, Partial { +} + +export class FileOperationError extends Error { + constructor(message: string, public fileOperationResult: FileOperationResult, public options?: ReadFileOptions & WriteFileOptions & CreateFileOptions) { + super(message); + Object.setPrototypeOf(this, FileOperationError.prototype); + } +} + +export const enum FileOperationResult { + FILE_IS_DIRECTORY, + FILE_NOT_FOUND, + FILE_NOT_MODIFIED_SINCE, + FILE_MODIFIED_SINCE, + FILE_MOVE_CONFLICT, + FILE_READ_ONLY, + FILE_PERMISSION_DENIED, + FILE_TOO_LARGE, + FILE_INVALID_PATH, + FILE_EXCEEDS_MEMORY_LIMIT, + FILE_NOT_DIRECTORY, + FILE_OTHER_ERROR +} + +export interface FileOverwriteOptions { + /** + * Overwrite the file to create if it already exists on disk. Otherwise + * an error will be thrown (FILE_MODIFIED_SINCE). + */ + overwrite: boolean; +} + +export interface FileReadStreamOptions { + + /** + * Is an integer specifying where to begin reading from in the file. If position is undefined, + * data will be read from the current file position. + */ + readonly position?: number; + + /** + * Is an integer specifying how many bytes to read from the file. By default, all bytes + * will be read. + */ + readonly length?: number; +} + +export interface FileUpdateOptions { + readEncoding: string; + writeEncoding: string; + overwriteEncoding: boolean; +} +export interface FileUpdateResult extends Stat { + encoding: string; +} + +export interface FileWriteOptions { + overwrite: boolean; + create: boolean; +} + +export interface FileOpenOptions { + create: boolean; +} + +export interface FileDeleteOptions { + recursive: boolean; + useTrash: boolean; +} + +export enum FileType { + Unknown = 0, + File = 1, + Directory = 2, + SymbolicLink = 64 +} + +export interface Stat { + type: FileType; + + /** + * The last modification date represented as millis from unix epoch. + */ + mtime: number; + + /** + * The creation date represented as millis from unix epoch. + */ + ctime: number; + + size: number; +} + +export interface WatchOptions { + recursive: boolean; + excludes: string[]; +} + +export const enum FileSystemProviderCapabilities { + FileReadWrite = 1 << 1, + FileOpenReadWriteClose = 1 << 2, + + FileFolderCopy = 1 << 3, + + PathCaseSensitive = 1 << 10, + Readonly = 1 << 11, + + Trash = 1 << 12, + + Access = 1 << 24, + Update = 1 << 25 +} + +export enum FileSystemProviderErrorCode { + FileExists = 'EntryExists', + FileNotFound = 'EntryNotFound', + FileNotADirectory = 'EntryNotADirectory', + FileIsADirectory = 'EntryIsADirectory', + NoPermissions = 'NoPermissions', + Unavailable = 'Unavailable', + Unknown = 'Unknown' +} + +export class FileSystemProviderError extends Error { + + constructor(message: string, public readonly code: FileSystemProviderErrorCode) { + super(message); + Object.setPrototypeOf(this, FileSystemProviderError.prototype); + } +} + +export function createFileSystemProviderError(error: Error | string, code: FileSystemProviderErrorCode): FileSystemProviderError { + const providerError = new FileSystemProviderError(error.toString(), code); + markAsFileSystemProviderError(providerError, code); + + return providerError; +} + +export function ensureFileSystemProviderError(error?: Error): Error { + if (!error) { + return createFileSystemProviderError('Unknown Error', FileSystemProviderErrorCode.Unknown); // https://github.com/Microsoft/vscode/issues/72798 + } + + return error; +} + +export const FileSystemProvider = Symbol('FileSystemProvider'); +export interface FileSystemProvider { + + readonly capabilities: FileSystemProviderCapabilities; + readonly onDidChangeCapabilities: Event; + + readonly onDidChangeFile: Event; + watch(resource: URI, opts: WatchOptions): IDisposable; + + stat(resource: URI): Promise; + mkdir(resource: URI): Promise; + readdir(resource: URI): Promise<[string, FileType][]>; + delete(resource: URI, opts: FileDeleteOptions): Promise; + + rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise; + copy?(from: URI, to: URI, opts: FileOverwriteOptions): Promise; + + readFile?(resource: URI): Promise; + writeFile?(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise; + + open?(resource: URI, opts: FileOpenOptions): Promise; + close?(fd: number): Promise; + read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; + write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; + + access?(resource: URI, mode?: number): Promise; + fsPath?(resource: URI): Promise; + + updateFile?(resource: URI, changes: TextDocumentContentChangeEvent[], opts: FileUpdateOptions): Promise; +} + +export interface FileSystemProviderWithAccessCapability extends FileSystemProvider { + access(resource: URI, mode?: number): Promise; + fsPath(resource: URI): Promise; +} + +export function hasAccessCapability(provider: FileSystemProvider): provider is FileSystemProviderWithAccessCapability { + return !!(provider.capabilities & FileSystemProviderCapabilities.Access); +} + +export interface FileSystemProviderWithUpdateCapability extends FileSystemProvider { + updateFile(resource: URI, changes: TextDocumentContentChangeEvent[], opts: FileUpdateOptions): Promise; +} + +export function hasUpdateCapability(provider: FileSystemProvider): provider is FileSystemProviderWithUpdateCapability { + return !!(provider.capabilities & FileSystemProviderCapabilities.Update); +} + +export interface FileSystemProviderWithFileReadWriteCapability extends FileSystemProvider { + readFile(resource: URI): Promise; + writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise; +} + +export function hasReadWriteCapability(provider: FileSystemProvider): provider is FileSystemProviderWithFileReadWriteCapability { + return !!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite); +} + +export interface FileSystemProviderWithFileFolderCopyCapability extends FileSystemProvider { + copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise; +} + +export function hasFileFolderCopyCapability(provider: FileSystemProvider): provider is FileSystemProviderWithFileFolderCopyCapability { + return !!(provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy); +} + +export interface FileSystemProviderWithOpenReadWriteCloseCapability extends FileSystemProvider { + open(resource: URI, opts: FileOpenOptions): Promise; + close(fd: number): Promise; + read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; +} + +export function hasOpenReadWriteCloseCapability(provider: FileSystemProvider): provider is FileSystemProviderWithOpenReadWriteCloseCapability { + return !!(provider.capabilities & FileSystemProviderCapabilities.FileOpenReadWriteClose); +} + +export function markAsFileSystemProviderError(error: Error, code: FileSystemProviderErrorCode): Error { + error.name = code ? `${code} (FileSystemError)` : 'FileSystemError'; + + return error; +} + +export function toFileSystemProviderErrorCode(error: Error | undefined | null): FileSystemProviderErrorCode { + + // Guard against abuse + if (!error) { + return FileSystemProviderErrorCode.Unknown; + } + + // FileSystemProviderError comes with the code + if (error instanceof FileSystemProviderError) { + return error.code; + } + + // Any other error, check for name match by assuming that the error + // went through the markAsFileSystemProviderError() method + const match = /^(.+) \(FileSystemError\)$/.exec(error.name); + if (!match) { + return FileSystemProviderErrorCode.Unknown; + } + + switch (match[1]) { + case FileSystemProviderErrorCode.FileExists: return FileSystemProviderErrorCode.FileExists; + case FileSystemProviderErrorCode.FileIsADirectory: return FileSystemProviderErrorCode.FileIsADirectory; + case FileSystemProviderErrorCode.FileNotADirectory: return FileSystemProviderErrorCode.FileNotADirectory; + case FileSystemProviderErrorCode.FileNotFound: return FileSystemProviderErrorCode.FileNotFound; + case FileSystemProviderErrorCode.NoPermissions: return FileSystemProviderErrorCode.NoPermissions; + case FileSystemProviderErrorCode.Unavailable: return FileSystemProviderErrorCode.Unavailable; + } + + return FileSystemProviderErrorCode.Unknown; +} + +export function toFileOperationResult(error: Error): FileOperationResult { + + // FileSystemProviderError comes with the result already + if (error instanceof FileOperationError) { + return error.fileOperationResult; + } + + // Otherwise try to find from code + switch (toFileSystemProviderErrorCode(error)) { + case FileSystemProviderErrorCode.FileNotFound: + return FileOperationResult.FILE_NOT_FOUND; + case FileSystemProviderErrorCode.FileIsADirectory: + return FileOperationResult.FILE_IS_DIRECTORY; + case FileSystemProviderErrorCode.FileNotADirectory: + return FileOperationResult.FILE_NOT_DIRECTORY; + case FileSystemProviderErrorCode.NoPermissions: + return FileOperationResult.FILE_PERMISSION_DENIED; + case FileSystemProviderErrorCode.FileExists: + return FileOperationResult.FILE_MOVE_CONFLICT; + default: + return FileOperationResult.FILE_OTHER_ERROR; + } +} + +/** + * A hint to disable etag checking for reading/writing. + */ +export const ETAG_DISABLED = ''; + +export function etag(stat: { mtime: number, size: number }): string; +export function etag(stat: { mtime: number | undefined, size: number | undefined }): string | undefined; +export function etag(stat: { mtime: number | undefined, size: number | undefined }): string | undefined { + if (typeof stat.size !== 'number' || typeof stat.mtime !== 'number') { + return undefined; + } + + return stat.mtime.toString(29) + stat.size.toString(31); +} diff --git a/packages/filesystem/src/common/filesystem-utils.ts b/packages/filesystem/src/common/filesystem-utils.ts index ca274b28b4488..11fed36301db6 100644 --- a/packages/filesystem/src/common/filesystem-utils.ts +++ b/packages/filesystem/src/common/filesystem-utils.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { FileStat } from './filesystem'; +import { FileStat } from '../common/files'; import URI from '@theia/core/lib/common/uri'; import { Path } from '@theia/core/lib/common'; @@ -47,7 +47,7 @@ export namespace FileSystemUtils { * @param ext the resource extension */ export function generateUniqueResourceURI(parentUri: URI, parent: FileStat, name: string, ext: string = ''): URI { - const children = !parent.children ? [] : parent.children!.map(child => new URI(child.uri)); + const children = !parent.children ? [] : parent.children!.map(child => child.resource); let index = 1; let base = name + ext; diff --git a/packages/filesystem/src/common/filesystem-watcher-protocol.ts b/packages/filesystem/src/common/filesystem-watcher-protocol.ts index 67674f38464b0..dc8845028a903 100644 --- a/packages/filesystem/src/common/filesystem-watcher-protocol.ts +++ b/packages/filesystem/src/common/filesystem-watcher-protocol.ts @@ -16,8 +16,8 @@ import { injectable, inject } from 'inversify'; import { JsonRpcServer, JsonRpcProxy } from '@theia/core'; - -export const fileSystemWatcherPath = '/services/fs-watcher'; +import { FileChangeType } from './files'; +export { FileChangeType }; export const FileSystemWatcherServer = Symbol('FileSystemWatcherServer'); export interface FileSystemWatcherServer extends JsonRpcServer { @@ -55,12 +55,6 @@ export interface FileChange { type: FileChangeType; } -export enum FileChangeType { - UPDATED = 0, - ADDED = 1, - DELETED = 2 -} - export const FileSystemWatcherServerProxy = Symbol('FileSystemWatcherServerProxy'); export type FileSystemWatcherServerProxy = JsonRpcProxy; diff --git a/packages/filesystem/src/common/filesystem.ts b/packages/filesystem/src/common/filesystem.ts index 668512f7587fd..56d64d8467719 100644 --- a/packages/filesystem/src/common/filesystem.ts +++ b/packages/filesystem/src/common/filesystem.ts @@ -14,14 +14,16 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +/* eslint-disable max-len */ + import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; -import { JsonRpcServer, ApplicationError } from '@theia/core/lib/common'; -import { injectable } from 'inversify'; -export const fileSystemPath = '/services/filesystem'; +import { ApplicationError } from '@theia/core/lib/common'; +/** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService` instead + */ export const FileSystem = Symbol('FileSystem'); - -export interface FileSystem extends JsonRpcServer { +export interface FileSystem { /** * Returns the file stat for the given URI. @@ -29,21 +31,29 @@ export interface FileSystem extends JsonRpcServer { * If the uri points to a folder it will contain one level of unresolved children. * * `undefined` if a file for the given URI does not exist. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.resolve` instead */ getFileStat(uri: string): Promise; /** * Finds out if a file identified by the resource exists. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.exists` instead */ exists(uri: string): Promise; /** * Resolve the contents of a file identified by the resource. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.read` instead */ resolveContent(uri: string, options?: { encoding?: string }): Promise<{ stat: FileStat, content: string }>; /** * Updates the content replacing its previous value. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.write` instead */ setContent(file: FileStat, content: string, options?: { encoding?: string }): Promise; @@ -58,6 +68,8 @@ export interface FileSystem extends JsonRpcServer { * | 2 | undefined | ✓ | read file in default encoding; write file in `overwriteEncoding` | * | 3 | ✓ | undefined | read & write file in `encoding` | * | 4 | ✓ | ✓ | read file in `encoding`; write file in `overwriteEncoding` | + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.write` instead */ updateContent(file: FileStat, contentChanges: TextDocumentContentChangeEvent[], options?: { encoding?: string, overwriteEncoding?: string }): Promise; @@ -73,6 +85,7 @@ export interface FileSystem extends JsonRpcServer { * | empty dir | ✓ | x | x | overwrite | * | dir | ✓ | x | overwrite | overwrite | * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.move` instead */ move(sourceUri: string, targetUri: string, options?: FileMoveOptions): Promise; @@ -80,6 +93,8 @@ export interface FileSystem extends JsonRpcServer { * Copies the file to a path identified by the resource. * * The optional parameter overwrite can be set to replace an existing file at the location. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.copy` instead */ copy(sourceUri: string, targetUri: string, options?: { overwrite?: boolean, recursive?: boolean }): Promise; @@ -88,12 +103,16 @@ export interface FileSystem extends JsonRpcServer { * will have the stat model object as a result. * * The optional parameter content can be used as value to fill into the new file. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.create` instead */ createFile(uri: string, options?: { content?: string, encoding?: string }): Promise; /** * Creates a new folder with the given path. The returned promise * will have the stat model object as a result. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.createFolder` instead */ createFolder(uri: string): Promise; @@ -106,16 +125,22 @@ export interface FileSystem extends JsonRpcServer { /** * Deletes the provided file. The optional moveToTrash parameter allows to * move the file to trash. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.delete` instead */ delete(uri: string, options?: FileDeleteOptions): Promise; /** * Returns the encoding of the given file resource. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908) use `FileService.read` without `autoGuessEncoding` option instead */ getEncoding(uri: string): Promise; /** * Guess encoding of a given file based on its content. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.read` with `autoGuessEncoding` option instead */ guessEncoding(uri: string): Promise; @@ -126,11 +151,15 @@ export interface FileSystem extends JsonRpcServer { /** * Returns a promise that resolves to a file stat representing the current user's home directory. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `EnvVariablesServer.getHomeDirUri` instead */ getCurrentUserHome(): Promise; /** * Resolves to an array of URIs pointing to the available drives on the filesystem. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `EnvVariablesServer.getDrives` instead */ getDrives(): Promise; @@ -140,6 +169,8 @@ export interface FileSystem extends JsonRpcServer { * Check `FileAccess.Constants` for possible values of mode. * It is possible to create a mask consisting of the bitwise `OR` of two or more values (e.g. FileAccess.Constants.W_OK | FileAccess.Constants.R_OK). * If `mode` is not defined, `FileAccess.Constants.F_OK` will be used instead. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.access` instead */ access(uri: string, mode?: number): Promise @@ -150,10 +181,15 @@ export interface FileSystem extends JsonRpcServer { * USE WITH CAUTION: You should always prefer URIs to paths if possible, as they are * portable and platform independent. Paths should only be used in cases you directly * interact with the OS, e.g. when running a command on the shell. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.fsPath` instead */ getFsPath(uri: string): Promise } +/** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileService.access` instead + */ export namespace FileAccess { export namespace Constants { @@ -192,79 +228,10 @@ export interface FileDeleteOptions { moveToTrash?: boolean } -/** - * A callback type, called when we try to save a file but realize it has been - * modified by somebody else since we have opened it. `originalStat` is the - * stat at the moment we opened the file, `currentStat` is the stat at the - * moment we try to save it (after the external modification). The callback - * should return true if we still want to save the file, false otherwise. - */ -export const FileShouldOverwrite = Symbol('FileShouldOverwrite'); -export interface FileShouldOverwrite { - (originalStat: FileStat, currentStat: FileStat): Promise; -} - -export interface FileSystemClient { - - /** - * Tests whether the given file can be overwritten - * in the case if it is out of sync with the given file stat. - */ - shouldOverwrite: FileShouldOverwrite; - - willCreate(uri: string): Promise; - - didCreate(uri: string, failed: boolean): Promise; - - willDelete(uri: string): Promise; - - didDelete(uri: string, failed: boolean): Promise; - - willMove(sourceUri: string, targetUri: string): Promise; - - didMove(sourceUri: string, targetUri: string, failed: boolean): Promise; - -} - -@injectable() -export class DispatchingFileSystemClient implements FileSystemClient { - - readonly clients = new Set(); - - shouldOverwrite(originalStat: FileStat, currentStat: FileStat): Promise { - return Promise.race(Array.from(this.clients, client => - client.shouldOverwrite(originalStat, currentStat)) - ); - } - - async willCreate(uri: string): Promise { - await Promise.all(Array.from(this.clients, client => client.willCreate(uri))); - } - - async didCreate(uri: string, failed: boolean): Promise { - await Promise.all(Array.from(this.clients, client => client.didCreate(uri, failed))); - } - - async willDelete(uri: string): Promise { - await Promise.all(Array.from(this.clients, client => client.willDelete(uri))); - } - - async didDelete(uri: string, failed: boolean): Promise { - await Promise.all(Array.from(this.clients, client => client.didDelete(uri, failed))); - } - - async willMove(sourceUri: string, targetUri: string): Promise { - await Promise.all(Array.from(this.clients, client => client.willMove(sourceUri, targetUri))); - } - - async didMove(sourceUri: string, targetUri: string, failed: boolean): Promise { - await Promise.all(Array.from(this.clients, client => client.didMove(sourceUri, targetUri, failed))); - } - -} - /** * A file resource with meta information. + * + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileStat` from `@theia/filesystem/lib/common/files` instead */ export interface FileStat { @@ -311,6 +278,9 @@ export namespace FileStat { } } +/** + * @deprecated since 1.4.0 - in order to suppot VS Code FS API (https://github.com/eclipse-theia/theia/pull/7908), use `FileOperationError` instead + */ export namespace FileSystemError { export const FileNotFound = ApplicationError.declare(-33000, (uri: string, prefix?: string) => ({ message: `${prefix ? prefix + ' ' : ''}'${uri}' has not been found.`, diff --git a/packages/filesystem/src/common/io.ts b/packages/filesystem/src/common/io.ts new file mode 100644 index 0000000000000..347013e496abf --- /dev/null +++ b/packages/filesystem/src/common/io.ts @@ -0,0 +1,111 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/platform/files/common/io.ts + +/* eslint-disable max-len */ + +import URI from '@theia/core/lib/common/uri'; +import { BinaryBuffer, BinaryBufferWriteableStream, BinaryBufferReadableStream } from '@theia/core/lib/common//buffer'; +import { CancellationToken, cancelled as canceled } from '@theia/core/lib/common/cancellation'; +import { FileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, ensureFileSystemProviderError } from './files'; + +export interface CreateReadStreamOptions extends FileReadStreamOptions { + + /** + * The size of the buffer to use before sending to the stream. + */ + bufferSize: number; +} + +export function createReadStream(provider: FileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: CreateReadStreamOptions, token?: CancellationToken): BinaryBufferReadableStream { + const stream = BinaryBufferWriteableStream.create(); + + // do not await reading but simply return the stream directly since it operates + // via events. finally end the stream and send through the possible error + + doReadFileIntoStream(provider, resource, stream, options, token).then(() => stream.end(), error => stream.end(error)); + + return stream; +} + +async function doReadFileIntoStream(provider: FileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, stream: BinaryBufferWriteableStream, options: CreateReadStreamOptions, token?: CancellationToken): Promise { + + // Check for cancellation + throwIfCancelled(token); + + // open handle through provider + const handle = await provider.open(resource, { create: false }); + + // Check for cancellation + throwIfCancelled(token); + + try { + let bytesRead = 0; + let allowedRemainingBytes = (options && typeof options.length === 'number') ? options.length : undefined; + + let buffer = BinaryBuffer.alloc(Math.min(options.bufferSize, typeof allowedRemainingBytes === 'number' ? allowedRemainingBytes : options.bufferSize)); + + let posInFile = options && typeof options.position === 'number' ? options.position : 0; + let posInBuffer = 0; + do { + // read from source (handle) at current position (pos) into buffer (buffer) at + // buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength). + bytesRead = await provider.read(handle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer); + + posInFile += bytesRead; + posInBuffer += bytesRead; + + if (typeof allowedRemainingBytes === 'number') { + allowedRemainingBytes -= bytesRead; + } + + // when buffer full, create a new one and emit it through stream + if (posInBuffer === buffer.byteLength) { + stream.write(buffer); + + buffer = BinaryBuffer.alloc(Math.min(options.bufferSize, typeof allowedRemainingBytes === 'number' ? allowedRemainingBytes : options.bufferSize)); + + posInBuffer = 0; + } + } while (bytesRead > 0 && (typeof allowedRemainingBytes !== 'number' || allowedRemainingBytes > 0) && throwIfCancelled(token)); + + // wrap up with last buffer (also respect maxBytes if provided) + if (posInBuffer > 0) { + let lastChunkLength = posInBuffer; + if (typeof allowedRemainingBytes === 'number') { + lastChunkLength = Math.min(posInBuffer, allowedRemainingBytes); + } + + stream.write(buffer.slice(0, lastChunkLength)); + } + } catch (error) { + throw ensureFileSystemProviderError(error); + } finally { + await provider.close(handle); + } +} + +function throwIfCancelled(token?: CancellationToken): boolean { + if (token && token.isCancellationRequested) { + throw canceled(); + } + + return true; +} diff --git a/packages/filesystem/src/common/remote-file-system-provider.ts b/packages/filesystem/src/common/remote-file-system-provider.ts new file mode 100644 index 0000000000000..d2617de3e9c2c --- /dev/null +++ b/packages/filesystem/src/common/remote-file-system-provider.ts @@ -0,0 +1,402 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject, postConstruct } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { Emitter } from '@theia/core/lib/common/event'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; +import { + FileWriteOptions, FileOpenOptions, FileChangeType, + FileSystemProviderCapabilities, FileChange, Stat, FileOverwriteOptions, WatchOptions, FileType, FileSystemProvider, FileDeleteOptions, + hasOpenReadWriteCloseCapability, hasFileFolderCopyCapability, hasReadWriteCapability, hasAccessCapability, + FileSystemProviderError, FileSystemProviderErrorCode, FileUpdateOptions, hasUpdateCapability, FileUpdateResult +} from './files'; +import { JsonRpcServer, JsonRpcProxy, JsonRpcProxyFactory } from '@theia/core/lib/common/messaging/proxy-factory'; +import { ApplicationError } from '@theia/core/lib/common/application-error'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import type { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; + +export const remoteFileSystemPath = '/services/remote-filesystem'; + +export const RemoteFileSystemServer = Symbol('RemoteFileSystemServer'); +export interface RemoteFileSystemServer extends JsonRpcServer { + getCapabilities(): Promise + stat(resource: string): Promise; + access(resource: string, mode?: number): Promise; + fsPath(resource: string): Promise; + open(resource: string, opts: FileOpenOptions): Promise; + close(fd: number): Promise; + read(fd: number, pos: number, length: number): Promise<{ bytes: number[]; bytesRead: number; }>; + readFile(resource: string): Promise; + write(fd: number, pos: number, data: number[], offset: number, length: number): Promise; + writeFile(resource: string, content: number[], opts: FileWriteOptions): Promise; + delete(resource: string, opts: FileDeleteOptions): Promise; + mkdir(resource: string): Promise; + readdir(resource: string): Promise<[string, FileType][]>; + rename(source: string, target: string, opts: FileOverwriteOptions): Promise; + copy(source: string, target: string, opts: FileOverwriteOptions): Promise; + watch(watcher: number, resource: string, opts: WatchOptions): Promise; + unwatch(watcher: number): Promise; + updateFile(resource: string, changes: TextDocumentContentChangeEvent[], opts: FileUpdateOptions): Promise; +} + +export interface RemoteFileChange { + readonly type: FileChangeType; + readonly resource: string; +} + +export interface RemoteFileSystemClient { + notifyDidChangeFile(event: { changes: RemoteFileChange[] }): void; + notifyDidChangeCapabilities(capabilities: FileSystemProviderCapabilities): void; +} + +export const RemoteFileSystemProviderError = ApplicationError.declare(-33005, + (message: string, data: { code: FileSystemProviderErrorCode, name: string }, stack: string) => + ({ message, data, stack }) +); + +export class RemoteFileSystemProxyFactory extends JsonRpcProxyFactory { + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected serializeError(e: any): any { + if (e instanceof FileSystemProviderError) { + const { code, name } = e; + return super.serializeError(RemoteFileSystemProviderError(e.message, { code, name }, e.stack)); + } + return super.serializeError(e); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected deserializeError(capturedError: Error, e: any): any { + const error = super.deserializeError(capturedError, e); + if (RemoteFileSystemProviderError.is(error)) { + const fileOperationError = new FileSystemProviderError(error.message, error.data.code); + fileOperationError.name = error.data.name; + fileOperationError.stack = error.stack; + return fileOperationError; + } + return e; + } +} + +@injectable() +export class RemoteFileSystemProvider implements Required, Disposable { + + private readonly onDidChangeFileEmitter = new Emitter(); + readonly onDidChangeFile = this.onDidChangeFileEmitter.event; + + private readonly onDidChangeCapabilitiesEmitter = new Emitter(); + readonly onDidChangeCapabilities = this.onDidChangeCapabilitiesEmitter.event; + + protected readonly toDispose = new DisposableCollection( + this.onDidChangeFileEmitter, + this.onDidChangeCapabilitiesEmitter + ); + + protected watcherSequence = 0; + protected readonly watchOptions = new Map(); + + private _capabilities: FileSystemProviderCapabilities = 0; + get capabilities(): FileSystemProviderCapabilities { return this._capabilities; } + + protected readonly deferredReady = new Deferred(); + get ready(): Promise { + return this.deferredReady.promise; + } + + @inject(RemoteFileSystemServer) + protected readonly server: JsonRpcProxy; + + @postConstruct() + protected init(): void { + this.server.getCapabilities().then(capabilities => { + this._capabilities = capabilities; + this.deferredReady.resolve(undefined); + }, this.deferredReady.reject); + this.server.setClient({ + notifyDidChangeFile: ({ changes }) => { + this.onDidChangeFileEmitter.fire(changes.map(event => ({ resource: new URI(event.resource), type: event.type }))); + }, + notifyDidChangeCapabilities: capabilities => this.setCapabilities(capabilities) + }); + const onInitialized = this.server.onDidOpenConnection(() => { + // skip reconnection on the first connection + onInitialized.dispose(); + this.toDispose.push(this.server.onDidOpenConnection(() => this.reconnect())); + }); + } + + dispose(): void { + this.toDispose.dispose(); + } + + protected setCapabilities(capabilities: FileSystemProviderCapabilities): void { + this._capabilities = capabilities; + this.onDidChangeCapabilitiesEmitter.fire(undefined); + } + + // --- forwarding calls + + stat(resource: URI): Promise { + return this.server.stat(resource.toString()); + } + + access(resource: URI, mode?: number): Promise { + return this.server.access(resource.toString(), mode); + } + + fsPath(resource: URI): Promise { + return this.server.fsPath(resource.toString()); + } + + open(resource: URI, opts: FileOpenOptions): Promise { + return this.server.open(resource.toString(), opts); + } + + close(fd: number): Promise { + return this.server.close(fd); + } + + async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const { bytes, bytesRead } = await this.server.read(fd, pos, length); + + // copy back the data that was written into the buffer on the remote + // side. we need to do this because buffers are not referenced by + // pointer, but only by value and as such cannot be directly written + // to from the other process. + data.set(bytes.slice(0, bytesRead), offset); + + return bytesRead; + } + + async readFile(resource: URI): Promise { + const bytes = await this.server.readFile(resource.toString()); + return Uint8Array.from(bytes); + } + + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + return this.server.write(fd, pos, [...data.values()], offset, length); + } + + writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + return this.server.writeFile(resource.toString(), [...content.values()], opts); + } + + delete(resource: URI, opts: FileDeleteOptions): Promise { + return this.server.delete(resource.toString(), opts); + } + + mkdir(resource: URI): Promise { + return this.server.mkdir(resource.toString()); + } + + readdir(resource: URI): Promise<[string, FileType][]> { + return this.server.readdir(resource.toString()); + } + + rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { + return this.server.rename(resource.toString(), target.toString(), opts); + } + + copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { + return this.server.copy(resource.toString(), target.toString(), opts); + } + + updateFile(resource: URI, changes: TextDocumentContentChangeEvent[], opts: FileUpdateOptions): Promise { + return this.server.updateFile(resource.toString(), changes, opts); + } + + watch(resource: URI, options: WatchOptions): Disposable { + const watcher = this.watcherSequence++; + const uri = resource.toString(); + this.watchOptions.set(watcher, { uri, options }); + this.server.watch(watcher, uri, options); + + const toUnwatch = Disposable.create(() => { + this.watchOptions.delete(watcher); + this.server.unwatch(watcher); + }); + this.toDispose.push(toUnwatch); + return toUnwatch; + } + + protected reconnect(): void { + for (const [watcher, { uri, options }] of this.watchOptions.entries()) { + this.server.watch(watcher, uri, options); + } + } + +} + +/** + * JSON-RPC server exposing a wrapped file system provider remotely. + */ +@injectable() +export class FileSystemProviderServer implements RemoteFileSystemServer { + + private readonly BUFFER_SIZE = 64 * 1024; + + protected readonly toDispose = new DisposableCollection(); + dispose(): void { + this.toDispose.dispose(); + } + + protected client: RemoteFileSystemClient | undefined; + setClient(client: RemoteFileSystemClient | undefined): void { + this.client = client; + } + + @inject(FileSystemProvider) + protected readonly provider: FileSystemProvider & Partial; + + @postConstruct() + protected init(): void { + if (this.provider.dispose) { + this.toDispose.push(Disposable.create(() => this.provider.dispose!())); + } + this.toDispose.push(this.provider.onDidChangeCapabilities(() => { + if (this.client) { + this.client.notifyDidChangeCapabilities(this.provider.capabilities); + } + })); + this.toDispose.push(this.provider.onDidChangeFile(changes => { + if (this.client) { + this.client.notifyDidChangeFile({ + changes: changes.map(({ resource, type }) => ({ resource: resource.toString(), type })) + }); + } + })); + } + + async getCapabilities(): Promise { + return this.provider.capabilities; + } + + stat(resource: string): Promise { + return this.provider.stat(new URI(resource)); + } + + access(resource: string, mode?: number): Promise { + if (hasAccessCapability(this.provider)) { + return this.provider.access(new URI(resource), mode); + } + throw new Error('not supported'); + } + + async fsPath(resource: string): Promise { + if (hasAccessCapability(this.provider)) { + return this.provider.fsPath(new URI(resource)); + } + throw new Error('not supported'); + } + + open(resource: string, opts: FileOpenOptions): Promise { + if (hasOpenReadWriteCloseCapability(this.provider)) { + return this.provider.open(new URI(resource), opts); + } + throw new Error('not supported'); + } + + close(fd: number): Promise { + if (hasOpenReadWriteCloseCapability(this.provider)) { + return this.provider.close(fd); + } + throw new Error('not supported'); + } + + async read(fd: number, pos: number, length: number): Promise<{ bytes: number[]; bytesRead: number; }> { + if (hasOpenReadWriteCloseCapability(this.provider)) { + const buffer = BinaryBuffer.alloc(this.BUFFER_SIZE); + const bytes = buffer.buffer; + const bytesRead = await this.provider.read(fd, pos, bytes, 0, length); + return { bytes: [...bytes.values()], bytesRead }; + } + throw new Error('not supported'); + } + + write(fd: number, pos: number, data: number[], offset: number, length: number): Promise { + if (hasOpenReadWriteCloseCapability(this.provider)) { + return this.provider.write(fd, pos, Uint8Array.from(data), offset, length); + } + throw new Error('not supported'); + } + + async readFile(resource: string): Promise { + if (hasReadWriteCapability(this.provider)) { + const buffer = await this.provider.readFile(new URI(resource)); + return [...buffer.values()]; + } + throw new Error('not supported'); + } + + writeFile(resource: string, content: number[], opts: FileWriteOptions): Promise { + if (hasReadWriteCapability(this.provider)) { + return this.provider.writeFile(new URI(resource), Uint8Array.from(content), opts); + } + throw new Error('not supported'); + } + + delete(resource: string, opts: FileDeleteOptions): Promise { + return this.provider.delete(new URI(resource), opts); + } + + mkdir(resource: string): Promise { + return this.provider.mkdir(new URI(resource)); + } + + readdir(resource: string): Promise<[string, FileType][]> { + return this.provider.readdir(new URI(resource)); + } + + rename(source: string, target: string, opts: FileOverwriteOptions): Promise { + return this.provider.rename(new URI(source), new URI(target), opts); + } + + copy(source: string, target: string, opts: FileOverwriteOptions): Promise { + if (hasFileFolderCopyCapability(this.provider)) { + return this.provider.copy(new URI(source), new URI(target), opts); + } + throw new Error('not supported'); + } + + updateFile(resource: string, changes: TextDocumentContentChangeEvent[], opts: FileUpdateOptions): Promise { + if (hasUpdateCapability(this.provider)) { + return this.provider.updateFile(new URI(resource), changes, opts); + } + throw new Error('not supported'); + } + + protected watchers = new Map(); + + async watch(req: number, resource: string, opts: WatchOptions): Promise { + const watcher = this.provider.watch(new URI(resource), opts); + this.watchers.set(req, watcher); + this.toDispose.push(Disposable.create(() => this.unwatch(req))); + } + + async unwatch(req: number): Promise { + const watcher = this.watchers.get(req); + if (watcher) { + this.watchers.delete(req); + watcher.dispose(); + } + } + +} diff --git a/packages/filesystem/src/common/test/mock-filesystem.ts b/packages/filesystem/src/common/test/mock-filesystem.ts deleted file mode 100644 index 64396c92c3316..0000000000000 --- a/packages/filesystem/src/common/test/mock-filesystem.ts +++ /dev/null @@ -1,107 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2017 Ericsson and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { injectable } from 'inversify'; -import { FileSystem, FileStat, FileSystemClient } from '../filesystem'; - -const mockFileStat = { - uri: '', - lastModification: 0, - isDirectory: true, -}; - -@injectable() -export class MockFilesystem implements FileSystem { - - dispose(): void { } - - getFileStat(uri: string): Promise { - return Promise.resolve(mockFileStat); - } - - exists(uri: string): Promise { - return Promise.resolve(true); - - } - - resolveContent(uri: string, options?: { encoding?: string }): Promise<{ stat: FileStat, content: string }> { - return Promise.resolve({ stat: mockFileStat, content: '' }); - } - - setContent(file: FileStat, content: string, options?: { encoding?: string }): Promise { - return Promise.resolve(mockFileStat); - } - - updateContent(): Promise { - return Promise.resolve(mockFileStat); - } - - move(sourceUri: string, targetUri: string, options?: { overwrite?: boolean }): Promise { - return Promise.resolve(mockFileStat); - } - - copy(sourceUri: string, targetUri: string, options?: { overwrite?: boolean, recursive?: boolean }): Promise { - return Promise.resolve(mockFileStat); - } - - createFile(uri: string, options?: { content?: string, encoding?: string }): Promise { - return Promise.resolve(mockFileStat); - } - - createFolder(uri: string): Promise { - return Promise.resolve(mockFileStat); - } - - touchFile(uri: string): Promise { - return Promise.resolve(mockFileStat); - } - - delete(uri: string, options?: { moveToTrash?: boolean }): Promise { - return Promise.resolve(); - } - - getEncoding(uri: string): Promise { - return Promise.resolve(''); - } - - guessEncoding(uri: string): Promise { - return Promise.resolve(''); - } - - getRoots(): Promise { - return Promise.resolve([mockFileStat]); - } - - getCurrentUserHome(): Promise { - return Promise.resolve(mockFileStat); - } - - async access(uri: string, mode?: number): Promise { - return true; - } - - setClient(client: FileSystemClient): void { - - } - - async getDrives(): Promise { - return []; - } - - async getFsPath(uri: string): Promise { - return undefined; - } -} diff --git a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts index f458b197e1cb8..24970718a1403 100644 --- a/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts +++ b/packages/filesystem/src/electron-browser/file-dialog/electron-file-dialog-service.ts @@ -20,7 +20,7 @@ import URI from '@theia/core/lib/common/uri'; import { isOSX } from '@theia/core/lib/common/os'; import { MaybeArray } from '@theia/core/lib/common/types'; import { MessageService } from '@theia/core/lib/common/message-service'; -import { FileStat } from '../../common'; +import { FileStat } from '../../common/files'; import { FileAccess } from '../../common/filesystem'; import { DefaultFileDialogService, OpenFileDialogProps, SaveFileDialogProps } from '../../browser/file-dialog'; @@ -69,7 +69,7 @@ export class ElectronFileDialogService extends DefaultFileDialogService { } const uri = FileUri.create(filePath); - const exists = await this.fileSystem.exists(uri.toString()); + const exists = await this.fileService.exists(uri); if (!exists) { return uri; } @@ -82,7 +82,7 @@ export class ElectronFileDialogService extends DefaultFileDialogService { protected async canReadWrite(uris: MaybeArray): Promise { for (const uri of Array.isArray(uris) ? uris : [uris]) { - if (!(await this.fileSystem.access(uri.toString(), FileAccess.Constants.R_OK | FileAccess.Constants.W_OK))) { + if (!(await this.fileService.access(uri, FileAccess.Constants.R_OK | FileAccess.Constants.W_OK))) { this.messageService.error(`Cannot access resource at ${uri.path}.`); return false; } diff --git a/packages/filesystem/src/node/disk-file-system-provider.ts b/packages/filesystem/src/node/disk-file-system-provider.ts new file mode 100644 index 0000000000000..7d14ac60ae53b --- /dev/null +++ b/packages/filesystem/src/node/disk-file-system-provider.ts @@ -0,0 +1,870 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/platform/files/node/diskFileSystemProvider.ts + +/* eslint-disable no-null/no-null */ +/* eslint-disable no-shadow */ + +import { injectable, inject, postConstruct } from 'inversify'; +import { basename, dirname, normalize, join } from 'path'; +import { v4 } from 'uuid'; +import * as os from 'os'; +import * as fs from 'fs'; +import { + mkdir, open, close, read, write, fdatasync, Stats, + lstat, stat, readdir, readFile, exists, chmod, + rmdir, unlink, rename, futimes, truncate +} from 'fs'; +import { promisify } from 'util'; +import URI from '@theia/core/lib/common/uri'; +import { Path } from '@theia/core/lib/common/path'; +import { FileUri } from '@theia/core/lib/node/file-uri'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { OS, isWindows } from '@theia/core/lib/common/os'; +import { retry } from '@theia/core/lib/common/promise-util'; +import { + FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability, + FileSystemProviderCapabilities, + Stat, + FileType, + FileWriteOptions, + createFileSystemProviderError, + FileSystemProviderErrorCode, + FileOpenOptions, + FileDeleteOptions, + FileOverwriteOptions, + FileSystemProviderError, + FileChange, + WatchOptions, + FileUpdateOptions, FileUpdateResult +} from '../common/files'; +import { FileSystemWatcherServer } from '../common/filesystem-watcher-protocol'; +import trash = require('trash'); +import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { EncodingService } from '@theia/core/lib/common/encoding-service'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; + +export namespace DiskFileSystemProvider { + export interface StatAndLink { + + // The stats of the file. If the file is a symbolic + // link, the stats will be of that target file and + // not the link itself. + // If the file is a symbolic link pointing to a non + // existing file, the stat will be of the link and + // the `dangling` flag will indicate this. + stat: fs.Stats; + + // Will be provided if the resource is a symbolic link + // on disk. Use the `dangling` flag to find out if it + // points to a resource that does not exist on disk. + symbolicLink?: { dangling: boolean }; + } +} + +@injectable() +export class DiskFileSystemProvider implements Disposable, + FileSystemProviderWithFileReadWriteCapability, + FileSystemProviderWithOpenReadWriteCloseCapability, + FileSystemProviderWithFileFolderCopyCapability { + + private readonly onDidChangeFileEmitter = new Emitter(); + readonly onDidChangeFile = this.onDidChangeFileEmitter.event; + + protected readonly toDispose = new DisposableCollection( + this.onDidChangeFileEmitter + ); + + @inject(FileSystemWatcherServer) + protected readonly watcher: FileSystemWatcherServer; + + @inject(EncodingService) + protected readonly encodingService: EncodingService; + + @postConstruct() + protected init(): void { + this.toDispose.push(this.watcher); + this.watcher.setClient({ + onDidFilesChanged: params => this.onDidChangeFileEmitter.fire(params.changes.map(({ uri, type }) => ({ + resource: new URI(uri), + type + }))) + }); + } + + // #region File Capabilities + + readonly onDidChangeCapabilities = Event.None; + + protected _capabilities: FileSystemProviderCapabilities | undefined; + get capabilities(): FileSystemProviderCapabilities { + if (!this._capabilities) { + this._capabilities = + FileSystemProviderCapabilities.FileReadWrite | + FileSystemProviderCapabilities.FileOpenReadWriteClose | + FileSystemProviderCapabilities.FileFolderCopy | + FileSystemProviderCapabilities.Access | + FileSystemProviderCapabilities.Trash | + FileSystemProviderCapabilities.Update; + + if (OS.type() === OS.Type.Linux) { + this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; + } + } + + return this._capabilities; + } + + // #endregion + + // #region File Metadata Resolving + + async stat(resource: URI): Promise { + try { + const { stat, symbolicLink } = await this.statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly + + return { + type: this.toType(stat, symbolicLink), + ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time + mtime: stat.mtime.getTime(), + size: stat.size + }; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + + async access(resource: URI, mode?: number): Promise { + try { + await promisify(fs.access)(this.toFilePath(resource), mode); + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + + async fsPath(resource: URI): Promise { + return FileUri.fsPath(resource); + } + + protected async statLink(path: string): Promise { + + // First stat the link + let lstats: Stats | undefined; + try { + lstats = await promisify(lstat)(path); + + // Return early if the stat is not a symbolic link at all + if (!lstats.isSymbolicLink()) { + return { stat: lstats }; + } + } catch (error) { + /* ignore - use stat() instead */ + } + + // If the stat is a symbolic link or failed to stat, use fs.stat() + // which for symbolic links will stat the target they point to + try { + const stats = await promisify(stat)(path); + + return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; + } catch (error) { + + // If the link points to a non-existing file we still want + // to return it as result while setting dangling: true flag + if (error.code === 'ENOENT' && lstats) { + return { stat: lstats, symbolicLink: { dangling: true } }; + } + + throw error; + } + } + + async readdir(resource: URI): Promise<[string, FileType][]> { + try { + const children = await promisify(fs.readdir)(this.toFilePath(resource)); + + const result: [string, FileType][] = []; + await Promise.all(children.map(async child => { + try { + const stat = await this.stat(resource.resolve(child)); + result.push([child, stat.type]); + } catch (error) { + console.trace(error); // ignore errors for individual entries that can arise from permission denied + } + })); + + return result; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + + private toType(entry: Stats, symbolicLink?: { dangling: boolean }): FileType { + // Signal file type by checking for file / directory, except: + // - symbolic links pointing to non-existing files are FileType.Unknown + // - files that are neither file nor directory are FileType.Unknown + let type: FileType; + if (symbolicLink?.dangling) { + type = FileType.Unknown; + } else if (entry.isFile()) { + type = FileType.File; + } else if (entry.isDirectory()) { + type = FileType.Directory; + } else { + type = FileType.Unknown; + } + + // Always signal symbolic link as file type additionally + if (symbolicLink) { + type |= FileType.SymbolicLink; + } + + return type; + } + + // #endregion + + // #region File Reading/Writing + + async readFile(resource: URI): Promise { + try { + const filePath = this.toFilePath(resource); + + return await promisify(readFile)(filePath); + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + + async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + let handle: number | undefined = undefined; + try { + const filePath = this.toFilePath(resource); + + // Validate target unless { create: true, overwrite: true } + if (!opts.create || !opts.overwrite) { + const fileExists = await promisify(exists)(filePath); + if (fileExists) { + if (!opts.overwrite) { + throw createFileSystemProviderError('File already exists', FileSystemProviderErrorCode.FileExists); + } + } else { + if (!opts.create) { + throw createFileSystemProviderError('File does not exist', FileSystemProviderErrorCode.FileNotFound); + } + } + } + + // Open + handle = await this.open(resource, { create: true }); + + // Write content at once + await this.write(handle, 0, content, 0, content.byteLength); + } catch (error) { + throw this.toFileSystemProviderError(error); + } finally { + if (typeof handle === 'number') { + await this.close(handle); + } + } + } + + private mapHandleToPos: Map = new Map(); + + private writeHandles: Set = new Set(); + private canFlush: boolean = true; + + async open(resource: URI, opts: FileOpenOptions): Promise { + try { + const filePath = this.toFilePath(resource); + + let flags: string | undefined = undefined; + if (opts.create) { + if (isWindows && await promisify(exists)(filePath)) { + try { + // On Windows and if the file exists, we use a different strategy of saving the file + // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows + // (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams + // (see https://github.com/Microsoft/vscode/issues/6363) + await promisify(truncate)(filePath, 0); + + // After a successful truncate() the flag can be set to 'r+' which will not truncate. + flags = 'r+'; + } catch (error) { + console.trace(error); + } + } + + // we take opts.create as a hint that the file is opened for writing + // as such we use 'w' to truncate an existing or create the + // file otherwise. we do not allow reading. + if (!flags) { + flags = 'w'; + } + } else { + // otherwise we assume the file is opened for reading + // as such we use 'r' to neither truncate, nor create + // the file. + flags = 'r'; + } + + const handle = await promisify(open)(filePath, flags); + + // remember this handle to track file position of the handle + // we init the position to 0 since the file descriptor was + // just created and the position was not moved so far (see + // also http://man7.org/linux/man-pages/man2/open.2.html - + // "The file offset is set to the beginning of the file.") + this.mapHandleToPos.set(handle, 0); + + // remember that this handle was used for writing + if (opts.create) { + this.writeHandles.add(handle); + } + + return handle; + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + + async close(fd: number): Promise { + try { + + // remove this handle from map of positions + this.mapHandleToPos.delete(fd); + + // if a handle is closed that was used for writing, ensure + // to flush the contents to disk if possible. + if (this.writeHandles.delete(fd) && this.canFlush) { + try { + await promisify(fdatasync)(fd); + } catch (error) { + // In some exotic setups it is well possible that node fails to sync + // In that case we disable flushing and log the error to our logger + this.canFlush = false; + console.error(error); + } + } + + return await promisify(close)(fd); + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + + async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const normalizedPos = this.normalizePos(fd, pos); + + let bytesRead: number | null = null; + try { + const result = await promisify(read)(fd, data, offset, length, normalizedPos); + + if (typeof result === 'number') { + bytesRead = result; // node.d.ts fail + } else { + bytesRead = result.bytesRead; + } + + return bytesRead; + } catch (error) { + throw this.toFileSystemProviderError(error); + } finally { + this.updatePos(fd, normalizedPos, bytesRead); + } + } + + private normalizePos(fd: number, pos: number): number | null { + + // when calling fs.read/write we try to avoid passing in the "pos" argument and + // rather prefer to pass in "null" because this avoids an extra seek(pos) + // call that in some cases can even fail (e.g. when opening a file over FTP - + // see https://github.com/microsoft/vscode/issues/73884). + // + // as such, we compare the passed in position argument with our last known + // position for the file descriptor and use "null" if they match. + if (pos === this.mapHandleToPos.get(fd)) { + return null; + } + + return pos; + } + + private updatePos(fd: number, pos: number | null, bytesLength: number | null): void { + const lastKnownPos = this.mapHandleToPos.get(fd); + if (typeof lastKnownPos === 'number') { + + // pos !== null signals that previously a position was used that is + // not null. node.js documentation explains, that in this case + // the internal file pointer is not moving and as such we do not move + // our position pointer. + // + // Docs: "If position is null, data will be read from the current file position, + // and the file position will be updated. If position is an integer, the file position + // will remain unchanged." + if (typeof pos === 'number') { + // do not modify the position + } else if (typeof bytesLength === 'number') { + this.mapHandleToPos.set(fd, lastKnownPos + bytesLength); + } else { + this.mapHandleToPos.delete(fd); + } + } + } + + async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + // we know at this point that the file to write to is truncated and thus empty + // if the write now fails, the file remains empty. as such we really try hard + // to ensure the write succeeds by retrying up to three times. + return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */); + } + + private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const normalizedPos = this.normalizePos(fd, pos); + + let bytesWritten: number | null = null; + try { + const result = await promisify(write)(fd, data, offset, length, normalizedPos); + + if (typeof result === 'number') { + bytesWritten = result; // node.d.ts fail + } else { + bytesWritten = result.bytesWritten; + } + + return bytesWritten; + } catch (error) { + throw this.toFileSystemProviderError(error); + } finally { + this.updatePos(fd, normalizedPos, bytesWritten); + } + } + + // #endregion + + // #region Move/Copy/Delete/Create Folder + + async mkdir(resource: URI): Promise { + try { + await promisify(mkdir)(this.toFilePath(resource)); + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + + async delete(resource: URI, opts: FileDeleteOptions): Promise { + try { + const filePath = this.toFilePath(resource); + + await this.doDelete(filePath, opts); + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + + protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise { + if (!opts.useTrash) { + if (opts.recursive) { + await this.rimraf(filePath); + } else { + await promisify(unlink)(filePath); + } + } else { + await trash(filePath); + } + } + + protected rimraf(path: string): Promise { + if (new Path(path).isRoot) { + throw new Error('rimraf - will refuse to recursively delete root'); + } + return this.rimrafMove(path); + } + + protected async rimrafMove(path: string): Promise { + try { + const pathInTemp = join(os.tmpdir(), v4()); + try { + await promisify(rename)(path, pathInTemp); + } catch (error) { + return this.rimrafUnlink(path); // if rename fails, delete without tmp dir + } + + // Delete but do not return as promise + this.rimrafUnlink(pathInTemp); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + } + + protected async rimrafUnlink(path: string): Promise { + try { + const stat = await promisify(lstat)(path); + + // Folder delete (recursive) - NOT for symbolic links though! + if (stat.isDirectory() && !stat.isSymbolicLink()) { + + // Children + const children = await promisify(readdir)(path); + await Promise.all(children.map(child => this.rimrafUnlink(join(path, child)))); + + // Folder + await promisify(rmdir)(path); + } else { + + // chmod as needed to allow for unlink + const mode = stat.mode; + if (!(mode & 128)) { // 128 === 0200 + await promisify(chmod)(path, mode | 128); + } + + return promisify(unlink)(path); + } + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + } + + async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + const fromFilePath = this.toFilePath(from); + const toFilePath = this.toFilePath(to); + + if (fromFilePath === toFilePath) { + return; // simulate node.js behaviour here and do a no-op if paths match + } + + try { + + // Ensure target does not exist + await this.validateTargetDeleted(from, to, 'move', opts.overwrite); + + // Move + await this.move(fromFilePath, toFilePath); + } catch (error) { + + // rewrite some typical errors that can happen especially around symlinks + // to something the user can better understand + if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') { + error = new Error(`Unable to move '${basename(fromFilePath)}' into '${basename(dirname(toFilePath))}' (${error.toString()}).`); + } + + throw this.toFileSystemProviderError(error); + } + } + + protected async move(source: string, target: string): Promise { + if (source === target) { + return Promise.resolve(); + } + + async function updateMtime(path: string): Promise { + const stat = await promisify(lstat)(path); + if (stat.isDirectory() || stat.isSymbolicLink()) { + return Promise.resolve(); // only for files + } + + const fd = await promisify(open)(path, 'a'); + try { + await promisify(futimes)(fd, stat.atime, new Date()); + } catch (error) { + // ignore + } + + return promisify(close)(fd); + } + + try { + await promisify(rename)(source, target); + await updateMtime(target); + } catch (error) { + + // In two cases we fallback to classic copy and delete: + // + // 1.) The EXDEV error indicates that source and target are on different devices + // In this case, fallback to using a copy() operation as there is no way to + // rename() between different devices. + // + // 2.) The user tries to rename a file/folder that ends with a dot. This is not + // really possible to move then, at least on UNC devices. + if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || source.endsWith('.')) { + await this.doCopy(source, target); + await this.rimraf(source); + await updateMtime(target); + } else { + throw error; + } + } + } + + async copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + const fromFilePath = this.toFilePath(from); + const toFilePath = this.toFilePath(to); + + if (fromFilePath === toFilePath) { + return; // simulate node.js behaviour here and do a no-op if paths match + } + + try { + + // Ensure target does not exist + await this.validateTargetDeleted(from, to, 'copy', opts.overwrite); + + // Copy + await this.doCopy(fromFilePath, toFilePath); + } catch (error) { + + // rewrite some typical errors that can happen especially around symlinks + // to something the user can better understand + if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') { + error = new Error(`Unable to copy '${basename(fromFilePath)}' into '${basename(dirname(toFilePath))}' (${error.toString()}).`); + } + + throw this.toFileSystemProviderError(error); + } + } + + private async validateTargetDeleted(from: URI, to: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise { + const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + + const fromFilePath = this.toFilePath(from); + const toFilePath = this.toFilePath(to); + + let isSameResourceWithDifferentPathCase = false; + if (!isPathCaseSensitive) { + isSameResourceWithDifferentPathCase = fromFilePath.toLowerCase() === toFilePath.toLowerCase(); + } + + if (isSameResourceWithDifferentPathCase && mode === 'copy') { + throw createFileSystemProviderError("'File cannot be copied to same path with different path case", FileSystemProviderErrorCode.FileExists); + } + + // handle existing target (unless this is a case change) + if (!isSameResourceWithDifferentPathCase && await promisify(exists)(toFilePath)) { + if (!overwrite) { + throw createFileSystemProviderError('File at target already exists', FileSystemProviderErrorCode.FileExists); + } + + // Delete target + await this.delete(to, { recursive: true, useTrash: false }); + } + } + + protected async doCopy(source: string, target: string, copiedSourcesIn?: { [path: string]: boolean }): Promise { + const copiedSources = copiedSourcesIn ? copiedSourcesIn : Object.create(null); + + const fileStat = await promisify(stat)(source); + if (!fileStat.isDirectory()) { + return this.doCopyFile(source, target, fileStat.mode & 511); + } + + if (copiedSources[source]) { + return Promise.resolve(); // escape when there are cycles (can happen with symlinks) + } + + copiedSources[source] = true; // remember as copied + + // Create folder + this.mkdirp(target, fileStat.mode & 511); + + // Copy each file recursively + const files = await promisify(readdir)(source); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + await this.doCopy(join(source, file), join(target, file), copiedSources); + } + } + + protected async mkdirp(path: string, mode?: number): Promise { + const mkdir = async () => { + try { + await promisify(fs.mkdir)(path, mode); + } catch (error) { + + // ENOENT: a parent folder does not exist yet + if (error.code === 'ENOENT') { + throw error; + } + + // Any other error: check if folder exists and + // return normally in that case if its a folder + let targetIsFile = false; + try { + const fileStat = await promisify(fs.stat)(path); + targetIsFile = !fileStat.isDirectory(); + } catch (statError) { + throw error; // rethrow original error if stat fails + } + + if (targetIsFile) { + throw new Error(`'${path}' exists and is not a directory.`); + } + } + }; + + // stop at root + if (path === dirname(path)) { + return; + } + + try { + await mkdir(); + } catch (error) { + + // ENOENT: a parent folder does not exist yet, continue + // to create the parent folder and then try again. + if (error.code === 'ENOENT') { + await this.mkdirp(dirname(path), mode); + + return mkdir(); + } + + // Any other error + throw error; + } + } + + protected doCopyFile(source: string, target: string, mode: number): Promise { + return new Promise((resolve, reject) => { + const reader = fs.createReadStream(source); + const writer = fs.createWriteStream(target, { mode }); + + let finished = false; + const finish = (error?: Error) => { + if (!finished) { + finished = true; + + // in error cases, pass to callback + if (error) { + return reject(error); + } + + // we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104 + fs.chmod(target, mode, error => error ? reject(error) : resolve()); + } + }; + + // handle errors properly + reader.once('error', error => finish(error)); + writer.once('error', error => finish(error)); + + // we are done (underlying fd has been closed) + writer.once('close', () => finish()); + + // start piping + reader.pipe(writer); + }); + } + + // #endregion + + // #region File Watching + + watch(resource: URI, opts: WatchOptions): Disposable { + const toUnwatch = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ })); + this.watcher.watchFileChanges(resource.toString(), { + ignored: opts.excludes + }).then(watcher => { + if (toUnwatch.disposed) { + this.watcher.unwatchFileChanges(watcher); + } else { + toUnwatch.push(Disposable.create(() => this.watcher.unwatchFileChanges(watcher))); + } + }); + this.toDispose.push(toUnwatch); + return toUnwatch; + } + + // #endregion + + async updateFile(resource: URI, changes: TextDocumentContentChangeEvent[], opts: FileUpdateOptions): Promise { + try { + const content = await this.readFile(resource); + const decoded = this.encodingService.decode(BinaryBuffer.wrap(content), opts.readEncoding); + const newContent = TextDocument.update(TextDocument.create('', '', 1, decoded), changes, 2).getText(); + const encoding = await this.encodingService.toResourceEncoding(opts.writeEncoding, { + overwriteEncoding: opts.overwriteEncoding, + read: async length => { + const fd = await this.open(resource, { create: false }); + try { + const data = new Uint8Array(length); + await this.read(fd, 0, data, 0, length); + return data; + } finally { + await this.close(fd); + } + } + }); + const encoded = this.encodingService.encode(newContent, encoding); + await this.writeFile(resource, encoded.buffer, { create: false, overwrite: true }); + const stat = await this.stat(resource); + return Object.assign(stat, { encoding: encoding.encoding }); + } catch (error) { + throw this.toFileSystemProviderError(error); + } + } + + // #region Helpers + + protected toFilePath(resource: URI): string { + return normalize(FileUri.fsPath(resource)); + } + + private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError { + if (error instanceof FileSystemProviderError) { + return error; // avoid double conversion + } + + let code: FileSystemProviderErrorCode; + switch (error.code) { + case 'ENOENT': + code = FileSystemProviderErrorCode.FileNotFound; + break; + case 'EISDIR': + code = FileSystemProviderErrorCode.FileIsADirectory; + break; + case 'ENOTDIR': + code = FileSystemProviderErrorCode.FileNotADirectory; + break; + case 'EEXIST': + code = FileSystemProviderErrorCode.FileExists; + break; + case 'EPERM': + case 'EACCES': + code = FileSystemProviderErrorCode.NoPermissions; + break; + default: + code = FileSystemProviderErrorCode.Unknown; + } + + return createFileSystemProviderError(error, code); + } + + // #endregion + + dispose(): void { + this.toDispose.dispose(); + } +} diff --git a/packages/filesystem/src/node/download/directory-archiver.ts b/packages/filesystem/src/node/download/directory-archiver.ts index c1ca2701bdcad..05246b99de0d0 100644 --- a/packages/filesystem/src/node/download/directory-archiver.ts +++ b/packages/filesystem/src/node/download/directory-archiver.ts @@ -14,18 +14,15 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; +import { injectable } from 'inversify'; import * as fs from 'fs-extra'; import { pack } from 'tar-fs'; import URI from '@theia/core/lib/common/uri'; -import { FileSystem } from '../../common/filesystem'; +import { FileUri } from '@theia/core/lib/node/file-uri'; @injectable() export class DirectoryArchiver { - @inject(FileSystem) - protected readonly fileSystem: FileSystem; - async archive(inputPath: string, outputPath: string, entries?: string[]): Promise { return new Promise(async (resolve, reject) => { pack(inputPath, { entries }).pipe(fs.createWriteStream(outputPath)).on('finish', () => resolve()).on('error', e => reject(e)); @@ -98,11 +95,12 @@ export class DirectoryArchiver { } protected async isDir(uri: URI): Promise { - const stat = await this.fileSystem.getFileStat(uri.toString()); - if (!stat) { - throw new Error(`File does not exist under: ${uri}.`); + try { + const stat = await fs.stat(FileUri.fsPath(uri)); + return stat.isDirectory(); + } catch { + return false; } - return stat.isDirectory; } protected equal(left: URI | URI[], right: URI | URI[]): boolean { diff --git a/packages/filesystem/src/node/download/file-download-handler.ts b/packages/filesystem/src/node/download/file-download-handler.ts index d97d3b2efa60e..cad45f6f432ac 100644 --- a/packages/filesystem/src/node/download/file-download-handler.ts +++ b/packages/filesystem/src/node/download/file-download-handler.ts @@ -25,7 +25,6 @@ import URI from '@theia/core/lib/common/uri'; import { isEmpty } from '@theia/core/lib/common/objects'; import { ILogger } from '@theia/core/lib/common/logger'; import { FileUri } from '@theia/core/lib/node/file-uri'; -import { FileSystem } from '../../common/filesystem'; import { DirectoryArchiver } from './directory-archiver'; import { FileDownloadData } from '../../common/download/file-download-data'; import { FileDownloadCache, DownloadStorageItem } from './file-download-cache'; @@ -43,9 +42,6 @@ export abstract class FileDownloadHandler { @inject(ILogger) protected readonly logger: ILogger; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; - @inject(DirectoryArchiver) protected readonly directoryArchiver: DirectoryArchiver; @@ -215,16 +211,19 @@ export class SingleFileDownloadHandler extends FileDownloadHandler { return; } const uri = new URI(query.uri).toString(true); - const stat = await this.fileSystem.getFileStat(uri); - if (stat === undefined) { + const filePath = FileUri.fsPath(uri); + + let stat: fs.Stats; + try { + stat = await fs.stat(filePath); + } catch { this.handleError(response, `The file does not exist. URI: ${uri}.`, NOT_FOUND); return; } try { const downloadId = v4(); - const filePath = FileUri.fsPath(uri); const options: PrepareDownloadOptions = { filePath, downloadId, remove: false }; - if (!stat.isDirectory) { + if (!stat.isDirectory()) { await this.prepareDownload(request, response, options); } else { const outputRootPath = await this.createTempDir(downloadId); @@ -264,8 +263,9 @@ export class MultiFileDownloadHandler extends FileDownloadHandler { return; } for (const uri of body.uris) { - const stat = await this.fileSystem.getFileStat(uri); - if (stat === undefined) { + try { + await fs.access(FileUri.fsPath(uri)); + } catch { this.handleError(response, `The file does not exist. URI: ${uri}.`, NOT_FOUND); return; } diff --git a/packages/filesystem/src/node/encoding-util.ts b/packages/filesystem/src/node/encoding-util.ts deleted file mode 100644 index 75ee756046945..0000000000000 --- a/packages/filesystem/src/node/encoding-util.ts +++ /dev/null @@ -1,54 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2019 Xuye Cai and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -// copied from vscode: https://github.com/Microsoft/vscode/blob/master/src/vs/base/node/encoding.ts -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -const jschardet = require('jschardet'); -const MINIMUM_THRESHOLD = 0.2; -jschardet.Constants.MINIMUM_THRESHOLD = MINIMUM_THRESHOLD; - -export namespace EncodingUtil { - const IGNORE_ENCODINGS = ['ascii', 'utf-8', 'utf-16', 'utf-32']; - export async function guessEncodingByBuffer(buffer: Buffer): Promise { - const guessed = jschardet.detect(buffer); - if (!guessed || !guessed.encoding) { - return undefined; - } - const enc = guessed.encoding.toLowerCase(); - // Ignore encodings that cannot guess correctly - // (http://chardet.readthedocs.io/en/latest/supported-encodings.html) - if (0 <= IGNORE_ENCODINGS.indexOf(enc)) { - return undefined; - } - return toIconvLiteEncoding(guessed.encoding); - } - - function toIconvLiteEncoding(encodingName: string): string { - const normalizedEncodingName = encodingName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); - const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName]; - - return mapped || normalizedEncodingName; - } - - const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { - 'ibm866': 'cp866', - 'big5': 'cp950' - }; -} diff --git a/packages/filesystem/src/node/file-change-collection.spec.ts b/packages/filesystem/src/node/file-change-collection.spec.ts index 41ece2c9ce9c6..9e608ed09e9bf 100644 --- a/packages/filesystem/src/node/file-change-collection.spec.ts +++ b/packages/filesystem/src/node/file-change-collection.spec.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import { FileUri } from '@theia/core/lib/node/file-uri'; import { FileChangeCollection } from './file-change-collection'; -import { FileChangeType } from '../common/filesystem-watcher-protocol'; +import { FileChangeType } from '../common/files'; describe('FileChangeCollection', () => { @@ -91,16 +91,20 @@ describe('FileChangeCollection', () => { expected: FileChangeType[] | FileChangeType }): void { const expectedTypes = Array.isArray(expected) ? expected : [expected]; - const expectation = expectedTypes.map(type => FileChangeType[type]).join(' + '); - it(`${changes.map(type => FileChangeType[type]).join(' + ')} => ${expectation}`, () => { + const expectation = expectedTypes.map(type => typeAsString(type)).join(' + '); + it(`${changes.map(type => typeAsString(type)).join(' + ')} => ${expectation}`, () => { const collection = new FileChangeCollection(); const uri = FileUri.create('/root/foo/bar.txt').toString(); for (const type of changes) { collection.push({ uri, type }); } - const actual = collection.values().map(({ type }) => FileChangeType[type]).join(' + '); + const actual = collection.values().map(({ type }) => typeAsString(type)).join(' + '); assert.equal(expectation, actual); }); } + function typeAsString(type: FileChangeType): string { + return type === FileChangeType.UPDATED ? 'UPDATED' : type === FileChangeType.ADDED ? 'ADDED' : 'DELETED'; + } + }); diff --git a/packages/filesystem/src/node/filesystem-backend-module.ts b/packages/filesystem/src/node/filesystem-backend-module.ts index f9651b1a5ce61..574ed49a513b2 100644 --- a/packages/filesystem/src/node/filesystem-backend-module.ts +++ b/packages/filesystem/src/node/filesystem-backend-module.ts @@ -16,29 +16,21 @@ import { ContainerModule, interfaces } from 'inversify'; import { ConnectionHandler, JsonRpcConnectionHandler, ILogger } from '@theia/core/lib/common'; -import { FileSystemNode } from './node-filesystem'; -import { FileSystem, FileSystemClient, fileSystemPath, DispatchingFileSystemClient } from '../common'; -import { FileSystemWatcherServer, FileSystemWatcherClient, fileSystemWatcherPath } from '../common/filesystem-watcher-protocol'; +import { FileSystemWatcherServer } from '../common/filesystem-watcher-protocol'; import { FileSystemWatcherServerClient } from './filesystem-watcher-client'; import { NsfwFileSystemWatcherServer } from './nsfw-watcher/nsfw-filesystem-watcher'; import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service'; import { NodeFileUploadService } from './node-file-upload-service'; import { NsfwOptions } from './nsfw-watcher/nsfw-options'; +import { DiskFileSystemProvider } from './disk-file-system-provider'; +import { + remoteFileSystemPath, RemoteFileSystemServer, RemoteFileSystemClient, FileSystemProviderServer, RemoteFileSystemProxyFactory +} from '../common/remote-file-system-provider'; +import { FileSystemProvider } from '../common/files'; +import { EncodingService } from '@theia/core/lib/common/encoding-service'; const SINGLE_THREADED = process.argv.indexOf('--no-cluster') !== -1; -export function bindFileSystem(bind: interfaces.Bind, props?: { - onFileSystemActivation: (context: interfaces.Context, fs: FileSystem) => void -}): void { - bind(FileSystemNode).toSelf().inSingletonScope().onActivation((context, fs) => { - if (props && props.onFileSystemActivation) { - props.onFileSystemActivation(context, fs); - } - return fs; - }); - bind(FileSystem).toService(FileSystemNode); -} - export function bindFileSystemWatcherServer(bind: interfaces.Bind, { singleThreaded }: { singleThreaded: boolean } = { singleThreaded: SINGLE_THREADED }): void { bind(NsfwOptions).toConstantValue({}); @@ -59,32 +51,19 @@ export function bindFileSystemWatcherServer(bind: interfaces.Bind, { singleThrea } export default new ContainerModule(bind => { - bind(DispatchingFileSystemClient).toSelf().inSingletonScope(); - bindFileSystem(bind, { - onFileSystemActivation: ({ container }, fs) => { - fs.setClient(container.get(DispatchingFileSystemClient)); - fs.setClient = () => { - throw new Error('use DispatchingFileSystemClient'); - }; - } - }); - bind(ConnectionHandler).toDynamicValue(({ container }) => - new JsonRpcConnectionHandler(fileSystemPath, client => { - const dispatching = container.get(DispatchingFileSystemClient); - dispatching.clients.add(client); - client.onDidCloseConnection(() => dispatching.clients.delete(client)); - return container.get(FileSystem); - }) - ).inSingletonScope(); - + bind(EncodingService).toSelf().inSingletonScope(); bindFileSystemWatcherServer(bind); + bind(DiskFileSystemProvider).toSelf(); + bind(FileSystemProvider).toService(DiskFileSystemProvider); + bind(FileSystemProviderServer).toSelf(); + bind(RemoteFileSystemServer).toService(FileSystemProviderServer); bind(ConnectionHandler).toDynamicValue(ctx => - new JsonRpcConnectionHandler(fileSystemWatcherPath, client => { - const server = ctx.container.get(FileSystemWatcherServer); + new JsonRpcConnectionHandler(remoteFileSystemPath, client => { + const server = ctx.container.get(RemoteFileSystemServer); server.setClient(client); client.onDidCloseConnection(() => server.dispose()); return server; - }) + }, RemoteFileSystemProxyFactory) ).inSingletonScope(); bind(NodeFileUploadService).toSelf().inSingletonScope(); diff --git a/packages/filesystem/src/node/node-filesystem.spec.ts b/packages/filesystem/src/node/node-filesystem.spec.ts deleted file mode 100644 index 6414463383318..0000000000000 --- a/packages/filesystem/src/node/node-filesystem.spec.ts +++ /dev/null @@ -1,818 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2017 TypeFox and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import * as os from 'os'; -import * as temp from 'temp'; -import * as chai from 'chai'; -import * as fs from 'fs-extra'; -import URI from '@theia/core/lib/common/uri'; -import { FileUri } from '@theia/core/lib/node'; -import { isWindows } from '@theia/core/lib/common/os'; -import { FileSystem } from '../common/filesystem'; -import { FileSystemNode } from './node-filesystem'; -import { expectThrowsAsync } from '@theia/core/lib/common/test/expect'; - -/* eslint-disable no-unused-expressions */ - -const expect = chai.expect; -const track = temp.track(); - -describe('NodeFileSystem', function (): void { - - let root: URI; - let fileSystem: FileSystem; - - this.timeout(10000); - - beforeEach(() => { - root = FileUri.create(fs.realpathSync(temp.mkdirSync('node-fs-root'))); - fileSystem = createFileSystem(); - }); - - afterEach(async () => { - track.cleanupSync(); - }); - - describe('01 #getFileStat', () => { - - it('Should return undefined if not file exists under the given URI.', async () => { - const uri = root.resolve('foo.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - const fileStat = await fileSystem.getFileStat(uri.toString()); - expect(fileStat).to.be.undefined; - }); - - it('Should return a proper result for a file.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo'); - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - - const stat = await fileSystem.getFileStat(uri.toString()); - expect(stat).to.not.be.undefined; - expect(stat!.isDirectory).to.be.false; - expect(stat!.uri).to.eq(uri.toString()); - }); - - it('Should return a proper result for a directory.', async () => { - const uri_1 = root.resolve('foo.txt'); - const uri_2 = root.resolve('bar.txt'); - fs.writeFileSync(FileUri.fsPath(uri_1), 'foo'); - fs.writeFileSync(FileUri.fsPath(uri_2), 'bar'); - expect(fs.statSync(FileUri.fsPath(uri_1)).isFile()).to.be.true; - expect(fs.statSync(FileUri.fsPath(uri_2)).isFile()).to.be.true; - - const stat = await fileSystem.getFileStat(root.toString()); - expect(stat).to.not.be.undefined; - expect(stat!.children!.length).to.equal(2); - - }); - - }); - - describe('02 #resolveContent', () => { - - it('Should be rejected with an error when trying to resolve the content of a non-existing file.', async () => { - const uri = root.resolve('foo.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - await expectThrowsAsync(fileSystem.resolveContent(uri.toString()), Error); - }); - - it('Should be rejected with an error when trying to resolve the content of a directory.', async () => { - const uri = root.resolve('foo'); - fs.mkdirSync(FileUri.fsPath(uri)); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.true; - expect(fs.statSync(FileUri.fsPath(uri)).isDirectory()).to.be.true; - - await expectThrowsAsync(fileSystem.resolveContent(uri.toString()), Error); - }); - - it('Should be rejected with an error if the desired encoding cannot be handled.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo', { encoding: 'utf8' }); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.true; - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(uri), { encoding: 'utf8' })).to.be.equal('foo'); - - await expectThrowsAsync(fileSystem.resolveContent(uri.toString(), { encoding: 'unknownEncoding' }), Error); - }); - - it('Should be return with the content for an existing file.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo', { encoding: 'utf8' }); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.true; - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(uri), { encoding: 'utf8' })) - .to.be.equal('foo'); - - const content = await fileSystem.resolveContent(uri.toString()); - expect(content).to.have.property('content') - .that.is.equal('foo'); - }); - - it('Should be return with the stat object for an existing file.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo', { encoding: 'utf8' }); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.true; - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(uri), { encoding: 'utf8' })) - .to.be.equal('foo'); - - const content = await fileSystem.resolveContent(uri.toString()); - expect(content).to.be.an('object'); - expect(content).to.have.property('stat'); - expect(content).to.have.property('stat') - .that.has.property('uri') - .that.is.equal(uri.toString()); - expect(content).to.have.property('stat') - .that.has.property('size') - .that.is.greaterThan(1); - expect(content).to.have.property('stat') - .that.has.property('lastModification') - .that.is.greaterThan(1); - expect(content).to.have.property('stat') - .that.has.property('isDirectory') - .that.is.false; - expect(content).to.have.property('stat') - .that.not.have.property('children'); - }); - - }); - - describe('03 #setContent', () => { - - it('Should be rejected with an error when trying to set the content of a non-existing file.', async () => { - const uri = root.resolve('foo.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - const stat = { - uri: uri.toString(), - lastModification: new Date().getTime(), - isDirectory: false - }; - - await expectThrowsAsync(fileSystem.setContent(stat, 'foo'), Error); - }); - - it('Should be rejected with an error when trying to set the content of a directory.', async () => { - const uri = root.resolve('foo'); - fs.mkdirSync(FileUri.fsPath(uri)); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.true; - expect(fs.statSync(FileUri.fsPath(uri)).isDirectory()).to.be.true; - - const stat = await fileSystem.getFileStat(uri.toString()); - expect(stat).to.not.be.undefined; - await expectThrowsAsync(fileSystem.setContent(stat!, 'foo'), Error); - }); - - it('Should be rejected with an error when trying to set the content of a file which is out-of-sync.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo', { encoding: 'utf8' }); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.true; - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(uri), { encoding: 'utf8' })) - .to.be.equal('foo'); - - const stat = await fileSystem.getFileStat(uri.toString()); - // Make sure current file stat is out-of-sync. - // Here the content is modified in the way that file sizes will differ. - fs.writeFileSync(FileUri.fsPath(uri), 'longer', { encoding: 'utf8' }); - expect(fs.readFileSync(FileUri.fsPath(uri), { encoding: 'utf8' })) - .to.be.equal('longer'); - expect(stat).to.not.be.undefined; - await expectThrowsAsync(fileSystem.setContent(stat!, 'baz'), Error); - }); - - it('Should be rejected with an error when trying to set the content when the desired encoding cannot be handled.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo', { encoding: 'utf8' }); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.true; - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(uri), { encoding: 'utf8' })).to.be.equal('foo'); - - const stat = await fileSystem.getFileStat(uri.toString()); - expect(stat).to.not.be.undefined; - await expectThrowsAsync(fileSystem.setContent(stat!, 'baz', { encoding: 'unknownEncoding' }), Error); - - }); - - it('Should return with a stat representing the latest state of the successfully modified file.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo', { encoding: 'utf8' }); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.true; - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(uri), { encoding: 'utf8' })).to.be.equal('foo'); - - const currentStat = await fileSystem.getFileStat(uri.toString()); - expect(currentStat).to.not.be.undefined; - - await fileSystem.setContent(currentStat!, 'baz'); - expect(fs.readFileSync(FileUri.fsPath(uri), { encoding: 'utf8' })) - .to.be.equal('baz'); - - }); - - }); - - describe('04 #move', () => { - - it('Should be rejected with an error if no file exists under the source location.', async () => { - const sourceUri = root.resolve('foo.txt'); - const targetUri = root.resolve('bar.txt'); - expect(fs.existsSync(FileUri.fsPath(sourceUri))).to.be.false; - - await expectThrowsAsync(fileSystem.move(sourceUri.toString(), targetUri.toString()), Error); - }); - - it("Should be rejected with an error if target exists and overwrite is not set to \'true\'.", async () => { - const sourceUri = root.resolve('foo.txt'); - const targetUri = root.resolve('bar.txt'); - fs.writeFileSync(FileUri.fsPath(sourceUri), 'foo'); - fs.writeFileSync(FileUri.fsPath(targetUri), 'bar'); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isFile()).to.be.true; - expect(fs.statSync(FileUri.fsPath(targetUri)).isFile()).to.be.true; - - await expectThrowsAsync(fileSystem.move(sourceUri.toString(), targetUri.toString()), Error); - }); - - it('Moving a file to an empty directory. Should be rejected with an error because files cannot be moved to an existing directory locations.', async () => { - const sourceUri = root.resolve('foo.txt'); - const targetUri = root.resolve('bar'); - fs.writeFileSync(FileUri.fsPath(sourceUri), 'foo'); - fs.mkdirSync(FileUri.fsPath(targetUri)); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(sourceUri), 'utf8')).to.be.equal('foo'); - expect(fs.statSync(FileUri.fsPath(targetUri)).isDirectory()).to.be.true; - expect(fs.readdirSync(FileUri.fsPath(targetUri))).to.be.empty; - - await expectThrowsAsync(fileSystem.move(sourceUri.toString(), targetUri.toString(), { overwrite: true }), Error); - }); - - it('Moving a file to a non-empty directory. Should be rejected with and error because files cannot be moved to an existing directory locations.', async () => { - const sourceUri = root.resolve('foo.txt'); - const targetUri = root.resolve('bar'); - const targetFileUri_01 = targetUri.resolve('bar_01.txt'); - const targetFileUri_02 = targetUri.resolve('bar_02.txt'); - fs.writeFileSync(FileUri.fsPath(sourceUri), 'foo'); - fs.mkdirSync(FileUri.fsPath(targetUri)); - fs.writeFileSync(FileUri.fsPath(targetFileUri_01), 'bar_01'); - fs.writeFileSync(FileUri.fsPath(targetFileUri_02), 'bar_02'); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(sourceUri), 'utf8')).to.be.equal('foo'); - expect(fs.statSync(FileUri.fsPath(targetUri)).isDirectory()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(targetFileUri_01), 'utf8')).to.be.equal('bar_01'); - expect(fs.readFileSync(FileUri.fsPath(targetFileUri_02), 'utf8')).to.be.equal('bar_02'); - expect(fs.readdirSync(FileUri.fsPath(targetUri))).to.include('bar_01.txt').and.to.include('bar_02.txt'); - - await expectThrowsAsync(fileSystem.move(sourceUri.toString(), targetUri.toString(), { overwrite: true }), Error); - }); - - it('Moving an empty directory to file. Should be rejected with an error because directories and cannot be moved to existing file locations.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('bar.txt'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - fs.writeFileSync(FileUri.fsPath(targetUri), 'bar'); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.statSync(FileUri.fsPath(targetUri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(targetUri), 'utf8')).to.be.equal('bar'); - expect(fs.readdirSync(FileUri.fsPath(sourceUri))).to.be.empty; - - await expectThrowsAsync(fileSystem.move(sourceUri.toString(), targetUri.toString(), { overwrite: true }), Error); - }); - - it('Moving a non-empty directory to file. Should be rejected with an error because directories cannot be moved to existing file locations.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('bar.txt'); - const sourceFileUri_01 = sourceUri.resolve('foo_01.txt'); - const sourceFileUri_02 = sourceUri.resolve('foo_02.txt'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - fs.writeFileSync(FileUri.fsPath(targetUri), 'bar'); - fs.writeFileSync(FileUri.fsPath(sourceFileUri_01), 'foo_01'); - fs.writeFileSync(FileUri.fsPath(sourceFileUri_02), 'foo_02'); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.statSync(FileUri.fsPath(targetUri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(targetUri), 'utf8')).to.be.equal('bar'); - expect(fs.readdirSync(FileUri.fsPath(sourceUri))).to.include('foo_01.txt').and.to.include('foo_02.txt'); - - await expectThrowsAsync(fileSystem.move(sourceUri.toString(), targetUri.toString(), { overwrite: true }), Error); - }); - - it('Moving file to file. Should overwrite the target file content and delete the source file.', async () => { - const sourceUri = root.resolve('foo.txt'); - const targetUri = root.resolve('bar.txt'); - fs.writeFileSync(FileUri.fsPath(sourceUri), 'foo'); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isFile()).to.be.true; - expect(fs.existsSync(FileUri.fsPath(targetUri))).to.be.false; - - const stat = await fileSystem.move(sourceUri.toString(), targetUri.toString(), { overwrite: true }); - expect(stat).is.an('object') - .and.has.property('uri') - .that.equals(targetUri.toString()); - expect(fs.existsSync(FileUri.fsPath(sourceUri))).to.be.false; - expect(fs.statSync(FileUri.fsPath(targetUri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(targetUri), 'utf8')) - .to.be.equal('foo'); - }); - - it('Moving an empty directory to an empty directory. Should remove the source directory.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('bar'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - fs.mkdirSync(FileUri.fsPath(targetUri)); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.statSync(FileUri.fsPath(targetUri)).isDirectory()).to.be.true; - expect(fs.readdirSync(FileUri.fsPath(sourceUri))).to.be.empty; - expect(fs.readdirSync(FileUri.fsPath(targetUri))).to.be.empty; - - const stat = await fileSystem.move(sourceUri.toString(), targetUri.toString(), { overwrite: true }); - expect(stat).is.an('object') - .and.has.property('uri') - .that.equals(targetUri.toString()); - expect(fs.existsSync(FileUri.fsPath(sourceUri))).to.be.false; - expect(fs.statSync(FileUri.fsPath(targetUri)).isDirectory()).to.be.true; - expect(fs.readdirSync(FileUri.fsPath(targetUri))).to.be.empty; - }); - - it('Moving an empty directory to a non-empty directory. Should be rejected because the target folder is not empty.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('bar'); - const targetFileUri_01 = targetUri.resolve('bar_01.txt'); - const targetFileUri_02 = targetUri.resolve('bar_02.txt'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - fs.mkdirSync(FileUri.fsPath(targetUri)); - fs.writeFileSync(FileUri.fsPath(targetFileUri_01), 'bar_01'); - fs.writeFileSync(FileUri.fsPath(targetFileUri_02), 'bar_02'); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.statSync(FileUri.fsPath(targetUri)).isDirectory()).to.be.true; - expect(fs.readdirSync(FileUri.fsPath(sourceUri))).to.be.empty; - expect(fs.readFileSync(FileUri.fsPath(targetFileUri_01), 'utf8')).to.be.equal('bar_01'); - expect(fs.readFileSync(FileUri.fsPath(targetFileUri_02), 'utf8')).to.be.equal('bar_02'); - expect(fs.readdirSync(FileUri.fsPath(targetUri))).to.include('bar_01.txt').and.to.include('bar_02.txt'); - - await expectThrowsAsync(fileSystem.move(sourceUri.toString(), targetUri.toString(), { overwrite: true }), Error); - }); - - it('Moving a non-empty directory to an empty directory. Source folder and its content should be moved to the target location.', async function (): Promise { - if (isWindows) { - // https://github.com/eclipse-theia/theia/issues/2088 - this.skip(); - return; - } - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('bar'); - const sourceFileUri_01 = sourceUri.resolve('foo_01.txt'); - const sourceFileUri_02 = sourceUri.resolve('foo_02.txt'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - fs.mkdirSync(FileUri.fsPath(targetUri)); - fs.writeFileSync(FileUri.fsPath(sourceFileUri_01), 'foo_01'); - fs.writeFileSync(FileUri.fsPath(sourceFileUri_02), 'foo_02'); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.statSync(FileUri.fsPath(targetUri)).isDirectory()).to.be.true; - expect(fs.readdirSync(FileUri.fsPath(targetUri))).to.be.empty; - expect(fs.readdirSync(FileUri.fsPath(sourceUri))).to.include('foo_01.txt').and.to.include('foo_02.txt'); - expect(fs.readFileSync(FileUri.fsPath(sourceFileUri_01), 'utf8')).to.be.equal('foo_01'); - expect(fs.readFileSync(FileUri.fsPath(sourceFileUri_02), 'utf8')).to.be.equal('foo_02'); - - const stat = await fileSystem.move(sourceUri.toString(), targetUri.toString(), { overwrite: true }); - expect(stat).is.an('object').and.has.property('uri').that.equals(targetUri.toString()); - expect(fs.existsSync(FileUri.fsPath(sourceUri))).to.be.false; - expect(fs.statSync(FileUri.fsPath(targetUri)).isDirectory()).to.be.true; - expect(fs.readdirSync(FileUri.fsPath(targetUri))).to.include('foo_01.txt').and.to.include('foo_02.txt'); - expect(fs.readFileSync(FileUri.fsPath(targetUri.resolve('foo_01.txt')), 'utf8')).to.be.equal('foo_01'); - expect(fs.readFileSync(FileUri.fsPath(targetUri.resolve('foo_02.txt')), 'utf8')).to.be.equal('foo_02'); - }); - - it('Moving a non-empty directory to a non-empty directory. Should be rejected because the target location is not empty.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('bar'); - const sourceFileUri_01 = sourceUri.resolve('foo_01.txt'); - const sourceFileUri_02 = sourceUri.resolve('foo_02.txt'); - const targetFileUri_01 = targetUri.resolve('bar_01.txt'); - const targetFileUri_02 = targetUri.resolve('bar_02.txt'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - fs.mkdirSync(FileUri.fsPath(targetUri)); - fs.writeFileSync(FileUri.fsPath(sourceFileUri_01), 'foo_01'); - fs.writeFileSync(FileUri.fsPath(sourceFileUri_02), 'foo_02'); - fs.writeFileSync(FileUri.fsPath(targetFileUri_01), 'bar_01'); - fs.writeFileSync(FileUri.fsPath(targetFileUri_02), 'bar_02'); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.statSync(FileUri.fsPath(targetUri)).isDirectory()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(sourceFileUri_01), 'utf8')).to.be.equal('foo_01'); - expect(fs.readFileSync(FileUri.fsPath(sourceFileUri_02), 'utf8')).to.be.equal('foo_02'); - expect(fs.readFileSync(FileUri.fsPath(targetFileUri_01), 'utf8')).to.be.equal('bar_01'); - expect(fs.readFileSync(FileUri.fsPath(targetFileUri_02), 'utf8')).to.be.equal('bar_02'); - expect(fs.readdirSync(FileUri.fsPath(sourceUri))).to.include('foo_01.txt').and.to.include('foo_02.txt'); - expect(fs.readdirSync(FileUri.fsPath(targetUri))).to.include('bar_01.txt').and.to.include('bar_02.txt'); - - await expectThrowsAsync(fileSystem.move(sourceUri.toString(), targetUri.toString(), { overwrite: true }), Error); - }); - - }); - - describe('05 #copy', () => { - - it('Copy a file from non existing location. Should be rejected with an error. Nothing to copy.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('bar'); - fs.mkdirSync(FileUri.fsPath(targetUri)); - expect(fs.existsSync(FileUri.fsPath(sourceUri))).to.be.false; - expect(fs.statSync(FileUri.fsPath(targetUri)).isDirectory()).to.be.true; - - await expectThrowsAsync(fileSystem.copy(sourceUri.toString(), targetUri.toString()), Error); - }); - - it('Copy a file to existing location without overwrite enabled. Should be rejected with an error.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('bar'); - fs.mkdirSync(FileUri.fsPath(targetUri)); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.statSync(FileUri.fsPath(targetUri)).isDirectory()).to.be.true; - - await expectThrowsAsync(fileSystem.copy(sourceUri.toString(), targetUri.toString()), Error); - }); - - it('Copy a file to existing location with the same file name. Should be rejected with an error.', async () => { - const sourceUri = root.resolve('foo'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - - await expectThrowsAsync(fileSystem.copy(sourceUri.toString(), sourceUri.toString()), Error); - }); - - it('Copy an empty directory to a non-existing location. Should return with the file stat representing the new file at the target location.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('bar'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.existsSync(FileUri.fsPath(targetUri))).to.be.false; - - const stat = await fileSystem.copy(sourceUri.toString(), targetUri.toString()); - expect(stat).to.be.an('object'); - expect(stat).to.have.property('uri') - .that.is.equal(targetUri.toString()); - expect(fs.existsSync(FileUri.fsPath(sourceUri))).to.be.true; - expect(fs.existsSync(FileUri.fsPath(targetUri))).to.be.true; - }); - - it('Copy an empty directory to a non-existing, nested location. Should return with the file stat representing the new file at the target location.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('nested/path/to/bar'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.existsSync(FileUri.fsPath(targetUri))).to.be.false; - - const stat = await fileSystem.copy(sourceUri.toString(), targetUri.toString()); - expect(stat).to.be.an('object'); - expect(stat).to.have.property('uri') - .that.is.equal(targetUri.toString()); - expect(fs.existsSync(FileUri.fsPath(sourceUri))).to.be.true; - expect(fs.existsSync(FileUri.fsPath(targetUri))).to.be.true; - }); - - it('Copy a directory with content to a non-existing location. Should return with the file stat representing the new file at the target location.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('bar'); - const subSourceUri = sourceUri.resolve('foo_01.txt'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - fs.writeFileSync(FileUri.fsPath(subSourceUri), 'foo'); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.statSync(FileUri.fsPath(subSourceUri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(subSourceUri), 'utf8')).to.be.equal('foo'); - expect(fs.existsSync(FileUri.fsPath(targetUri))).to.be.false; - - const stat = await fileSystem.copy(sourceUri.toString(), targetUri.toString()); - expect(stat).to.be.an('object'); - expect(stat).to.have.property('uri').that.is.equal(targetUri.toString()); - expect(fs.existsSync(FileUri.fsPath(sourceUri))).to.be.true; - expect(fs.existsSync(FileUri.fsPath(targetUri))).to.be.true; - expect(fs.readdirSync(FileUri.fsPath(sourceUri))).to.contain('foo_01.txt'); - expect(fs.readdirSync(FileUri.fsPath(targetUri))).to.contain('foo_01.txt'); - expect(fs.readFileSync(FileUri.fsPath(subSourceUri), 'utf8')).to.be.equal('foo'); - expect(fs.readFileSync(FileUri.fsPath(targetUri.resolve('foo_01.txt')), 'utf8')).to.be.equal('foo'); - }); - - it('Copy a directory with content to a non-existing, nested location. Should return with the file stat representing the new file at the target location.', async () => { - const sourceUri = root.resolve('foo'); - const targetUri = root.resolve('nested/path/to/bar'); - const subSourceUri = sourceUri.resolve('foo_01.txt'); - fs.mkdirSync(FileUri.fsPath(sourceUri)); - fs.writeFileSync(FileUri.fsPath(subSourceUri), 'foo'); - expect(fs.statSync(FileUri.fsPath(sourceUri)).isDirectory()).to.be.true; - expect(fs.statSync(FileUri.fsPath(subSourceUri)).isFile()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(subSourceUri), 'utf8')).to.be.equal('foo'); - expect(fs.existsSync(FileUri.fsPath(targetUri))).to.be.false; - - const stat = await fileSystem.copy(sourceUri.toString(), targetUri.toString()); - expect(stat).to.be.an('object'); - expect(stat).to.have.property('uri') - .that.is.equal(targetUri.toString()); - expect(fs.existsSync(FileUri.fsPath(sourceUri))).to.be.true; - expect(fs.existsSync(FileUri.fsPath(targetUri))).to.be.true; - expect(fs.readdirSync(FileUri.fsPath(sourceUri))).to.contain('foo_01.txt'); - expect(fs.readdirSync(FileUri.fsPath(targetUri))).to.contain('foo_01.txt'); - expect(fs.readFileSync(FileUri.fsPath(subSourceUri), 'utf8')).to.be.equal('foo'); - expect(fs.readFileSync(FileUri.fsPath(targetUri.resolve('foo_01.txt')), 'utf8')).to.be.equal('foo'); - }); - - }); - - describe('07 #createFile', () => { - - it('Should be rejected with an error if a file already exists with the given URI.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo'); - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - - await expectThrowsAsync(fileSystem.createFile(uri.toString()), Error); - }); - - it('Should be rejected with an error if the encoding is given but cannot be handled.', async () => { - const uri = root.resolve('foo.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - await expectThrowsAsync(fileSystem.createFile(uri.toString(), { encoding: 'unknownEncoding' }), Error); - }); - - it('Should create an empty file without any contents by default.', async () => { - const uri = root.resolve('foo.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - const stat = await fileSystem.createFile(uri.toString()); - expect(stat).is.an('object'); - expect(stat).has.property('uri').that.is.equal(uri.toString()); - expect(stat).not.has.property('children'); - expect(fs.readFileSync(FileUri.fsPath(uri), 'utf8')).to.be.empty; - }); - - it('Should create a file with the desired content.', async () => { - const uri = root.resolve('foo.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - const stat = await fileSystem.createFile(uri.toString(), { content: 'foo' }); - expect(stat).is.an('object'); - expect(stat).has.property('uri') - .that.is.equal(uri.toString()); - expect(stat).not.has.property('children'); - expect(fs.readFileSync(FileUri.fsPath(uri), 'utf8')) - .to.be.equal('foo'); - }); - - it('Should create a file with the desired content into a non-existing, nested location.', async () => { - const uri = root.resolve('foo/bar/baz.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - const stat = await fileSystem.createFile(uri.toString(), { content: 'foo' }); - expect(stat).is.an('object'); - expect(stat).has.property('uri') - .that.is.equal(uri.toString()); - expect(stat).not.has.property('children'); - expect(fs.readFileSync(FileUri.fsPath(uri), 'utf8')) - .to.be.equal('foo'); - }); - - it('Should create a file with the desired content and encoding.', async () => { - const uri = root.resolve('foo.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - const stat = await fileSystem.createFile(uri.toString(), { content: 'foo', encoding: 'utf8' }); - expect(stat).is.an('object'); - expect(stat).has.property('uri') - .that.is.equal(uri.toString()); - expect(stat).not.has.property('children'); - expect(fs.readFileSync(FileUri.fsPath(uri), 'utf8')) - .to.be.equal('foo'); - }); - - }); - - describe('08 #createFolder', () => { - - it('Should be rejected with an error if a FILE already exist under the desired URI.', async () => { - const uri = root.resolve('foo'); - fs.writeFileSync(FileUri.fsPath(uri), 'some content'); - expect(fs.statSync(FileUri.fsPath(uri)).isDirectory()).to.be.false; - - await expectThrowsAsync(fileSystem.createFolder(uri.toString()), Error); - }); - - it('Should NOT be rejected with an error if a DIRECTORY already exist under the desired URI.', async () => { - const uri = root.resolve('foo'); - fs.mkdirSync(FileUri.fsPath(uri)); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.true; - - const stat = await fileSystem.createFolder(uri.toString()); - expect(stat).to.be.an('object'); - expect(stat).to.have.property('uri') - .that.equals(uri.toString()); - expect(stat).to.have.property('children') - .that.is.empty; - }); - - it('Should create a directory and return with the stat object on successful directory creation.', async () => { - const uri = root.resolve('foo'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - const stat = await fileSystem.createFolder(uri.toString()); - expect(stat).to.be.an('object'); - expect(stat).to.have.property('uri') - .that.equals(uri.toString()); - expect(stat).to.have.property('children') - .that.is.empty; - }); - - it('Should create all the missing directories and return with the stat object on successful creation.', async () => { - const uri = root.resolve('foo/bar/foobar/barfoo'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - const stat = await fileSystem.createFolder(uri.toString()); - expect(stat).to.be.an('object'); - expect(stat).to.have.property('uri') - .that.equals(uri.toString()); - expect(stat).to.have.property('children') - .that.is.empty; - }); - - }); - - describe('09 #touch', () => { - - it('Should create a new file if it does not exist yet.', async () => { - const uri = root.resolve('foo.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - const stat = await fileSystem.touchFile(uri.toString()); - expect(stat).is.an('object'); - expect(stat).has.property('uri') - .that.equals(uri.toString()); - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - }); - - it('Should update the modification timestamp on an existing file.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo'); - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - - const initialStat = await fileSystem.getFileStat(uri.toString()); - expect(initialStat).to.not.be.undefined; - - expect(initialStat).is.an('object'); - expect(initialStat).has.property('uri').that.equals(uri.toString()); - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - - // https://nodejs.org/en/docs/guides/working-with-different-filesystems/#timestamp-resolution - await sleep(1000); - - const updatedStat = await fileSystem.touchFile(uri.toString()); - expect(updatedStat).is.an('object'); - expect(updatedStat).has.property('uri').that.equals(uri.toString()); - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - expect(updatedStat.lastModification).to.be.greaterThan(initialStat!.lastModification); - }); - - }); - - describe('#10 delete', () => { - - it('Should be rejected when the file to delete does not exist.', async () => { - const uri = root.resolve('foo.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - await expectThrowsAsync(fileSystem.delete(uri.toString(), { moveToTrash: false }), Error); - }); - - it('Should delete the file.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo'); - expect(fs.readFileSync(FileUri.fsPath(uri), 'utf8')).to.be.equal('foo'); - - await fileSystem.delete(uri.toString(), { moveToTrash: false }); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - }); - - it('Should delete a directory without content.', async () => { - const uri = root.resolve('foo'); - fs.mkdirSync(FileUri.fsPath(uri)); - expect(fs.statSync(FileUri.fsPath(uri)).isDirectory()).to.be.true; - - await fileSystem.delete(uri.toString(), { moveToTrash: false }); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - }); - - it('Should delete a directory with all its content.', async () => { - const uri = root.resolve('foo'); - const subUri = uri.resolve('bar.txt'); - fs.mkdirSync(FileUri.fsPath(uri)); - fs.writeFileSync(FileUri.fsPath(subUri), 'bar'); - expect(fs.statSync(FileUri.fsPath(uri)).isDirectory()).to.be.true; - expect(fs.readFileSync(FileUri.fsPath(subUri), 'utf8')).to.be.equal('bar'); - - await fileSystem.delete(uri.toString(), { moveToTrash: false }); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - expect(fs.existsSync(FileUri.fsPath(subUri))).to.be.false; - }); - - }); - - describe('#11 getEncoding', () => { - - it('Should be rejected with an error if no file exists under the given URI.', async () => { - const uri = root.resolve('foo.txt'); - expect(fs.existsSync(FileUri.fsPath(uri))).to.be.false; - - await expectThrowsAsync(fileSystem.getEncoding(uri.toString()), Error); - }); - - it('Should be rejected with an error if the URI points to a directory instead of a file.', async () => { - const uri = root.resolve('foo'); - fs.mkdirSync(FileUri.fsPath(uri)); - expect(fs.statSync(FileUri.fsPath(uri)).isDirectory()).to.be.true; - - await expectThrowsAsync(fileSystem.getEncoding(uri.toString()), Error); - }); - - it('Should return with the encoding of the file.', async () => { - const uri = root.resolve('foo.txt'); - fs.writeFileSync(FileUri.fsPath(uri), 'foo'); - expect(fs.statSync(FileUri.fsPath(uri)).isFile()).to.be.true; - - const encoding = await fileSystem.getEncoding(uri.toString()); - expect(encoding).to.be.equal('utf8'); - }); - - }); - - describe('#14 roots', async () => { - - it('should not throw error', async () => { - expect(await createFileSystem().getRoots()).to.be.not.empty; - }); - - }); - - describe('#15 currentUserHome', async () => { - - it('should exist', async () => { - const userHome = await createFileSystem().getCurrentUserHome(); - expect(userHome).to.not.be.undefined; - const actual = userHome!.uri.toString(); - const expected = FileUri.create(os.homedir()).toString(); - expect(expected).to.be.equal(actual); - }); - - }); - - describe('#16 drives', async () => { - - it('should list URIs of the drives', async function (): Promise { - this.timeout(10_000); - const drives = await createFileSystem().getDrives(); - expect(drives).to.be.not.empty; - }); - - }); - - describe('#17 fsPath', async () => { - - it('should return undefined', async function (): Promise { - expect(await createFileSystem().getFsPath('http://www.theia-ide.org')).to.be.undefined; - }); - - it('should return a platform specific path', async function (): Promise { - if (isWindows) { - expect(await createFileSystem().getFsPath('file:///C:/user/theia')).to.be.equal('c:\\user\\theia'); - expect(await createFileSystem().getFsPath('file:///C%3A/user/theia')).to.be.equal('c:\\user\\theia'); - } else { - expect(await createFileSystem().getFsPath('file:///user/home/theia')).to.be.equal('/user/home/theia'); - } - }); - }); - - function createFileSystem(): FileSystem { - return new FileSystemNode(); - } - - function sleep(time: number): Promise { - return new Promise(resolve => setTimeout(resolve, time)); - } - -}); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -process.on('unhandledRejection', (reason: any) => { - console.error('Unhandled promise rejection: ' + reason); -}); diff --git a/packages/filesystem/src/node/node-filesystem.ts b/packages/filesystem/src/node/node-filesystem.ts deleted file mode 100644 index 0af7ccff984f6..0000000000000 --- a/packages/filesystem/src/node/node-filesystem.ts +++ /dev/null @@ -1,616 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2017 TypeFox and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import * as mv from 'mv'; -import * as trash from 'trash'; -import * as paths from 'path'; -import * as fs from 'fs-extra'; -import { v4 } from 'uuid'; -import * as os from 'os'; -import * as touch from 'touch'; -import * as drivelist from 'drivelist'; -import { injectable, inject, optional } from 'inversify'; -import { TextDocument } from 'vscode-languageserver-types'; -import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; -import URI from '@theia/core/lib/common/uri'; -import { TextDocumentContentChangeDelta } from '@theia/core/lib/common/lsp-types'; -import { FileUri } from '@theia/core/lib/node/file-uri'; -import { FileStat, FileSystem, FileSystemClient, FileSystemError, FileMoveOptions, FileDeleteOptions, FileAccess } from '../common/filesystem'; -import * as iconv from 'iconv-lite'; -import { EncodingUtil } from './encoding-util'; - -@injectable() -export class FileSystemNodeOptions { - - encoding: string; - recursive: boolean; - overwrite: boolean; - moveToTrash: boolean; - - public static DEFAULT: FileSystemNodeOptions = { - encoding: 'utf8', - overwrite: false, - recursive: true, - moveToTrash: true - }; - -} - -@injectable() -export class FileSystemNode implements FileSystem { - - constructor( - @inject(FileSystemNodeOptions) @optional() protected readonly options: FileSystemNodeOptions = FileSystemNodeOptions.DEFAULT - ) { } - - protected client: FileSystemClient | undefined; - setClient(client: FileSystemClient | undefined): void { - this.client = client; - } - - async getFileStat(uri: string): Promise { - const uri_ = new URI(uri); - const stat = await this.doGetStat(uri_, 1); - return stat; - } - - async exists(uri: string): Promise { - return fs.pathExists(FileUri.fsPath(new URI(uri))); - } - - async resolveContent(uri: string, options?: { encoding?: string }): Promise<{ stat: FileStat, content: string }> { - const _uri = new URI(uri); - const stat = await this.doGetStat(_uri, 0); - if (!stat) { - throw FileSystemError.FileNotFound(uri); - } - if (stat.isDirectory) { - throw FileSystemError.FileIsDirectory(uri, 'Cannot resolve the content.'); - } - const encoding = await this.doGetEncoding(options); - const contentBuffer = await fs.readFile(FileUri.fsPath(_uri)); - const content = iconv.decode(contentBuffer, encoding); - return { stat, content }; - } - - async setContent(file: FileStat, content: string, options?: { encoding?: string }): Promise { - const _uri = new URI(file.uri); - const stat = await this.doGetStat(_uri, 0); - if (!stat) { - throw FileSystemError.FileNotFound(file.uri); - } - if (stat.isDirectory) { - throw FileSystemError.FileIsDirectory(file.uri, 'Cannot set the content.'); - } - if (!(await this.isInSync(file, stat))) { - throw this.createOutOfSyncError(file, stat); - } - const encoding = await this.doGetEncoding(options); - const encodedContent = iconv.encode(content, encoding); - await fs.writeFile(FileUri.fsPath(_uri), encodedContent); - const newStat = await this.doGetStat(_uri, 1); - if (newStat) { - return newStat; - } - throw FileSystemError.FileNotFound(file.uri, 'Error occurred while writing file content.'); - } - - async updateContent(file: FileStat, contentChanges: TextDocumentContentChangeEvent[], options?: { encoding?: string, overwriteEncoding?: string }): Promise { - const _uri = new URI(file.uri); - const stat = await this.doGetStat(_uri, 0); - if (!stat) { - throw FileSystemError.FileNotFound(file.uri); - } - if (stat.isDirectory) { - throw FileSystemError.FileIsDirectory(file.uri, 'Cannot set the content.'); - } - if (!this.checkInSync(file, stat)) { - throw this.createOutOfSyncError(file, stat); - } - if (contentChanges.length === 0 && !(options && options.overwriteEncoding)) { - return stat; - } - const encoding = await this.doGetEncoding(options); - const contentBuffer = await fs.readFile(FileUri.fsPath(_uri)); - const content = iconv.decode(contentBuffer, encoding); - const newContent = this.applyContentChanges(content, contentChanges); - const writeEncoding = options && options.overwriteEncoding ? options.overwriteEncoding : encoding; - const encodedNewContent = iconv.encode(newContent, writeEncoding); - await fs.writeFile(FileUri.fsPath(_uri), encodedNewContent); - const newStat = await this.doGetStat(_uri, 1); - if (newStat) { - return newStat; - } - throw FileSystemError.FileNotFound(file.uri, 'Error occurred while writing file content.'); - } - - protected applyContentChanges(content: string, contentChanges: TextDocumentContentChangeEvent[]): string { - let document = TextDocument.create('', '', 1, content); - for (const change of contentChanges) { - let newContent: string; - if (TextDocumentContentChangeDelta.is(change)) { - const start = document.offsetAt(change.range.start); - const end = document.offsetAt(change.range.end); - newContent = document.getText().substr(0, start) + change.text + document.getText().substr(end); - } else { - newContent = change.text; - } - document = TextDocument.create(document.uri, document.languageId, document.version, newContent); - } - return document.getText(); - } - - protected async isInSync(file: FileStat, stat: FileStat): Promise { - if (this.checkInSync(file, stat)) { - return true; - } - return this.client ? this.client.shouldOverwrite(file, stat) : false; - } - - protected checkInSync(file: FileStat, stat: FileStat): boolean { - return stat.lastModification === file.lastModification && stat.size === file.size; - } - - protected createOutOfSyncError(file: FileStat, stat: FileStat): Error { - return FileSystemError.FileIsOutOfSync(file, stat); - } - - async move(sourceUri: string, targetUri: string, options?: FileMoveOptions): Promise { - if (this.client) { - await this.client.willMove(sourceUri, targetUri); - } - let result: FileStat; - let failed = false; - try { - result = await this.doMove(sourceUri, targetUri, options); - } catch (e) { - failed = true; - throw e; - } finally { - if (this.client) { - await this.client.didMove(sourceUri, targetUri, failed); - } - } - return result; - } - protected async doMove(sourceUri: string, targetUri: string, options?: FileMoveOptions): Promise { - const _sourceUri = new URI(sourceUri); - const _targetUri = new URI(targetUri); - const [sourceStat, targetStat, overwrite] = await Promise.all([this.doGetStat(_sourceUri, 1), this.doGetStat(_targetUri, 1), this.doGetOverwrite(options)]); - if (!sourceStat) { - throw FileSystemError.FileNotFound(sourceUri); - } - if (targetStat && !overwrite) { - throw FileSystemError.FileExists(targetUri, "Did you set the 'overwrite' flag to true?"); - } - - // Different types. Files <-> Directory. - if (targetStat && sourceStat.isDirectory !== targetStat.isDirectory) { - if (targetStat.isDirectory) { - throw FileSystemError.FileIsDirectory(targetStat.uri, `Cannot move '${sourceStat.uri}' file to an existing location.`); - } - throw FileSystemError.FileNotDirectory(targetStat.uri, `Cannot move '${sourceStat.uri}' directory to an existing location.`); - } - const [sourceMightHaveChildren, targetMightHaveChildren] = await Promise.all([this.mayHaveChildren(_sourceUri), this.mayHaveChildren(_targetUri)]); - // Handling special Windows case when source and target resources are empty folders. - // Source should be deleted and target should be touched. - if (overwrite && targetStat && targetStat.isDirectory && sourceStat.isDirectory && !sourceMightHaveChildren && !targetMightHaveChildren) { - // The value should be a Unix timestamp in seconds. - // For example, `Date.now()` returns milliseconds, so it should be divided by `1000` before passing it in. - const now = Date.now() / 1000; - await fs.utimes(FileUri.fsPath(_targetUri), now, now); - await fs.rmdir(FileUri.fsPath(_sourceUri)); - const newStat = await this.doGetStat(_targetUri, 1); - if (newStat) { - return newStat; - } - throw FileSystemError.FileNotFound(targetUri, `Error occurred when moving resource from '${sourceUri}' to '${targetUri}'.`); - } else if (overwrite && targetStat && targetStat.isDirectory && sourceStat.isDirectory && !targetMightHaveChildren && sourceMightHaveChildren) { - // Copy source to target, since target is empty. Then wipe the source content. - const newStat = await this.copy(sourceUri, targetUri, { overwrite }); - await this.delete(sourceUri); - return newStat; - } else { - return new Promise((resolve, reject) => { - mv(FileUri.fsPath(_sourceUri), FileUri.fsPath(_targetUri), { mkdirp: true, clobber: overwrite }, async error => { - if (error) { - reject(error); - return; - } - resolve(await this.doGetStat(_targetUri, 1)); - }); - }); - } - } - - async copy(sourceUri: string, targetUri: string, options?: { overwrite?: boolean, recursive?: boolean }): Promise { - const _sourceUri = new URI(sourceUri); - const _targetUri = new URI(targetUri); - const [sourceStat, targetStat, overwrite, recursive] = await Promise.all([ - this.doGetStat(_sourceUri, 0), - this.doGetStat(_targetUri, 0), - this.doGetOverwrite(options), - this.doGetRecursive(options) - ]); - if (!sourceStat) { - throw FileSystemError.FileNotFound(sourceUri); - } - if (targetStat && !overwrite) { - throw FileSystemError.FileExists(targetUri, "Did you set the 'overwrite' flag to true?"); - } - if (targetStat && targetStat.uri === sourceStat.uri) { - throw FileSystemError.FileExists(targetUri, 'Cannot perform copy, source and destination are the same.'); - } - await fs.copy(FileUri.fsPath(_sourceUri), FileUri.fsPath(_targetUri), { overwrite, recursive }); - const newStat = await this.doGetStat(_targetUri, 1); - if (newStat) { - return newStat; - } - throw FileSystemError.FileNotFound(targetUri, `Error occurred while copying ${sourceUri} to ${targetUri}.`); - } - - async createFile(uri: string, options?: { content?: string, encoding?: string }): Promise { - if (this.client) { - await this.client.willCreate(uri); - } - let result: FileStat; - let failed = false; - try { - result = await this.doCreateFile(uri, options); - } catch (e) { - failed = true; - throw e; - } finally { - if (this.client) { - await this.client.didCreate(uri, failed); - } - } - return result; - } - - protected async doCreateFile(uri: string, options?: { content?: string, encoding?: string }): Promise { - const _uri = new URI(uri); - const parentUri = _uri.parent; - const [stat, parentStat] = await Promise.all([this.doGetStat(_uri, 0), this.doGetStat(parentUri, 0)]); - if (stat) { - throw FileSystemError.FileExists(uri, 'Error occurred while creating the file.'); - } - if (!parentStat) { - await fs.mkdirs(FileUri.fsPath(parentUri)); - } - const content = await this.doGetContent(options); - const encoding = await this.doGetEncoding(options); - const encodedNewContent = iconv.encode(content, encoding); - await fs.writeFile(FileUri.fsPath(_uri), encodedNewContent); - const newStat = await this.doGetStat(_uri, 1); - if (newStat) { - return newStat; - } - throw FileSystemError.FileNotFound(uri, 'Error occurred while creating the file.'); - } - - async createFolder(uri: string): Promise { - if (this.client) { - await this.client.willCreate(uri); - } - let result: FileStat; - let failed = false; - try { - result = await this.doCreateFolder(uri); - } catch (e) { - failed = true; - throw e; - } finally { - if (this.client) { - await this.client.didCreate(uri, failed); - } - } - return result; - } - - async doCreateFolder(uri: string): Promise { - const _uri = new URI(uri); - const stat = await this.doGetStat(_uri, 0); - if (stat) { - if (stat.isDirectory) { - return stat; - } - throw FileSystemError.FileExists(uri, 'Error occurred while creating the directory: path is a file.'); - } - await fs.mkdirs(FileUri.fsPath(_uri)); - const newStat = await this.doGetStat(_uri, 1); - if (newStat) { - return newStat; - } - throw FileSystemError.FileNotFound(uri, 'Error occurred while creating the directory.'); - } - - async touchFile(uri: string): Promise { - const _uri = new URI(uri); - const stat = await this.doGetStat(_uri, 0); - if (!stat) { - return this.createFile(uri); - } else { - return new Promise((resolve, reject) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - touch(FileUri.fsPath(_uri), async (error: any) => { - if (error) { - reject(error); - return; - } - resolve(await this.doGetStat(_uri, 1)); - }); - }); - } - } - - async delete(uri: string, options?: FileDeleteOptions): Promise { - if (this.client) { - await this.client.willDelete(uri); - } - let failed = false; - try { - await this.doDelete(uri, options); - } catch (e) { - failed = true; - throw e; - } finally { - if (this.client) { - await this.client.didDelete(uri, failed); - } - } - } - - protected async doDelete(uri: string, options?: FileDeleteOptions): Promise { - const _uri = new URI(uri); - const stat = await this.doGetStat(_uri, 0); - if (!stat) { - throw FileSystemError.FileNotFound(uri); - } - // Windows 10. - // Deleting an empty directory throws `EPERM error` instead of `unlinkDir`. - // https://github.com/paulmillr/chokidar/issues/566 - const moveToTrash = await this.doGetMoveToTrash(options); - if (moveToTrash) { - return trash([FileUri.fsPath(_uri)]); - } else { - const filePath = FileUri.fsPath(_uri); - const outputRootPath = paths.join(os.tmpdir(), v4()); - try { - await new Promise((resolve, reject) => mv(filePath, outputRootPath, { mkdirp: true, clobber: true }, async error => { - if (error) { - reject(error); - return; - } - resolve(undefined); - })); - // There is no reason for the promise returned by this function not to resolve - // as soon as the move is complete. Clearing up the temporary files can be - // done in the background. - fs.remove(outputRootPath); - } catch (error) { - return fs.remove(filePath); - } - } - } - - async getEncoding(uri: string): Promise { - const _uri = new URI(uri); - const stat = await this.doGetStat(_uri, 0); - if (!stat) { - throw FileSystemError.FileNotFound(uri); - } - if (stat.isDirectory) { - throw FileSystemError.FileIsDirectory(uri, 'Cannot get the encoding.'); - } - return this.options.encoding; - } - - async guessEncoding(uri: string): Promise { - const _uri = new URI(uri); - const stat = await this.doGetStat(_uri, 0); - if (!stat) { - throw FileSystemError.FileNotFound(uri); - } - if (stat.isDirectory) { - throw FileSystemError.FileIsDirectory(uri, 'Cannot guess the encoding.'); - } - return EncodingUtil.guessEncodingByBuffer(await fs.readFile(FileUri.fsPath(_uri))); - } - - async getRoots(): Promise { - const cwdRoot = paths.parse(process.cwd()).root; - const rootUri = FileUri.create(cwdRoot); - const root = await this.doGetStat(rootUri, 1); - if (root) { - return [root]; - } - return []; - } - - async getCurrentUserHome(): Promise { - return this.getFileStat(FileUri.create(os.homedir()).toString()); - } - - getDrives(): Promise { - return new Promise((resolve, reject) => { - drivelist.list((error: Error, drives: { readonly mountpoints: { readonly path: string; }[] }[]) => { - if (error) { - reject(error); - return; - } - - const uris = drives - .map(drive => drive.mountpoints) - .reduce((prev, curr) => prev.concat(curr), []) - .map(mountpoint => mountpoint.path) - .filter(this.filterMountpointPath.bind(this)) - .map(path => FileUri.create(path)) - .map(uri => uri.toString()); - - resolve(uris); - }); - }); - } - - /** - * Filters hidden and system partitions. - */ - protected filterMountpointPath(path: string): boolean { - // OS X: This is your sleep-image. When your Mac goes to sleep it writes the contents of its memory to the hard disk. (https://bit.ly/2R6cztl) - if (path === '/private/var/vm') { - return false; - } - // Ubuntu: This system partition is simply the boot partition created when the computers mother board runs UEFI rather than BIOS. (https://bit.ly/2N5duHr) - if (path === '/boot/efi') { - return false; - } - return true; - } - - dispose(): void { - // NOOP - } - - async access(uri: string, mode: number = FileAccess.Constants.F_OK): Promise { - try { - await fs.access(FileUri.fsPath(uri), mode); - return true; - } catch { - return false; - } - } - - async getFsPath(uri: string): Promise { - if (!uri.startsWith('file:/')) { - return undefined; - } else { - return FileUri.fsPath(uri); - } - } - - protected async doGetStat(uri: URI, depth: number): Promise { - try { - const stats = await fs.stat(FileUri.fsPath(uri)); - if (stats.isDirectory()) { - return this.doCreateDirectoryStat(uri, stats, depth); - } - return this.doCreateFileStat(uri, stats); - } catch (error) { - if (isErrnoException(error)) { - if (error.code === 'ENOENT' || error.code === 'EACCES' || error.code === 'EBUSY' || error.code === 'EPERM') { - return undefined; - } - } - throw error; - } - } - - protected async doCreateFileStat(uri: URI, stat: fs.Stats): Promise { - return { - uri: uri.toString(), - lastModification: stat.mtime.getTime(), - isDirectory: false, - size: stat.size - }; - } - - protected async doCreateDirectoryStat(uri: URI, stat: fs.Stats, depth: number): Promise { - const children = depth > 0 ? await this.doGetChildren(uri, depth) : []; - return { - uri: uri.toString(), - lastModification: stat.mtime.getTime(), - isDirectory: true, - children - }; - } - - protected async doGetChildren(uri: URI, depth: number): Promise { - const files = await fs.readdir(FileUri.fsPath(uri)); - const children = await Promise.all(files.map(fileName => uri.resolve(fileName)).map(childUri => this.doGetStat(childUri, depth - 1))); - return children.filter(notEmpty); - } - - /** - * Return `true` if it's possible for this URI to have children. - * It might not be possible to be certain because of permission problems or other filesystem errors. - */ - protected async mayHaveChildren(uri: URI): Promise { - /* If there's a problem reading the root directory. Assume it's not empty to avoid overwriting anything. */ - try { - const rootStat = await this.doGetStat(uri, 0); - if (rootStat === undefined) { - return true; - } - /* Not a directory. */ - if (rootStat !== undefined && rootStat.isDirectory === false) { - return false; - } - } catch { - return true; - } - - /* If there's a problem with it's children then the directory must not be empty. */ - try { - const stat = await this.doGetStat(uri, 1); - if (stat !== undefined && stat.children !== undefined) { - return stat.children.length > 0; - } else { - return true; - } - } catch { - return true; - } - } - - protected async doGetEncoding(option?: { encoding?: string }): Promise { - return option && typeof (option.encoding) !== 'undefined' - ? option.encoding - : this.options.encoding; - } - - protected async doGetOverwrite(option?: { overwrite?: boolean }): Promise { - return option && typeof (option.overwrite) !== 'undefined' - ? option.overwrite - : this.options.overwrite; - } - - protected async doGetRecursive(option?: { recursive?: boolean }): Promise { - return option && typeof (option.recursive) !== 'undefined' - ? option.recursive - : this.options.recursive; - } - - protected async doGetMoveToTrash(option?: { moveToTrash?: boolean }): Promise { - return option && typeof (option.moveToTrash) !== 'undefined' - ? option.moveToTrash - : this.options.moveToTrash; - } - - protected async doGetContent(option?: { content?: string }): Promise { - return (option && option.content) || ''; - } - -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function isErrnoException(error: any | NodeJS.ErrnoException): error is NodeJS.ErrnoException { - return (error).code !== undefined && (error).errno !== undefined; -} - -function notEmpty(value: T | undefined): value is T { - return value !== undefined; -} diff --git a/packages/getting-started/src/browser/getting-started-widget.tsx b/packages/getting-started/src/browser/getting-started-widget.tsx index 71c6fb16a9fe7..d7d4671b07bf3 100644 --- a/packages/getting-started/src/browser/getting-started-widget.tsx +++ b/packages/getting-started/src/browser/getting-started-widget.tsx @@ -20,12 +20,12 @@ import { injectable, inject, postConstruct } from 'inversify'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import { CommandRegistry, isOSX, environment } from '@theia/core/lib/common'; import { WorkspaceCommands, WorkspaceService } from '@theia/workspace/lib/browser'; -import { FileStat, FileSystem } from '@theia/filesystem/lib/common/filesystem'; import { FileSystemUtils } from '@theia/filesystem/lib/common/filesystem-utils'; import { KeymapsCommands } from '@theia/keymaps/lib/browser'; import { CommonCommands, LabelProvider } from '@theia/core/lib/browser'; import { ApplicationInfo, ApplicationServer } from '@theia/core/lib/common/application-protocol'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; /** * Default implementation of the `GettingStartedWidget`. @@ -59,7 +59,6 @@ export class GettingStartedWidget extends ReactWidget { */ protected applicationName = FrontendApplicationConfigProvider.get().applicationName; - protected stat: FileStat | undefined; protected home: string | undefined; /** @@ -85,8 +84,8 @@ export class GettingStartedWidget extends ReactWidget { @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(EnvVariablesServer) + protected readonly environments: EnvVariablesServer; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @@ -103,8 +102,7 @@ export class GettingStartedWidget extends ReactWidget { this.applicationInfo = await this.appServer.getApplicationInfo(); this.recentWorkspaces = await this.workspaceService.recentWorkspaces(); - this.stat = await this.fileSystem.getCurrentUserHome(); - this.home = this.stat ? new URI(this.stat.uri).path.toString() : undefined; + this.home = new URI(await this.environments.getHomeDirUri()).path.toString(); this.update(); } diff --git a/packages/git/src/browser/diff/git-diff-contribution.ts b/packages/git/src/browser/diff/git-diff-contribution.ts index 082a4368b612a..cd248fc2a5784 100644 --- a/packages/git/src/browser/diff/git-diff-contribution.ts +++ b/packages/git/src/browser/diff/git-diff-contribution.ts @@ -24,7 +24,6 @@ import { open, OpenerService } from '@theia/core/lib/browser'; import { NavigatorContextMenu, FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution'; import { UriCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import { GitQuickOpenService } from '../git-quick-open-service'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; import URI from '@theia/core/lib/common/uri'; import { GIT_RESOURCE_SCHEME } from '../git-resource'; @@ -32,6 +31,7 @@ import { Git, Repository } from '../../common'; import { WorkspaceRootUriAwareCommandHandler } from '@theia/workspace/lib/browser/workspace-commands'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export namespace GitDiffCommands { export const OPEN_FILE_DIFF: Command = { @@ -62,7 +62,7 @@ export class GitDiffContribution extends AbstractViewContribution @inject(WidgetManager) protected readonly widgetManager: WidgetManager, @inject(FrontendApplication) protected readonly app: FrontendApplication, @inject(GitQuickOpenService) protected readonly quickOpenService: GitQuickOpenService, - @inject(FileSystem) protected readonly fileSystem: FileSystem, + @inject(FileService) protected readonly fileService: FileService, @inject(OpenerService) protected openerService: OpenerService, @inject(MessageService) protected readonly notifications: MessageService, @inject(ScmService) protected readonly scmService: ScmService @@ -91,25 +91,23 @@ export class GitDiffContribution extends AbstractViewContribution await this.quickOpenService.chooseTagsAndBranches( async (fromRevision, toRevision) => { const uri = fileUri.toString(); - const fileStat = await this.fileSystem.getFileStat(uri); + const fileStat = await this.fileService.resolve(fileUri); const options: Git.Options.Diff = { uri, range: { fromRevision } }; - if (fileStat) { - if (fileStat.isDirectory) { - this.showWidget(options); - } else { - const fromURI = fileUri.withScheme(GIT_RESOURCE_SCHEME).withQuery(fromRevision); - const toURI = fileUri; - const diffUri = DiffUris.encode(fromURI, toURI); - if (diffUri) { - open(this.openerService, diffUri).catch(e => { - this.notifications.error(e.message); - }); - } + if (fileStat.isDirectory) { + this.showWidget(options); + } else { + const fromURI = fileUri.withScheme(GIT_RESOURCE_SCHEME).withQuery(fromRevision); + const toURI = fileUri; + const diffUri = DiffUris.encode(fromURI, toURI); + if (diffUri) { + open(this.openerService, diffUri).catch(e => { + this.notifications.error(e.message); + }); } } }, this.findGitRepository(fileUri)); diff --git a/packages/git/src/browser/git-quick-open-service.ts b/packages/git/src/browser/git-quick-open-service.ts index b1db474762b15..54a8ee3a0d4bd 100644 --- a/packages/git/src/browser/git-quick-open-service.ts +++ b/packages/git/src/browser/git-quick-open-service.ts @@ -21,11 +21,12 @@ import { Git, Repository, Branch, BranchType, Tag, Remote, StashEntry } from '.. import { GitRepositoryProvider } from './git-repository-provider'; import { MessageService } from '@theia/core/lib/common/message-service'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import { GitErrorHandler } from './git-error-handler'; import { ProgressService } from '@theia/core/lib/common/progress-service'; import URI from '@theia/core/lib/common/uri'; import { LabelProvider } from '@theia/core/lib/browser'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat } from '@theia/filesystem/lib/common/files'; export enum GitAction { PULL, @@ -44,20 +45,18 @@ export class GitQuickOpenService { @inject(ProgressService) protected readonly progressService: ProgressService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; - constructor( - @inject(Git) protected readonly git: Git, - @inject(GitRepositoryProvider) protected readonly repositoryProvider: GitRepositoryProvider, - @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService, - @inject(MessageService) protected readonly messageService: MessageService, - @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, - @inject(FileSystem) protected readonly fileSystem: FileSystem - ) { } + @inject(Git) protected readonly git: Git; + @inject(GitRepositoryProvider) protected readonly repositoryProvider: GitRepositoryProvider; + @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService; + @inject(MessageService) protected readonly messageService: MessageService; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(FileService) protected readonly fileService: FileService; async clone(url?: string, folder?: string, branch?: string): Promise { - return this.withProgress(async () => { + return this.withProgress(async () => { if (!folder) { const roots = await this.workspaceService.roots; - folder = roots[0].uri; + folder = roots[0].resource.toString(); } if (url) { @@ -99,7 +98,7 @@ export class GitQuickOpenService { private buildDefaultProjectPath = this.doBuildDefaultProjectPath.bind(this); private async doBuildDefaultProjectPath(folderPath: string, gitURI: string): Promise { - if (!(await this.fileSystem.exists(folderPath))) { + if (!(await this.fileService.exists(new URI(folderPath)))) { // user specifies its own project path, doesn't want us to guess it return folderPath; } @@ -116,7 +115,7 @@ export class GitQuickOpenService { if (!repository) { return; } - return this.withProgress(async () => { + return this.withProgress(async () => { const remotes = await this.getRemotes(); const execute = async (item: QuickOpenItem) => { try { @@ -141,7 +140,7 @@ export class GitQuickOpenService { if (!repository) { return; } - return this.withProgress(async () => { + return this.withProgress(async () => { try { if (action === GitAction.PULL) { await this.git.pull(repository, { remote: defaultRemote }); @@ -161,7 +160,7 @@ export class GitQuickOpenService { if (!repository) { return; } - return this.withProgress(async () => { + return this.withProgress(async () => { const [remotes, currentBranch] = await Promise.all([this.getRemotes(), this.getCurrentBranch()]); const execute = async (item: QuickOpenItem) => { try { @@ -185,7 +184,7 @@ export class GitQuickOpenService { if (!repository) { return; } - return this.withProgress(async () => { + return this.withProgress(async () => { const remotes = await this.getRemotes(); const defaultRemote = remotes[0].name; // I wish I could use assignment destructuring here. (GH-413) const executeRemote = async (remoteItem: GitQuickOpenItem) => { @@ -228,7 +227,7 @@ export class GitQuickOpenService { if (!repository) { return; } - return this.withProgress(async () => { + return this.withProgress(async () => { const [branches, currentBranch] = await Promise.all([this.getBranches(), this.getCurrentBranch()]); const execute = async (item: GitQuickOpenItem) => { try { @@ -249,7 +248,7 @@ export class GitQuickOpenService { if (!repository) { return; } - return this.withProgress(async () => { + return this.withProgress(async () => { const [branches, currentBranch] = await Promise.all([this.getBranches(), this.getCurrentBranch()]); if (currentBranch) { // We do not show the current branch. @@ -313,7 +312,7 @@ export class GitQuickOpenService { if (!repository) { return; } - return this.withProgress(async () => { + return this.withProgress(async () => { const [branches, tags, currentBranch] = await Promise.all([this.getBranches(repository), this.getTags(repository), this.getCurrentBranch(repository)]); const execute = async (item: GitQuickOpenItem) => { execFunc(item.ref.name, currentBranch ? currentBranch.name : ''); @@ -332,7 +331,7 @@ export class GitQuickOpenService { if (!repository) { throw new Error('No repositories were selected.'); } - return this.withProgress(async () => { + return this.withProgress(async () => { const lastMessage = (await this.git.exec(repository, ['log', '--format=%B', '-n', '1'])).stdout.trim(); if (lastMessage.length === 0) { throw new Error(`Repository ${repository.localUri} is not yet initialized.`); @@ -399,7 +398,7 @@ export class GitQuickOpenService { if (!repository) { return; } - return this.withProgress(async () => { + return this.withProgress(async () => { const list = await this.git.stash(repository, { action: 'list' }); if (list) { const quickOpenItems = list.map(stash => new GitQuickOpenItem(stash, this.wrapWithProgress(async () => { @@ -451,7 +450,7 @@ export class GitQuickOpenService { if (!repository) { return; } - return this.withProgress(async () => { + return this.withProgress(async () => { try { await this.git.stash(repository, { action: 'apply' @@ -467,7 +466,7 @@ export class GitQuickOpenService { if (!repository) { return; } - return this.withProgress(async () => { + return this.withProgress(async () => { try { await this.git.stash(repository, { action: 'pop' @@ -485,17 +484,17 @@ export class GitQuickOpenService { const items = wsRoots.map>(root => this.toRepositoryPathQuickOpenItem(root)); this.open(items, placeholder); } else { - const rootUri = new URI(wsRoots[0].uri); + const rootUri = wsRoots[0].resource; this.doInitRepository(rootUri.toString()); } } private async doInitRepository(uri: string): Promise { - this.withProgress(async () => this.git.exec({localUri: uri}, ['init'])); + this.withProgress(async () => this.git.exec({ localUri: uri }, ['init'])); } private toRepositoryPathQuickOpenItem(root: FileStat): GitQuickOpenItem { - const rootUri = new URI(root.uri); + const rootUri = root.resource; const toLabel = (item: GitQuickOpenItem) => this.labelProvider.getName(item.ref); const toDescription = (item: GitQuickOpenItem) => this.labelProvider.getLongName(item.ref.parent); const execute = async (item: GitQuickOpenItem) => { @@ -535,7 +534,7 @@ export class GitQuickOpenService { if (!repository) { return []; } - return this.withProgress(async () => { + return this.withProgress(async () => { try { return await this.git.remote(repository, { verbose: true }); } catch (error) { @@ -549,7 +548,7 @@ export class GitQuickOpenService { if (!repository) { return []; } - return this.withProgress(async () => { + return this.withProgress(async () => { const result = await this.git.exec(repository, ['tag', '--sort=-creatordate']); return result.stdout !== '' ? result.stdout.trim().split('\n').map(tag => ({ name: tag })) : []; }); @@ -559,7 +558,7 @@ export class GitQuickOpenService { if (!repository) { return []; } - return this.withProgress(async () => { + return this.withProgress(async () => { try { const [local, remote] = await Promise.all([ this.git.branch(repository, { type: 'local' }), @@ -577,7 +576,7 @@ export class GitQuickOpenService { if (!repository) { return undefined; } - return this.withProgress(async () => { + return this.withProgress(async () => { try { return await this.git.branch(repository, { type: 'current' }); } catch (error) { diff --git a/packages/git/src/browser/git-repository-provider.spec.ts b/packages/git/src/browser/git-repository-provider.spec.ts index 6414b062cd8bf..7e82c8d356273 100644 --- a/packages/git/src/browser/git-repository-provider.spec.ts +++ b/packages/git/src/browser/git-repository-provider.spec.ts @@ -21,10 +21,7 @@ import { Container } from 'inversify'; import { Git, Repository } from '../common'; import { DugiteGit } from '../node/dugite-git'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; -import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; -import { FileSystemNode } from '@theia/filesystem/lib/node/node-filesystem'; -import { FileChange } from '@theia/filesystem/lib/browser'; +import { FileStat, FileChangesEvent } from '@theia/filesystem/lib/common/files'; import { Emitter, CommandService } from '@theia/core'; import { LocalStorageService, StorageService, LabelProvider } from '@theia/core/lib/browser'; import { GitRepositoryProvider } from './git-repository-provider'; @@ -40,29 +37,22 @@ import { EditorManager } from '@theia/editor/lib/browser'; import { GitErrorHandler } from './git-error-handler'; import { GitPreferences } from './git-preferences'; import { GitRepositoryTracker } from './git-repository-tracker'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; const expect = chai.expect; disableJSDOM(); -const folderA = { - uri: 'file:///home/repoA', - lastModification: 0, - isDirectory: true -}; +const folderA = FileStat.dir('file:///home/repoA'); const repoA1 = { - localUri: `${folderA.uri}/1` + localUri: `${folderA.resource.toString()}/1` }; const repoA2 = { - localUri: `${folderA.uri}/2` + localUri: `${folderA.resource.toString()}/2` }; -const folderB = { - uri: 'file:///home/repoB', - lastModification: 0, - isDirectory: true -}; +const folderB = FileStat.dir('file:///home/repoB'); const repoB = { - localUri: folderB.uri + localUri: folderB.resource.toString() }; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -71,14 +61,13 @@ describe('GitRepositoryProvider', () => { let mockGit: DugiteGit; let mockWorkspaceService: WorkspaceService; - let mockFilesystem: FileSystem; - let mockFileSystemWatcher: FileSystemWatcher; + let mockFilesystem: FileService; let mockStorageService: StorageService; let mockGitRepositoryTracker: GitRepositoryTracker; let gitRepositoryProvider: GitRepositoryProvider; const mockRootChangeEmitter: Emitter = new Emitter(); - const mockFileChangeEmitter: Emitter = new Emitter(); + const mockFileChangeEmitter: Emitter = new Emitter(); before(() => { disableJSDOM = enableJSDOM(); @@ -90,8 +79,7 @@ describe('GitRepositoryProvider', () => { beforeEach(() => { mockGit = sinon.createStubInstance(DugiteGit); mockWorkspaceService = sinon.createStubInstance(WorkspaceService); - mockFilesystem = sinon.createStubInstance(FileSystemNode); - mockFileSystemWatcher = sinon.createStubInstance(FileSystemWatcher); + mockFilesystem = sinon.createStubInstance(FileService); mockStorageService = sinon.createStubInstance(LocalStorageService); mockGitRepositoryTracker = sinon.createStubInstance(GitRepositoryTracker); @@ -99,8 +87,7 @@ describe('GitRepositoryProvider', () => { testContainer.bind(GitRepositoryProvider).toSelf().inSingletonScope(); testContainer.bind(Git).toConstantValue(mockGit); testContainer.bind(WorkspaceService).toConstantValue(mockWorkspaceService); - testContainer.bind(FileSystem).toConstantValue(mockFilesystem); - testContainer.bind(FileSystemWatcher).toConstantValue(mockFileSystemWatcher); + testContainer.bind(FileService).toConstantValue(mockFilesystem); testContainer.bind(StorageService).toConstantValue(mockStorageService); testContainer.bind(ScmService).toSelf().inSingletonScope(); testContainer.bind(GitScmProvider.Factory).toFactory(createGitScmProviderFactory); @@ -115,7 +102,7 @@ describe('GitRepositoryProvider', () => { testContainer.bind(GitRepositoryTracker).toConstantValue(mockGitRepositoryTracker); sinon.stub(mockWorkspaceService, 'onWorkspaceChanged').value(mockRootChangeEmitter.event); - sinon.stub(mockFileSystemWatcher, 'onFilesChanged').value(mockFileChangeEmitter.event); + sinon.stub(mockFilesystem, 'onDidFilesChange').value(mockFileChangeEmitter.event); }); it('should adds all existing git repo(s) on theia loads', async () => { @@ -127,7 +114,7 @@ describe('GitRepositoryProvider', () => { (mockWorkspaceService.tryGetRoots).returns(roots); gitRepositoryProvider = testContainer.get(GitRepositoryProvider); (mockFilesystem.exists).resolves(true); - (mockGit.repositories).withArgs(folderA.uri, {}).resolves(allRepos); + (mockGit.repositories).withArgs(folderA.resource.toString(), {}).resolves(allRepos); await gitRepositoryProvider['initialize'](); expect(gitRepositoryProvider.allRepositories.length).to.eq(allRepos.length); @@ -148,8 +135,8 @@ describe('GitRepositoryProvider', () => { stubWsRoots.returns(oldRoots); gitRepositoryProvider = testContainer.get(GitRepositoryProvider); (mockFilesystem.exists).resolves(true); - (mockGit.repositories).withArgs(folderA.uri, {}).resolves(allReposA); - (mockGit.repositories).withArgs(folderB.uri, {}).resolves(allReposB); + (mockGit.repositories).withArgs(folderA.resource.toString(), {}).resolves(allReposA); + (mockGit.repositories).withArgs(folderB.resource.toString(), {}).resolves(allReposB); let counter = 0; gitRepositoryProvider.onDidChangeRepository(selected => { @@ -190,8 +177,8 @@ describe('GitRepositoryProvider', () => { stubWsRoots.onCall(2).returns(newRoots); gitRepositoryProvider = testContainer.get(GitRepositoryProvider); (mockFilesystem.exists).resolves(true); - (mockGit.repositories).withArgs(folderA.uri, {}).resolves(allReposA); - (mockGit.repositories).withArgs(folderB.uri, {}).resolves(allReposB); + (mockGit.repositories).withArgs(folderA.resource.toString(), {}).resolves(allReposA); + (mockGit.repositories).withArgs(folderB.resource.toString(), {}).resolves(allReposB); let counter = 0; gitRepositoryProvider.onDidChangeRepository(selected => { @@ -206,7 +193,7 @@ describe('GitRepositoryProvider', () => { } }); gitRepositoryProvider['initialize']().then(() => - mockFileChangeEmitter.fire([]) + mockFileChangeEmitter.fire(new FileChangesEvent([])) ).catch(e => done(new Error('gitRepositoryProvider.initialize() throws an error')) ); @@ -221,9 +208,9 @@ describe('GitRepositoryProvider', () => { sinon.stub(mockWorkspaceService, 'roots').value(Promise.resolve(roots)); (mockWorkspaceService.tryGetRoots).returns(roots); gitRepositoryProvider = testContainer.get(GitRepositoryProvider); - (mockFilesystem.exists).withArgs(folderA.uri).resolves(true); // folderA exists - (mockFilesystem.exists).withArgs(folderB.uri).resolves(false); // folderB does not exist - (mockGit.repositories).withArgs(folderA.uri, {}).resolves(allReposA); + (mockFilesystem.exists).withArgs(folderA.resource.toString()).resolves(true); // folderA exists + (mockFilesystem.exists).withArgs(folderB.resource.toString()).resolves(false); // folderB does not exist + (mockGit.repositories).withArgs(folderA.resource.toString(), {}).resolves(allReposA); await gitRepositoryProvider['initialize'](); expect(gitRepositoryProvider.allRepositories.length).to.eq(allReposA.length); @@ -242,10 +229,10 @@ describe('GitRepositoryProvider', () => { (mockWorkspaceService.tryGetRoots).returns(roots); gitRepositoryProvider = testContainer.get(GitRepositoryProvider); (mockFilesystem.exists).resolves(true); - (mockGit.repositories).withArgs(folderA.uri, {}).resolves(allReposA); - (mockGit.repositories).withArgs(folderA.uri, { maxCount: 1 }).resolves([allReposA[0]]); - (mockGit.repositories).withArgs(folderB.uri, {}).resolves(allReposB); - (mockGit.repositories).withArgs(folderB.uri, { maxCount: 1 }).resolves([allReposB[0]]); + (mockGit.repositories).withArgs(folderA.resource.toString(), {}).resolves(allReposA); + (mockGit.repositories).withArgs(folderA.resource.toString(), { maxCount: 1 }).resolves([allReposA[0]]); + (mockGit.repositories).withArgs(folderB.resource.toString(), {}).resolves(allReposB); + (mockGit.repositories).withArgs(folderB.resource.toString(), { maxCount: 1 }).resolves([allReposB[0]]); await gitRepositoryProvider['initialize'](); expect(gitRepositoryProvider.selectedRepository && gitRepositoryProvider.selectedRepository.localUri).to.eq(allReposA[0].localUri); diff --git a/packages/git/src/browser/git-repository-provider.ts b/packages/git/src/browser/git-repository-provider.ts index 9b2c46c430e89..32a83cdeadc62 100644 --- a/packages/git/src/browser/git-repository-provider.ts +++ b/packages/git/src/browser/git-repository-provider.ts @@ -16,17 +16,16 @@ import debounce = require('lodash.debounce'); -import { injectable, inject } from 'inversify'; +import { injectable, inject, postConstruct } from 'inversify'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { StorageService } from '@theia/core/lib/browser/storage-service'; -import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { Git, Repository } from '../common'; import { GitCommitMessageValidator } from './git-commit-message-validator'; import { GitScmProvider } from './git-scm-provider'; import { ScmService } from '@theia/scm/lib/browser/scm-service'; import { ScmRepository } from '@theia/scm/lib/browser/scm-repository'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export interface GitRefreshOptions { readonly maxCount: number @@ -45,17 +44,15 @@ export class GitRepositoryProvider { @inject(GitCommitMessageValidator) protected readonly commitMessageValidator: GitCommitMessageValidator; - constructor( - @inject(Git) protected readonly git: Git, - @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, - @inject(FileSystemWatcher) protected readonly watcher: FileSystemWatcher, - @inject(FileSystem) protected readonly fileSystem: FileSystem, - @inject(ScmService) protected readonly scmService: ScmService, - @inject(StorageService) protected readonly storageService: StorageService - ) { - this.initialize(); - } + @inject(Git) protected readonly git: Git; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(ScmService) protected readonly scmService: ScmService; + @inject(StorageService) protected readonly storageService: StorageService; + + @inject(FileService) + protected readonly fileService: FileService; + @postConstruct() protected async initialize(): Promise { const [selectedRepository, allRepositories] = await Promise.all([ this.storageService.getData(this.selectedRepoStorageKey), @@ -71,7 +68,7 @@ export class GitRepositoryProvider { this.selectedRepository = selectedRepository; await this.refresh(); - this.watcher.onFilesChanged(_changedFiles => this.lazyRefresh()); + this.fileService.onDidFilesChange(_ => this.lazyRefresh()); } protected lazyRefresh: () => Promise = debounce(() => this.refresh(), 1000); @@ -131,7 +128,7 @@ export class GitRepositoryProvider { const repositories: Repository[] = []; const refreshing: Promise[] = []; for (const root of await this.workspaceService.roots) { - refreshing.push(this.git.repositories(root.uri, { ...options }).then( + refreshing.push(this.git.repositories(root.resource.toString(), { ...options }).then( result => { repositories.push(...result); }, () => { /* no-op*/ } )); diff --git a/packages/git/src/browser/git-scm-provider.ts b/packages/git/src/browser/git-scm-provider.ts index a86828d029a52..8ff6e810a653d 100644 --- a/packages/git/src/browser/git-scm-provider.ts +++ b/packages/git/src/browser/git-scm-provider.ts @@ -22,7 +22,6 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { CommandService } from '@theia/core/lib/common/command'; import { ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { EditorOpenerOptions, EditorManager } from '@theia/editor/lib/browser/editor-manager'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { WorkspaceCommands } from '@theia/workspace/lib/browser'; import { Repository, Git, CommitWithChanges, GitFileChange, WorkingDirectoryStatus, GitFileStatus } from '../common'; import { GIT_RESOURCE_SCHEME } from './git-resource'; @@ -32,6 +31,7 @@ import { ScmProvider, ScmCommand, ScmResourceGroup, ScmAmendSupport, ScmCommit } import { ScmHistoryCommit, ScmFileChange } from '@theia/scm-extra/lib/browser/scm-file-change-node'; import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { GitCommitDetailWidgetOptions } from './history/git-commit-detail-widget'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; @injectable() export class GitScmProviderOptions { @@ -61,8 +61,8 @@ export class GitScmProvider implements ScmProvider { @inject(GitErrorHandler) protected readonly gitErrorHandler: GitErrorHandler; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; @inject(Git) protected readonly git: Git; @@ -207,13 +207,13 @@ export class GitScmProvider implements ScmProvider { return DiffUris.encode( changeUri.withScheme(GIT_RESOURCE_SCHEME).withQuery('HEAD'), changeUri.withScheme(GIT_RESOURCE_SCHEME), - changeUri.displayName + ' (Index)'); + this.labelProvider.getName(changeUri) + ' (Index)'); } if (this.stagedChanges.find(c => c.uri === change.uri)) { return DiffUris.encode( changeUri.withScheme(GIT_RESOURCE_SCHEME), changeUri, - changeUri.displayName + ' (Working tree)'); + this.labelProvider.getName(changeUri) + ' (Working tree)'); } if (this.mergeChanges.find(c => c.uri === change.uri)) { return changeUri; @@ -221,7 +221,7 @@ export class GitScmProvider implements ScmProvider { return DiffUris.encode( changeUri.withScheme(GIT_RESOURCE_SCHEME).withQuery('HEAD'), changeUri, - changeUri.displayName + ' (Working tree)'); + this.labelProvider.getName(changeUri) + ' (Working tree)'); } if (change.staged) { return changeUri.withScheme(GIT_RESOURCE_SCHEME); @@ -230,7 +230,7 @@ export class GitScmProvider implements ScmProvider { return DiffUris.encode( changeUri.withScheme(GIT_RESOURCE_SCHEME), changeUri, - changeUri.displayName + ' (Working tree)'); + this.labelProvider.getName(changeUri) + ' (Working tree)'); } return changeUri; } @@ -265,14 +265,14 @@ export class GitScmProvider implements ScmProvider { async stage(uriArg: string | string[]): Promise { try { const { repository, unstagedChanges, mergeChanges } = this; - const uris = Array.isArray(uriArg) ? uriArg : [ uriArg ]; + const uris = Array.isArray(uriArg) ? uriArg : [uriArg]; const unstagedUris = uris .filter(uri => { const resourceUri = new URI(uri); return unstagedChanges.some(change => resourceUri.isEqualOrParent(new URI(change.uri))) || mergeChanges.some(change => resourceUri.isEqualOrParent(new URI(change.uri))); } - ); + ); if (unstagedUris.length !== 0) { // TODO resolve deletion conflicts // TODO confirm staging of a unresolved file @@ -295,13 +295,13 @@ export class GitScmProvider implements ScmProvider { async unstage(uriArg: string | string[]): Promise { try { const { repository, stagedChanges } = this; - const uris = Array.isArray(uriArg) ? uriArg : [ uriArg ]; + const uris = Array.isArray(uriArg) ? uriArg : [uriArg]; const stagedUris = uris .filter(uri => { const resourceUri = new URI(uri); return stagedChanges.some(change => resourceUri.isEqualOrParent(new URI(change.uri))); } - ); + ); if (stagedUris.length !== 0) { await this.git.unstage(repository, uris); } @@ -326,7 +326,7 @@ export class GitScmProvider implements ScmProvider { } async discard(uriArg: string | string[]): Promise { const { repository } = this; - const uris = Array.isArray(uriArg) ? uriArg : [ uriArg ]; + const uris = Array.isArray(uriArg) ? uriArg : [uriArg]; const status = this.getStatus(); if (!status) { @@ -359,15 +359,15 @@ export class GitScmProvider implements ScmProvider { await Promise.all( pairs.map(pair => { const discardSingle = async () => { - if (pair.isInIndex) { - try { - await this.git.unstage(repository, pair.uri, { treeish: 'HEAD', reset: 'working-tree' }); - } catch (error) { - this.gitErrorHandler.handleError(error); + if (pair.isInIndex) { + try { + await this.git.unstage(repository, pair.uri, { treeish: 'HEAD', reset: 'working-tree' }); + } catch (error) { + this.gitErrorHandler.handleError(error); + } + } else { + await this.commands.executeCommand(WorkspaceCommands.FILE_DELETE.id, new URI(pair.uri)); } - } else { - await this.commands.executeCommand(WorkspaceCommands.FILE_DELETE.id, new URI(pair.uri)); - } }; return discardSingle(); }) @@ -377,7 +377,7 @@ export class GitScmProvider implements ScmProvider { protected confirm(paths: string[]): Promise { let fileText: string; if (paths.length <= 3) { - fileText = paths.map(path => new URI(path).displayName).join(', '); + fileText = paths.map(path => this.labelProvider.getName(new URI(path))).join(', '); } else { fileText = `${paths.length} files`; } @@ -396,7 +396,7 @@ export class GitScmProvider implements ScmProvider { protected async delete(uri: URI): Promise { try { - await this.fileSystem.delete(uri.toString()); + await this.fileService.delete(uri, { recursive: true }); } catch (e) { console.error(e); } diff --git a/packages/git/src/electron-node/askpass/askpass.ts b/packages/git/src/electron-node/askpass/askpass.ts index c61d9b6a0e76a..11a6333b2a706 100644 --- a/packages/git/src/electron-node/askpass/askpass.ts +++ b/packages/git/src/electron-node/askpass/askpass.ts @@ -99,7 +99,7 @@ export class Askpass implements Disposable { } } - protected onRequest(req: http.ServerRequest, res: http.ServerResponse): void { + protected onRequest(req: http.IncomingMessage, res: http.ServerResponse): void { const chunks: string[] = []; req.setEncoding('utf8'); req.on('data', (d: string) => chunks.push(d)); diff --git a/packages/keymaps/src/browser/keymaps-service.ts b/packages/keymaps/src/browser/keymaps-service.ts index 9661add59ce96..2bd3693afbdb2 100644 --- a/packages/keymaps/src/browser/keymaps-service.ts +++ b/packages/keymaps/src/browser/keymaps-service.ts @@ -16,7 +16,7 @@ import { inject, injectable, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; -import { ResourceProvider, Resource } from '@theia/core/lib/common/resource'; +import { ResourceProvider, Resource, ResourceError } from '@theia/core/lib/common/resource'; import { OpenerService, open, WidgetOpenerOptions, Widget } from '@theia/core/lib/browser'; import { KeybindingRegistry, KeybindingScope } from '@theia/core/lib/browser/keybinding'; import { Keybinding } from '@theia/core/lib/common/keybinding'; @@ -67,7 +67,7 @@ export class KeymapsService { * Parsed the read keybindings. */ protected async parseKeybindings(): Promise { - const content = await this.resource.readContents(); + const content = await this.readContents(); const keybindings: Keybinding[] = []; const json = jsoncparser.parse(content, undefined, { disallowComments: false }); if (Array.isArray(json)) { @@ -80,6 +80,17 @@ export class KeymapsService { return keybindings; } + protected async readContents(): Promise { + try { + return await this.resource.readContents(); + } catch (e) { + if (ResourceError.NotFound.is(e)) { + return ''; + } + throw e; + } + } + /** * Open the keybindings widget. * @param ref the optional reference for opening the widget. diff --git a/packages/markers/src/browser/marker-manager.ts b/packages/markers/src/browser/marker-manager.ts index 312909866a914..7e3129239c40e 100644 --- a/packages/markers/src/browser/marker-manager.ts +++ b/packages/markers/src/browser/marker-manager.ts @@ -17,8 +17,9 @@ import { injectable, inject, postConstruct } from 'inversify'; import { Event, Emitter } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; -import { FileSystemWatcher, FileChangeEvent, FileChangeType } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { Marker } from '../common/marker'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileChangesEvent, FileChangeType } from '@theia/filesystem/lib/common/files'; /* * argument to the `findMarkers` method. @@ -115,22 +116,22 @@ export abstract class MarkerManager { protected readonly uri2MarkerCollection = new Map>(); protected readonly onDidChangeMarkersEmitter = new Emitter(); - @inject(FileSystemWatcher) protected fileWatcher: FileSystemWatcher; + @inject(FileService) + protected readonly fileService: FileService; @postConstruct() protected init(): void { - this.fileWatcher.onFilesChanged(event => { - const relevantEvent = event.filter(({ type }) => type === FileChangeType.DELETED); - if (relevantEvent.length) { - this.cleanMarkers(relevantEvent); + this.fileService.onDidFilesChange(event => { + if (event.gotDeleted()) { + this.cleanMarkers(event); } }); } - protected cleanMarkers(event: FileChangeEvent): void { + protected cleanMarkers(event: FileChangesEvent): void { for (const uriString of this.uri2MarkerCollection.keys()) { const uri = new URI(uriString); - if (FileChangeEvent.isDeleted(event, uri)) { + if (event.contains(uri, FileChangeType.DELETED)) { this.cleanAllMarkers(uri); } } diff --git a/packages/markers/src/browser/marker-tree-label-provider.spec.ts b/packages/markers/src/browser/marker-tree-label-provider.spec.ts index 49b8b93171924..540870ce9704f 100644 --- a/packages/markers/src/browser/marker-tree-label-provider.spec.ts +++ b/packages/markers/src/browser/marker-tree-label-provider.spec.ts @@ -22,7 +22,6 @@ import URI from '@theia/core/lib/common/uri'; import { expect } from 'chai'; import { Container } from 'inversify'; import { ContributionProvider, Event } from '@theia/core/lib/common'; -import { FileStat, FileSystem } from '@theia/filesystem/lib/common'; import { LabelProvider, LabelProviderContribution, DefaultUriLabelProviderContribution, ApplicationShell, WidgetManager } from '@theia/core/lib/browser'; import { MarkerInfoNode } from './marker-tree'; import { MarkerTreeLabelProvider } from './marker-tree-label-provider'; @@ -31,7 +30,8 @@ import { TreeLabelProvider } from '@theia/core/lib/browser/tree/tree-label-provi import { WorkspaceService } from '@theia/workspace/lib/browser'; import { WorkspaceUriLabelProviderContribution } from '@theia/workspace/lib/browser/workspace-uri-contribution'; import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution'; -import { MockFilesystem } from '@theia/filesystem/lib/common/test/mock-filesystem'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat } from '@theia/filesystem/lib/common/files'; disableJSDOM(); @@ -54,7 +54,7 @@ before(() => { onDidCreateWidget: Event.None // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - testContainer.bind(FileSystem).to(MockFilesystem).inSingletonScope(); + testContainer.bind(FileService).toConstantValue({}); testContainer.bind(DefaultUriLabelProviderContribution).toSelf().inSingletonScope(); testContainer.bind(WorkspaceUriLabelProviderContribution).toSelf().inSingletonScope(); @@ -95,11 +95,7 @@ describe('Marker Tree Label Provider', () => { describe('getLongName', () => { describe('single-root workspace', () => { beforeEach(() => { - const root = { - uri: 'file:///home/a', - lastModification: 0, - isDirectory: true - }; + const root = FileStat.dir('file:///home/a'); workspaceService['_workspace'] = root; workspaceService['_roots'] = [root]; }); @@ -131,21 +127,9 @@ describe('Marker Tree Label Provider', () => { describe('multi-root workspace', () => { beforeEach(() => { const uri: string = 'file:///file'; - const file = { - uri: uri, - lastModification: 0, - isDirectory: false - }; - const root1 = { - uri: 'file:///root1', - lastModification: 0, - isDirectory: true - }; - const root2 = { - uri: 'file:///root2', - lastModification: 0, - isDirectory: true - }; + const file = FileStat.file(uri); + const root1 = FileStat.dir('file:///root1'); + const root2 = FileStat.dir('file:///root2'); workspaceService['_workspace'] = file; workspaceService['_roots'] = [root1, root2]; }); @@ -199,11 +183,7 @@ describe('Marker Tree Label Provider', () => { describe('#getDescription', () => { beforeEach(() => { - const root = { - uri: 'file:///home/a', - lastModification: 0, - isDirectory: true - }; + const root = FileStat.dir('file:///home/a'); workspaceService['_workspace'] = root; workspaceService['_roots'] = [root]; }); diff --git a/packages/markers/src/browser/problem/problem-manager.spec.ts b/packages/markers/src/browser/problem/problem-manager.spec.ts index f04a08c532baf..60fc10a330eb2 100644 --- a/packages/markers/src/browser/problem/problem-manager.spec.ts +++ b/packages/markers/src/browser/problem/problem-manager.spec.ts @@ -26,7 +26,7 @@ import { LocalStorageService, StorageService } from '@theia/core/lib/browser/sto import { Event } from '@theia/core/lib/common/event'; import { ILogger } from '@theia/core/lib/common/logger'; import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; -import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; disableJSDOM(); @@ -39,10 +39,9 @@ before(() => { testContainer.bind(ILogger).to(MockLogger); testContainer.bind(StorageService).to(LocalStorageService).inSingletonScope(); testContainer.bind(LocalStorageService).toSelf().inSingletonScope(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - testContainer.bind(FileSystemWatcher).toConstantValue({ - onFilesChanged: Event.None - } as FileSystemWatcher); + testContainer.bind(FileService).toConstantValue({ + onDidFilesChange: Event.None + }); testContainer.bind(ProblemManager).toSelf(); manager = testContainer.get(ProblemManager); diff --git a/packages/markers/src/browser/problem/problem-tree-model.spec.ts b/packages/markers/src/browser/problem/problem-tree-model.spec.ts index 72e33b7186f08..6271d1bc4e95c 100644 --- a/packages/markers/src/browser/problem/problem-tree-model.spec.ts +++ b/packages/markers/src/browser/problem/problem-tree-model.spec.ts @@ -22,13 +22,13 @@ import { expect } from 'chai'; import { Container } from 'inversify'; import { Diagnostic, Range, DiagnosticSeverity } from 'vscode-languageserver-types'; import { Event } from '@theia/core/lib/common/event'; -import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { Marker } from '../../common/marker'; import { MarkerManager } from '../marker-manager'; import { MarkerNode, MarkerOptions } from '../marker-tree'; import { PROBLEM_OPTIONS } from './problem-container'; import { ProblemManager } from './problem-manager'; import { ProblemTree } from './problem-tree-model'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; disableJSDOM(); @@ -41,9 +41,9 @@ before(() => { testContainer.bind(MarkerManager).toSelf().inSingletonScope(); testContainer.bind(ProblemManager).toSelf(); testContainer.bind(MarkerOptions).toConstantValue(PROBLEM_OPTIONS); - testContainer.bind(FileSystemWatcher).toConstantValue({ - onFilesChanged: Event.None - } as FileSystemWatcher); + testContainer.bind(FileService).toConstantValue({ + onDidFilesChange: Event.None + }); testContainer.bind(ProblemTree).toSelf().inSingletonScope(); problemTree = testContainer.get(ProblemTree); diff --git a/packages/mini-browser/src/browser/mini-browser-content.ts b/packages/mini-browser/src/browser/mini-browser-content.ts index 8aaefbf3d2086..713c50e103da7 100644 --- a/packages/mini-browser/src/browser/mini-browser-content.ts +++ b/packages/mini-browser/src/browser/mini-browser-content.ts @@ -20,11 +20,9 @@ import { Message } from '@phosphor/messaging'; import URI from '@theia/core/lib/common/uri'; import { ILogger } from '@theia/core/lib/common/logger'; import { Emitter } from '@theia/core/lib/common/event'; -import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { parseCssTime, Key, KeyCode } from '@theia/core/lib/browser'; -import { FileSystemWatcher, FileChangeEvent } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; import { BaseWidget, addEventListener } from '@theia/core/lib/browser/widgets/widget'; import { LocationMapperService } from './location-mapper-service'; @@ -32,6 +30,8 @@ import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/appl import debounce = require('lodash.debounce'); import { MiniBrowserContentStyle } from './mini-browser-content-style'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileChangesEvent, FileChangeType } from '@theia/filesystem/lib/common/files'; /** * Initializer properties for the embedded browser widget. @@ -177,11 +177,8 @@ export class MiniBrowserContent extends BaseWidget { @inject(ApplicationShellMouseTracker) protected readonly mouseTracker: ApplicationShellMouseTracker; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; - - @inject(FileSystemWatcher) - protected readonly fileSystemWatcher: FileSystemWatcher; + @inject(FileService) + protected readonly fileService: FileService; protected readonly submitInputEmitter = new Emitter(); protected readonly navigateBackEmitter = new Emitter(); @@ -253,20 +250,18 @@ export class MiniBrowserContent extends BaseWidget { } protected async listenOnContentChange(location: string): Promise { - if (location.startsWith('file://')) { - if (await this.fileSystem.exists(location)) { - const fileUri = new URI(location); - const watcher = await this.fileSystemWatcher.watchFileChanges(fileUri); - this.toDispose.push(watcher); - const onFileChange = (event: FileChangeEvent) => { - if (FileChangeEvent.isChanged(event, fileUri)) { - this.go(location, { - showLoadIndicator: false - }); - } - }; - this.toDispose.push(this.fileSystemWatcher.onFilesChanged(debounce(onFileChange, 500))); - } + if (await this.fileService.exists(new URI(location))) { + const fileUri = new URI(location); + const watcher = this.fileService.watch(fileUri); + this.toDispose.push(watcher); + const onFileChange = (event: FileChangesEvent) => { + if (event.contains(fileUri, FileChangeType.ADDED) || event.contains(fileUri, FileChangeType.UPDATED)) { + this.go(location, { + showLoadIndicator: false + }); + } + }; + this.toDispose.push(this.fileService.onDidFilesChange(debounce(onFileChange, 500))); } } diff --git a/packages/mini-browser/src/node/mini-browser-endpoint.ts b/packages/mini-browser/src/node/mini-browser-endpoint.ts index 128a43d90161c..101a922503a3f 100644 --- a/packages/mini-browser/src/node/mini-browser-endpoint.ts +++ b/packages/mini-browser/src/node/mini-browser-endpoint.ts @@ -22,7 +22,6 @@ import URI from '@theia/core/lib/common/uri'; import { FileUri } from '@theia/core/lib/node/file-uri'; import { ILogger } from '@theia/core/lib/common/logger'; import { MaybePromise } from '@theia/core/lib/common/types'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; import { MiniBrowserService } from '../common/mini-browser-service'; @@ -35,7 +34,7 @@ export interface FileStatWithContent { /** * The file stat. */ - readonly stat: FileStat; + readonly stat: fs.Stats & { uri: string }; /** * The content of the file as a UTF-8 encoded string. @@ -80,9 +79,6 @@ export class MiniBrowserEndpoint implements BackendApplicationContribution, Mini @inject(ILogger) protected readonly logger: ILogger; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; - @inject(ContributionProvider) @named(MiniBrowserEndpointHandler) protected readonly contributions: ContributionProvider; @@ -110,13 +106,13 @@ export class MiniBrowserEndpoint implements BackendApplicationContribution, Mini } protected async response(uri: string, response: Response): Promise { - const exists = await this.fileSystem.exists(uri); + const exists = await fs.pathExists(FileUri.fsPath(uri)); if (!exists) { return this.missingResourceHandler()(uri, response); } const statWithContent = await this.readContent(uri); try { - if (!statWithContent.stat.isDirectory) { + if (!statWithContent.stat.isDirectory()) { const extension = uri.split('.').pop(); if (!extension) { return this.defaultHandler()(statWithContent, response); @@ -143,7 +139,9 @@ export class MiniBrowserEndpoint implements BackendApplicationContribution, Mini } protected async readContent(uri: string): Promise { - return this.fileSystem.resolveContent(uri); + const fsPath = FileUri.fsPath(uri); + const [stat, content] = await Promise.all([fs.stat(fsPath), fs.readFile(fsPath, 'utf8')]); + return { stat: Object.assign(stat, { uri }), content }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -176,8 +174,8 @@ export class MiniBrowserEndpoint implements BackendApplicationContribution, Mini protected defaultHandler(): (statWithContent: FileStatWithContent, response: Response) => MaybePromise { return async (statWithContent: FileStatWithContent, response: Response) => { - const { stat, content } = statWithContent; - const mimeType = lookup(FileUri.fsPath(stat.uri)); + const { content } = statWithContent; + const mimeType = lookup(FileUri.fsPath(statWithContent.stat.uri)); if (!mimeType) { this.logger.warn(`Cannot handle unexpected resource. URI: ${statWithContent.stat.uri}.`); response.contentType('application/octet-stream'); diff --git a/packages/monaco/src/browser/monaco-bulk-edit-service.ts b/packages/monaco/src/browser/monaco-bulk-edit-service.ts index 5a248ea2bd900..1216d8fb9e1c9 100644 --- a/packages/monaco/src/browser/monaco-bulk-edit-service.ts +++ b/packages/monaco/src/browser/monaco-bulk-edit-service.ts @@ -23,7 +23,7 @@ export class MonacoBulkEditService implements monaco.editor.IBulkEditService { @inject(MonacoWorkspace) protected readonly workspace: MonacoWorkspace; - apply(edit: monaco.languages.WorkspaceEdit): Promise { + apply(edit: monaco.languages.WorkspaceEdit): Promise { return this.workspace.applyBulkEdit(edit); } diff --git a/packages/monaco/src/browser/monaco-editor-model.ts b/packages/monaco/src/browser/monaco-editor-model.ts index 62bb1a2e024c5..22e7fac12d522 100644 --- a/packages/monaco/src/browser/monaco-editor-model.ts +++ b/packages/monaco/src/browser/monaco-editor-model.ts @@ -16,7 +16,7 @@ import { Position } from 'vscode-languageserver-types'; import { TextDocumentSaveReason, TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; -import { TextEditorDocument } from '@theia/editor/lib/browser'; +import { TextEditorDocument, EncodingMode } from '@theia/editor/lib/browser'; import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { CancellationTokenSource, CancellationToken } from '@theia/core/lib/common/cancellation'; @@ -69,16 +69,18 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { protected readonly onDidChangeValidEmitter = new Emitter(); readonly onDidChangeValid = this.onDidChangeValidEmitter.event; - private preferredEncoding: string | undefined = undefined; - private readonly defaultEncoding: string | undefined; + protected readonly onDidChangeEncodingEmitter = new Emitter(); + readonly onDidChangeEncoding = this.onDidChangeEncodingEmitter.event; + + private preferredEncoding: string | undefined; + private contentEncoding: string | undefined; protected resourceVersion: ResourceVersion | undefined; constructor( protected readonly resource: Resource, protected readonly m2p: MonacoToProtocolConverter, - protected readonly p2m: ProtocolToMonacoConverter, - options?: { encoding?: string | undefined } + protected readonly p2m: ProtocolToMonacoConverter ) { this.toDispose.push(resource); this.toDispose.push(this.toDisposeOnAutoSave); @@ -89,10 +91,9 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { this.toDispose.push(this.onDidChangeValidEmitter); this.toDispose.push(Disposable.create(() => this.cancelSave())); this.toDispose.push(Disposable.create(() => this.cancelSync())); - this.defaultEncoding = options && options.encoding ? options.encoding : undefined; this.resolveModel = this.readContents().then( content => this.initialize(content || ''), - e => console.error(`Failed to initialize for '${this.uri}':`, e) + e => console.error(`Failed to initialize for '${this.resource.uri.toString()}':`, e) ); } @@ -100,24 +101,41 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { this.toDispose.dispose(); } - async reopenWithEncoding(encoding: string): Promise { - if (encoding === this.preferredEncoding || (!this.preferredEncoding && encoding === this.defaultEncoding)) { - return; + setEncoding(encoding: string, mode: EncodingMode): Promise { + if (mode === EncodingMode.Decode && this.dirty) { + return Promise.resolve(); } - if (this.dirty) { - return; + if (!this.setPreferredEncoding(encoding)) { + return Promise.resolve(); } - this.preferredEncoding = encoding; - return this.sync(); + if (mode === EncodingMode.Decode) { + return this.sync(); + } + return this.scheduleSave(TextDocumentSaveReason.Manual, this.cancelSave(), true); } - async saveWithEncoding(encoding: string): Promise { - return this.scheduleSave(TextDocumentSaveReason.Manual, this.cancelSave(), encoding) - .then(() => { this.preferredEncoding = encoding; }); + getEncoding(): string | undefined { + return this.preferredEncoding || this.contentEncoding; } - getEncoding(): string | undefined { - return this.preferredEncoding || this.defaultEncoding; + protected setPreferredEncoding(encoding: string): boolean { + if (encoding === this.preferredEncoding || (!this.preferredEncoding && encoding === this.contentEncoding)) { + return false; + } + this.preferredEncoding = encoding; + this.onDidChangeEncodingEmitter.fire(encoding); + return true; + } + + protected updateContentEncoding(): void { + const contentEncoding = this.resource.encoding; + if (!contentEncoding || this.contentEncoding === contentEncoding) { + return; + } + this.contentEncoding = contentEncoding; + if (!this.preferredEncoding) { + this.onDidChangeEncodingEmitter.fire(contentEncoding); + } } /** @@ -182,7 +200,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { } get uri(): string { - return this.model.uri.toString(); + return this.resource.uri.toString(); } protected _languageId: string | undefined; @@ -306,6 +324,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { protected async readContents(): Promise { try { const content = await this.resource.readContents({ encoding: this.getEncoding() }); + this.updateContentEncoding(); this.setValid(true); return content; } catch (e) { @@ -347,7 +366,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { return this.saveCancellationTokenSource.token; } - protected scheduleSave(reason: TextDocumentSaveReason, token: CancellationToken = this.cancelSave(), overwriteEncoding?: string): Promise { + protected scheduleSave(reason: TextDocumentSaveReason, token: CancellationToken = this.cancelSave(), overwriteEncoding?: boolean): Promise { return this.run(() => this.doSave(reason, token, overwriteEncoding)); } @@ -401,7 +420,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { } } - protected async doSave(reason: TextDocumentSaveReason, token: CancellationToken, overwriteEncoding?: string): Promise { + protected async doSave(reason: TextDocumentSaveReason, token: CancellationToken, overwriteEncoding?: boolean): Promise { if (token.isCancellationRequested || !this.resource.saveContents) { return; } @@ -412,7 +431,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { } const changes = [...this.contentChanges]; - if (changes.length === 0 && overwriteEncoding === undefined) { + if (changes.length === 0 && !overwriteEncoding && reason !== TextDocumentSaveReason.Manual) { return; } @@ -423,6 +442,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { await Resource.save(this.resource, { changes, content, options: { encoding, overwriteEncoding, version } }, token); this.contentChanges.splice(0, changes.length); this.resourceVersion = this.resource.version; + this.updateContentEncoding(); this.setValid(true); if (token.isCancellationRequested) { diff --git a/packages/monaco/src/browser/monaco-editor.ts b/packages/monaco/src/browser/monaco-editor.ts index bc020088c0aa9..c6a3cf602001c 100644 --- a/packages/monaco/src/browser/monaco-editor.ts +++ b/packages/monaco/src/browser/monaco-editor.ts @@ -40,6 +40,7 @@ import { MonacoEditorModel } from './monaco-editor-model'; import { MonacoToProtocolConverter } from './monaco-to-protocol-converter'; import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter'; import { TextEdit } from 'vscode-languageserver-types'; +import { UTF8 } from '@theia/core/lib/common/encodings'; import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions; import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration; @@ -82,8 +83,7 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { protected readonly onLanguageChangedEmitter = new Emitter(); readonly onLanguageChanged = this.onLanguageChangedEmitter.event; protected readonly onScrollChangedEmitter = new Emitter(); - protected readonly onEncodingChangedEmitter = new Emitter(); - readonly onEncodingChanged = this.onEncodingChangedEmitter.event; + readonly onEncodingChanged = this.document.onDidChangeEncoding; readonly documents = new Set(); @@ -103,8 +103,7 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { this.onDocumentContentChangedEmitter, this.onMouseDownEmitter, this.onLanguageChangedEmitter, - this.onScrollChangedEmitter, - this.onEncodingChangedEmitter + this.onScrollChangedEmitter ]); this.documents.add(document); this.autoSizing = options && options.autoSizing !== undefined ? options.autoSizing : false; @@ -115,19 +114,11 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor { } getEncoding(): string { - return this.document.getEncoding() || 'utf8'; - } - - setEncoding(encoding: string, mode: EncodingMode): void { - if (mode === EncodingMode.Decode) { - // reopen file with encoding - this.document.reopenWithEncoding(encoding) - .then(() => this.onEncodingChangedEmitter.fire(encoding)); - } else { - // encode and save file - this.document.saveWithEncoding(encoding) - .then(() => this.onEncodingChangedEmitter.fire(encoding)); - } + return this.document.getEncoding() || UTF8; + } + + setEncoding(encoding: string, mode: EncodingMode): Promise { + return this.document.setEncoding(encoding, mode); } protected create(options?: IStandaloneEditorConstructionOptions, override?: monaco.editor.IEditorOverrideServices): Disposable { diff --git a/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts b/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts index 3f79e4f86829e..f48a63ec9b456 100644 --- a/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts +++ b/packages/monaco/src/browser/monaco-snippet-suggest-provider.ts @@ -22,15 +22,16 @@ import * as jsoncparser from 'jsonc-parser'; import { injectable, inject } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { FileSystem, FileSystemError } from '@theia/filesystem/lib/common'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileOperationError } from '@theia/filesystem/lib/common/files'; @injectable() export class MonacoSnippetSuggestProvider implements monaco.languages.CompletionItemProvider { private static readonly _maxPrefix = 10000; - @inject(FileSystem) - protected readonly filesystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; protected readonly snippets = new Map(); protected readonly pendingSnippets = new Map[]>(); @@ -137,14 +138,15 @@ export class MonacoSnippetSuggestProvider implements monaco.languages.Completion */ protected async loadURI(uri: string | URI, options: SnippetLoadOptions, toDispose: DisposableCollection): Promise { try { - const { content } = await this.filesystem.resolveContent(uri.toString(), { encoding: 'utf-8' }); + const resource = typeof uri === 'string' ? new URI(uri) : uri; + const { value } = await this.fileService.read(resource); if (toDispose.disposed) { return; } - const snippets = content && jsoncparser.parse(content, undefined, { disallowComments: false }); + const snippets = value && jsoncparser.parse(value, undefined, { disallowComments: false }); toDispose.push(this.fromJSON(snippets, options)); } catch (e) { - if (!FileSystemError.FileNotFound.is(e) && !FileSystemError.FileIsDirectory.is(e)) { + if (!(e instanceof FileOperationError)) { console.error(e); } } diff --git a/packages/monaco/src/browser/monaco-text-model-service.ts b/packages/monaco/src/browser/monaco-text-model-service.ts index 95cf469d9c1b1..9668552af9efe 100644 --- a/packages/monaco/src/browser/monaco-text-model-service.ts +++ b/packages/monaco/src/browser/monaco-text-model-service.ts @@ -30,8 +30,7 @@ export interface MonacoEditorModelFactory { readonly scheme: string; createModel( - resource: Resource, - options?: { encoding?: string | undefined } + resource: Resource ): MaybePromise; } @@ -87,9 +86,8 @@ export class MonacoTextModelService implements monaco.editor.ITextModelService { } protected createModel(resource: Resource): MaybePromise { - const options = { encoding: this.editorPreferences.get('files.encoding') }; const factory = this.factories.getContributions().find(({ scheme }) => resource.uri.scheme === scheme); - return factory ? factory.createModel(resource, options) : new MonacoEditorModel(resource, this.m2p, this.p2m, options); + return factory ? factory.createModel(resource) : new MonacoEditorModel(resource, this.m2p, this.p2m); } protected readonly modelOptions: { [name: string]: (keyof monaco.editor.ITextModelUpdateOptions | undefined) } = { diff --git a/packages/monaco/src/browser/monaco-theming-service.ts b/packages/monaco/src/browser/monaco-theming-service.ts index 9a9cc03a696ab..666a2f3d3cabc 100644 --- a/packages/monaco/src/browser/monaco-theming-service.ts +++ b/packages/monaco/src/browser/monaco-theming-service.ts @@ -22,9 +22,9 @@ import * as plistparser from 'fast-plist'; import { ThemeService, BuiltinThemeProvider } from '@theia/core/lib/browser/theming'; import URI from '@theia/core/lib/common/uri'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; import { MonacoThemeRegistry } from './textmate/monaco-theme-registry'; import { getThemes, putTheme, MonacoThemeState } from './monaco-indexed-db'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export interface MonacoTheme { id?: string; @@ -58,10 +58,9 @@ export interface MonacoThemeJson { @injectable() export class MonacoThemingService { - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; - // eslint-disable-next-line @typescript-eslint/no-explicit-any register(theme: MonacoTheme, pending: { [uri: string]: Promise } = {}): Disposable { const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ })); this.doRegister(theme, pending, toDispose); @@ -92,8 +91,8 @@ export class MonacoThemingService { pending: { [uri: string]: Promise }, toDispose: DisposableCollection ): Promise { - /* eslint-enable @typescript-eslint/no-explicit-any */ - const { content } = await this.fileSystem.resolveContent(uri); + const result = await this.fileService.read(new URI(uri)); + const content = result.value; if (toDispose.disposed) { return; } @@ -122,7 +121,6 @@ export class MonacoThemingService { return json; } - /* eslint-disable @typescript-eslint/no-explicit-any */ protected doLoadTheme( themeUri: URI, referencedPath: string, @@ -136,7 +134,6 @@ export class MonacoThemingService { } return pending[referencedUri]; } - /* eslint-enable @typescript-eslint/no-explicit-any */ static init(): void { this.updateBodyUiTheme(); diff --git a/packages/monaco/src/browser/monaco-workspace.ts b/packages/monaco/src/browser/monaco-workspace.ts index 57c1090fc56f0..663fe777f70e3 100644 --- a/packages/monaco/src/browser/monaco-workspace.ts +++ b/packages/monaco/src/browser/monaco-workspace.ts @@ -20,19 +20,21 @@ import { URI as Uri } from 'vscode-uri'; import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { Emitter } from '@theia/core/lib/common/event'; -import { FileSystem } from '@theia/filesystem/lib/common'; +import { FileSystemPreferences } from '@theia/filesystem/lib/browser'; import { EditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser'; import { MonacoTextModelService } from './monaco-text-model-service'; import { WillSaveMonacoModelEvent, MonacoEditorModel, MonacoModelContentChangedEvent } from './monaco-editor-model'; import { MonacoEditor } from './monaco-editor'; import { ProblemManager } from '@theia/markers/lib/browser'; import { MaybePromise } from '@theia/core/lib/common/types'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files'; // Note: `newUri` and `oldUri` are both optional although it is mandatory in `monaco.languages.ResourceFileEdit`. // See: https://github.com/microsoft/monaco-editor/issues/1396 export interface ResourceEdit { - readonly newUri?: string; - readonly oldUri?: string; + readonly newUri?: URI; + readonly oldUri?: URI; readonly options?: { readonly overwrite?: boolean; readonly ignoreIfNotExists?: boolean; @@ -42,37 +44,33 @@ export interface ResourceEdit { } export interface CreateResourceEdit extends ResourceEdit { - readonly newUri: string; + readonly newUri: URI; } export namespace CreateResourceEdit { export function is(arg: Edit): arg is CreateResourceEdit { - return 'newUri' in arg - && typeof (arg as any).newUri === 'string' // eslint-disable-line @typescript-eslint/no-explicit-any - && (!('oldUri' in arg) || typeof (arg as any).oldUri === 'undefined'); // eslint-disable-line @typescript-eslint/no-explicit-any + return ('newUri' in arg && (arg as any).newUri instanceof URI) && // eslint-disable-line @typescript-eslint/no-explicit-any + !('oldUri' in arg && (arg as any).oldUri instanceof URI); // eslint-disable-line @typescript-eslint/no-explicit-any } } export interface DeleteResourceEdit extends ResourceEdit { - readonly oldUri: string; + readonly oldUri: URI; } export namespace DeleteResourceEdit { export function is(arg: Edit): arg is DeleteResourceEdit { - return 'oldUri' in arg - && typeof (arg as any).oldUri === 'string' // eslint-disable-line @typescript-eslint/no-explicit-any - && (!('newUri' in arg) || typeof (arg as any).newUri === 'undefined'); // eslint-disable-line @typescript-eslint/no-explicit-any + return !('newUri' in arg && (arg as any).newUri instanceof URI) && // eslint-disable-line @typescript-eslint/no-explicit-any + ('oldUri' in arg && (arg as any).oldUri instanceof URI); // eslint-disable-line @typescript-eslint/no-explicit-any } } export interface RenameResourceEdit extends ResourceEdit { - readonly newUri: string; - readonly oldUri: string; + readonly newUri: URI; + readonly oldUri: URI; } export namespace RenameResourceEdit { export function is(arg: Edit): arg is RenameResourceEdit { - return 'oldUri' in arg - && typeof (arg as any).oldUri === 'string' // eslint-disable-line @typescript-eslint/no-explicit-any - && 'newUri' in arg - && typeof (arg as any).newUri === 'string'; // eslint-disable-line @typescript-eslint/no-explicit-any + return ('newUri' in arg && (arg as any).newUri instanceof URI) && // eslint-disable-line @typescript-eslint/no-explicit-any + ('oldUri' in arg && (arg as any).oldUri instanceof URI); // eslint-disable-line @typescript-eslint/no-explicit-any } } @@ -138,8 +136,11 @@ export class MonacoWorkspace { protected readonly onDidSaveTextDocumentEmitter = new Emitter(); readonly onDidSaveTextDocument = this.onDidSaveTextDocumentEmitter.event; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; + + @inject(FileSystemPreferences) + protected readonly filePreferences: FileSystemPreferences; @inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService; @@ -253,7 +254,7 @@ export class MonacoWorkspace { }); } - async applyBulkEdit(workspaceEdit: monaco.languages.WorkspaceEdit, options?: EditorOpenerOptions): Promise { + async applyBulkEdit(workspaceEdit: monaco.languages.WorkspaceEdit, options?: EditorOpenerOptions): Promise { try { const unresolvedEdits = this.groupEdits(workspaceEdit); const edits = await this.openEditors(unresolvedEdits, options); @@ -285,11 +286,13 @@ export class MonacoWorkspace { } } const ariaSummary = this.getAriaSummary(totalEdits, totalFiles); - return { ariaSummary }; + return { ariaSummary, success: true }; } catch (e) { - const ariaSummary = `Error applying workspace edits: ${e.toString()}`; - console.error(ariaSummary); - return { ariaSummary }; + console.error('Failed to apply workspace edits:', e); + return { + ariaSummary: `Error applying workspace edits: ${e.toString()}`, + success: false + }; } } @@ -365,8 +368,8 @@ export class MonacoWorkspace { editorEdit.textEdits.push(...resourceTextEdit.edits); } else { const { options } = edit; - const oldUri = !!edit.oldUri ? edit.oldUri.toString() : undefined; - const newUri = !!edit.newUri ? edit.newUri.toString() : undefined; + const oldUri = !!edit.oldUri ? new URI(edit.oldUri.toString()) : undefined; + const newUri = !!edit.newUri ? new URI(edit.newUri.toString()) : undefined; result.push({ oldUri, newUri, @@ -381,33 +384,27 @@ export class MonacoWorkspace { const options = edit.options || {}; if (RenameResourceEdit.is(edit)) { // rename - if (options.overwrite === undefined && options.ignoreIfExists && await this.fileSystem.exists(edit.newUri)) { + if (options.overwrite === undefined && options.ignoreIfExists && await this.fileService.exists(edit.newUri)) { return; // not overwriting, but ignoring, and the target file exists } - await this.fileSystem.move(edit.oldUri, edit.newUri, { overwrite: options.overwrite }); + await this.fileService.move(edit.oldUri, edit.newUri, { overwrite: options.overwrite }); } else if (DeleteResourceEdit.is(edit)) { // delete file - if (!options.ignoreIfNotExists || await this.fileSystem.exists(edit.oldUri)) { - if (options.recursive === false) { - console.warn("Ignored 'recursive': 'false' option. Deleting recursively."); + if (await this.fileService.exists(edit.oldUri)) { + let useTrash = this.filePreferences['files.enableTrash']; + if (useTrash && !(this.fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) { + useTrash = false; // not supported by provider } - await this.fileSystem.delete(edit.oldUri); + await this.fileService.delete(edit.oldUri, { useTrash, recursive: options.recursive }); + } else if (!options.ignoreIfNotExists) { + throw new Error(`${edit.oldUri} does not exist and can not be deleted`); } } else if (CreateResourceEdit.is(edit)) { - const exists = await this.fileSystem.exists(edit.newUri); // create file - if (options.overwrite === undefined && options.ignoreIfExists && exists) { + if (options.overwrite === undefined && options.ignoreIfExists && await this.fileService.exists(edit.newUri)) { return; // not overwriting, but ignoring, and the target file exists } - if (exists && options.overwrite) { - const stat = await this.fileSystem.getFileStat(edit.newUri); - if (!stat) { - throw new Error(`Cannot get file stat for the resource: ${edit.newUri}.`); - } - await this.fileSystem.setContent(stat, ''); - } else { - await this.fileSystem.createFile(edit.newUri); - } + await this.fileService.create(edit.newUri, undefined, { overwrite: options.overwrite }); } } diff --git a/packages/navigator/src/browser/navigator-diff.spec.ts b/packages/navigator/src/browser/navigator-diff.spec.ts index 9f5dd3a3ff7ab..99aa6e561cf58 100644 --- a/packages/navigator/src/browser/navigator-diff.spec.ts +++ b/packages/navigator/src/browser/navigator-diff.spec.ts @@ -24,13 +24,13 @@ import { Container, ContainerModule } from 'inversify'; import { SelectionService, ILogger } from '@theia/core/lib/common'; import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; import URI from '@theia/core/lib/common/uri'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { OpenerService } from '@theia/core/lib/browser'; import { MockOpenerService } from '@theia/core/lib/browser/test/mock-opener-service'; import { MessageService } from '@theia/core/lib/common/message-service'; import { MessageClient } from '@theia/core/lib/common/message-service-protocol'; -import { FileSystemNode } from '@theia/filesystem/lib/node/node-filesystem'; import { FileUri } from '@theia/core/lib/node/file-uri'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { DiskFileSystemProvider } from '@theia/filesystem/lib/node/disk-file-system-provider'; disableJSDOM(); @@ -43,7 +43,10 @@ beforeEach(() => { bind(SelectionService).toSelf().inSingletonScope(); bind(NavigatorDiff).toSelf().inSingletonScope(); bind(OpenerService).to(MockOpenerService); - bind(FileSystem).to(FileSystemNode).inSingletonScope(); + const fileService = new FileService(); + fileService['resourceForError'] = (resource: URI) => resource.toString(); + fileService.registerProvider('file', new DiskFileSystemProvider()); + bind(FileService).toConstantValue(fileService); bind(MessageService).toSelf().inSingletonScope(); bind(MessageClient).toSelf().inSingletonScope(); }); @@ -52,30 +55,24 @@ beforeEach(() => { }); describe('NavigatorDiff', () => { - it('should allow a valid first file to be added', done => { + it('should allow a valid first file to be added', async () => { const diff = testContainer.get(NavigatorDiff); testContainer.get(SelectionService).selection = [{ uri: new URI(FileUri.create(path.resolve(__dirname, '../../test-resources/testFileA.json')).toString()) }]; - diff.addFirstComparisonFile() - .then(result => { - expect(result).to.be.true; - done(); - }); + const result = await diff.addFirstComparisonFile(); + expect(result).to.be.true; }); - it('should reject invalid file when added', done => { + it('should reject invalid file when added', async () => { const diff = testContainer.get(NavigatorDiff); testContainer.get(SelectionService).selection = [{ uri: new URI(FileUri.create(path.resolve(__dirname, '../../test-resources/nonExistentFile.json')).toString()) }]; - diff.addFirstComparisonFile() - .then(result => { - expect(result).to.be.false; - done(); - }); + const result = await diff.addFirstComparisonFile(); + expect(result).to.be.false; }); it('should run comparison when second file is added', done => { diff --git a/packages/navigator/src/browser/navigator-diff.ts b/packages/navigator/src/browser/navigator-diff.ts index 960cc228ecefc..28e00a72cb1c6 100644 --- a/packages/navigator/src/browser/navigator-diff.ts +++ b/packages/navigator/src/browser/navigator-diff.ts @@ -16,12 +16,13 @@ import { inject, injectable } from 'inversify'; import URI from '@theia/core/lib/common/uri'; -import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; import { SelectionService, UriSelection } from '@theia/core/lib/common'; import { OpenerService, open } from '@theia/core/lib/browser/opener-service'; import { MessageService } from '@theia/core/lib/common/message-service'; import { Command } from '@theia/core/lib/common/command'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileOperationError, FileOperationResult } from '@theia/filesystem/lib/common/files'; export namespace NavigatorDiffCommands { const COMPARE_CATEGORY = 'Compare'; @@ -39,8 +40,8 @@ export namespace NavigatorDiffCommands { @injectable() export class NavigatorDiff { - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; @inject(OpenerService) protected openerService: OpenerService; @@ -71,11 +72,12 @@ export class NavigatorDiff { protected async isDirectory(uri: URI): Promise { try { - const stat = await this.fileSystem.getFileStat(uri.path.toString()); - if (!stat || stat.isDirectory) { + const stat = await this.fileService.resolve(uri); + return stat.isDirectory; + } catch (e) { + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { return true; } - } catch (e) { } return false; diff --git a/packages/navigator/src/browser/navigator-model.spec.ts b/packages/navigator/src/browser/navigator-model.spec.ts deleted file mode 100644 index 25c2729675ef5..0000000000000 --- a/packages/navigator/src/browser/navigator-model.spec.ts +++ /dev/null @@ -1,327 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Ericsson and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; -let disableJSDOM = enableJSDOM(); - -import { Container } from 'inversify'; -import { Event, Emitter, ILogger, Logger } from '@theia/core'; -import { - CompositeTreeNode, DefaultOpenerService, ExpandableTreeNode, LabelProvider, OpenerService, - Tree, TreeNode, TreeSelectionService, TreeExpansionService, TreeExpansionServiceImpl, - TreeNavigationService, TreeSearch, CorePreferences -} from '@theia/core/lib/browser'; -import { TreeSelectionServiceImpl } from '@theia/core/lib/browser/tree/tree-selection-impl'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; -import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; -import { FileSystemNode } from '@theia/filesystem/lib/node/node-filesystem'; -import { DirNode, FileChange, FileMoveEvent, FileTreeModel, FileStatNode } from '@theia/filesystem/lib/browser'; -import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { FileNavigatorTree, WorkspaceNode, WorkspaceRootNode } from './navigator-tree'; -import { FileNavigatorModel } from './navigator-model'; -import { createMockPreferenceProxy } from '@theia/core/lib/browser/preferences/test'; -import { expect } from 'chai'; -import URI from '@theia/core/lib/common/uri'; -import * as sinon from 'sinon'; -import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -import { ProgressService } from '@theia/core/lib/common/progress-service'; - -disableJSDOM(); - -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable no-unused-expressions */ - -let root: CompositeTreeNode; -let workspaceRootFolder: DirNode; - -let childA: FileStatNode; -let childB: FileStatNode; - -let homeFolder: DirNode; -let childC: FileStatNode; - -let folderA: FileStat; -let folderB: FileStat; - -/** - * The setup function construct a navigator file tree depicted below: - * - * -- root (invisible root node) - * |__ workspaceRootFolder - * |__ childA - * |__ childB - * - * The following nodes are not in the navigator file tree: - * - * -- homeFolder - * |__ childC - * |__ folderA - * |__ folderB - */ -const setup = () => { - root = { id: 'WorkspaceNodeId', name: 'WorkspaceNode', parent: undefined, children: [] }; - workspaceRootFolder = { - parent: root, - uri: new URI('file:///home/rootFolder'), - selected: false, expanded: true, children: [], id: 'id_rootFolder', name: 'name_rootFolder', - fileStat: { uri: 'file:///home/rootFolder', isDirectory: true, lastModification: 0 } - }; - childA = { - id: 'idA', name: 'nameA', parent: workspaceRootFolder, uri: new URI('file:///home/rootFolder/childA'), selected: false, - fileStat: { uri: 'file:///home/rootFolder/childA', isDirectory: true, lastModification: 0 } - }; - childB = { - id: 'idB', name: 'nameB', parent: workspaceRootFolder, uri: new URI('file:///home/rootFolder/childB'), selected: false, - fileStat: { uri: 'file:///home/rootFolder/childB', isDirectory: true, lastModification: 0 } - }; - root.children = [workspaceRootFolder]; - workspaceRootFolder.children = [childA, childB]; - - homeFolder = { - parent: root, - uri: new URI('file:///home'), - selected: false, expanded: true, children: [], id: 'id_rootFolder', name: 'name_rootFolder', - fileStat: { uri: 'file:///home/rootFolder', isDirectory: true, lastModification: 0 } - }; - childC = { - id: 'idC', name: 'nameC', parent: homeFolder, uri: new URI('file:///home/childC'), selected: false, - fileStat: { uri: 'file:///home/childC', isDirectory: false, lastModification: 0 } - }; - homeFolder.children = [childC]; - - folderA = Object.freeze({ - uri: 'file:///home/folderA', - lastModification: 0, - isDirectory: true - }); - folderB = Object.freeze({ - uri: 'file:///home/folderB', - lastModification: 0, - isDirectory: true - }); -}; - -// TODO rewrite as integration tests instead of testing mocks -describe('FileNavigatorModel', () => { - let testContainer: Container; - - let mockOpenerService: OpenerService; - let mockFileNavigatorTree: FileNavigatorTree; - let mockWorkspaceService: WorkspaceService; - let mockFilesystem: FileSystem; - let mockLabelProvider: LabelProvider; - let mockFileSystemWatcher: FileSystemWatcher; - let mockILogger: ILogger; - let mockTreeSelectionService: TreeSelectionService; - let mockTreeExpansionService: TreeExpansionService; - let mockTreeNavigationService: TreeNavigationService; - let mockTreeSearch: TreeSearch; - let mockPreferences: CorePreferences; - - const mockWorkspaceServiceEmitter: Emitter = new Emitter(); - const mockWorkspaceOnLocationChangeEmitter: Emitter = new Emitter(); - const mockFileChangeEmitter: Emitter = new Emitter(); - const mockFileMoveEmitter: Emitter = new Emitter(); - const mockTreeChangeEmitter: Emitter = new Emitter(); - const mockExpansionChangeEmitter: Emitter> = new Emitter(); - - let navigatorModel: FileNavigatorModel; - const toRestore: Array = []; - - before(() => { - disableJSDOM = enableJSDOM(); - }); - after(() => { - disableJSDOM(); - }); - - beforeEach(() => { - mockOpenerService = sinon.createStubInstance(DefaultOpenerService); - mockFileNavigatorTree = sinon.createStubInstance(FileNavigatorTree); - mockWorkspaceService = sinon.createStubInstance(WorkspaceService); - mockFilesystem = sinon.createStubInstance(FileSystemNode); - mockLabelProvider = sinon.createStubInstance(LabelProvider); - mockFileSystemWatcher = sinon.createStubInstance(FileSystemWatcher); - mockILogger = sinon.createStubInstance(Logger); - mockTreeSelectionService = sinon.createStubInstance(TreeSelectionServiceImpl); - mockTreeExpansionService = sinon.createStubInstance(TreeExpansionServiceImpl); - mockTreeNavigationService = sinon.createStubInstance(TreeNavigationService); - mockTreeSearch = sinon.createStubInstance(TreeSearch); - mockPreferences = createMockPreferenceProxy({}); - const mockApplicationStateService = sinon.createStubInstance(FrontendApplicationStateService); - - testContainer = new Container(); - testContainer.bind(FileNavigatorModel).toSelf().inSingletonScope(); - testContainer.bind(OpenerService).toConstantValue(mockOpenerService); - testContainer.bind(FileNavigatorTree).toConstantValue(mockFileNavigatorTree); - testContainer.bind(WorkspaceService).toConstantValue(mockWorkspaceService); - testContainer.bind(FileSystem).toConstantValue(mockFilesystem); - testContainer.bind(LabelProvider).toConstantValue(mockLabelProvider); - testContainer.bind(FileSystemWatcher).toConstantValue(mockFileSystemWatcher); - testContainer.bind(ILogger).toConstantValue(mockILogger); - testContainer.bind(Tree).toConstantValue(mockFileNavigatorTree); - testContainer.bind(TreeSelectionService).toConstantValue(mockTreeSelectionService); - testContainer.bind(TreeExpansionService).toConstantValue(mockTreeExpansionService); - testContainer.bind(TreeNavigationService).toConstantValue(mockTreeNavigationService); - testContainer.bind(TreeSearch).toConstantValue(mockTreeSearch); - testContainer.bind(CorePreferences).toConstantValue(mockPreferences); - testContainer.bind(FrontendApplicationStateService).toConstantValue(mockApplicationStateService); - testContainer.bind(ProgressService).toConstantValue({ - withProgress: (_, __, task) => task() - }); - - sinon.stub(mockWorkspaceService, 'onWorkspaceChanged').value(mockWorkspaceServiceEmitter.event); - sinon.stub(mockWorkspaceService, 'onWorkspaceLocationChanged').value(mockWorkspaceOnLocationChangeEmitter.event); - sinon.stub(mockFileSystemWatcher, 'onFilesChanged').value(mockFileChangeEmitter.event); - sinon.stub(mockFileSystemWatcher, 'onDidMove').value(mockFileMoveEmitter.event); - sinon.stub(mockFileNavigatorTree, 'onChanged').value(mockTreeChangeEmitter.event); - sinon.stub(mockFileNavigatorTree, 'onDidChangeBusy').value(Event.None); - sinon.stub(mockTreeExpansionService, 'onExpansionChanged').value(mockExpansionChangeEmitter.event); - - setup(); - navigatorModel = testContainer.get(FileNavigatorModel); - }); - afterEach(() => { - toRestore.forEach(res => { - res.restore(); - }); - toRestore.length = 0; - }); - - describe('updateRoot() function', () => { - it('should assign "this.root" a WorkspaceNode with WorkspaceRootNodes (one for each root folder in the workspace) as its children', async () => { - sinon.stub(mockWorkspaceService, 'roots').value([folderA, folderB]); - sinon.stub(mockWorkspaceService, 'opened').value(true); - (mockFileNavigatorTree.createWorkspaceRoot).callsFake((stat, rootNode) => - Promise.resolve({ - parent: rootNode, - uri: new URI(stat.uri), - selected: false, expanded: true, children: [], id: 'id_rootFolder', name: 'name_rootFolder', - fileStat: { uri: stat.uri, isDirectory: true, lastModification: 0 } - }) - ); - - await navigatorModel['updateRoot'](); - const thisRoot = navigatorModel['root'] as WorkspaceNode; - expect(thisRoot).not.to.be.undefined; - expect(thisRoot.children.length).to.eq(2); - expect(thisRoot.children[0].uri.toString()).to.eq(folderA.uri); - expect(thisRoot.children[1].uri.toString()).to.eq(folderB.uri); - }); - - it('should assign "this.root" undefined if there is no workspace open', async () => { - sinon.stub(mockWorkspaceService, 'opened').value(false); - - await navigatorModel['updateRoot'](); - const thisRoot = navigatorModel['root'] as WorkspaceNode; - expect(thisRoot).to.be.undefined; - }); - }); - - describe('move() function', () => { - it('should do nothing if user tries to move a root folder', () => { - const stubMove = sinon.stub(FileTreeModel.prototype, 'move').callsFake(() => { }); - const stubCheckRoot = sinon.stub(WorkspaceRootNode, 'is').returns(true); - toRestore.push(...[stubMove, stubCheckRoot]); - - navigatorModel.move(workspaceRootFolder, childA); - expect(stubMove.called).to.be.false; - }); - - it('should pass argument to move() in FileTreeModel class if the node being moved is not a root folder', () => { - const stubMove = sinon.stub(FileTreeModel.prototype, 'move').callsFake(() => { }); - const stubCheckRoot = sinon.stub(WorkspaceRootNode, 'is').returns(false); - toRestore.push(...[stubMove, stubCheckRoot]); - - navigatorModel.move(childA, workspaceRootFolder); - expect(stubMove.called).to.be.true; - }); - }); - - describe('revealFile() function', () => { - it('should return undefined if the uri to be revealed does not contain an absolute path', async () => { - const ret = await navigatorModel.revealFile(new URI('folderC/untitled')); - expect(ret).to.be.undefined; - }); - - it('should return undefined if node being revealed is not part of the file tree', async () => { - navigatorModel['root'] = root; - (mockFileNavigatorTree.createId).callsFake((rootNode, uri) => `${rootNode ? rootNode.id : 'no_root_node'}:${uri.path.toString()}`); - sinon.stub(navigatorModel, 'getNode').callsFake((id: string | undefined): TreeNode | undefined => { - if (id) { - if (id.endsWith(childA.uri.path.toString())) { - return childA; - } else if (id.endsWith(childB.uri.path.toString())) { - return childB; - } else if (id.endsWith(workspaceRootFolder.uri.path.toString())) { - return workspaceRootFolder; - } else if (id.endsWith(childC.uri.path.toString())) { - return childC; - } - } - return undefined; - }); - const ret = await navigatorModel.revealFile(childC.uri); // childC is not under any root folder of the workspace - expect(ret).to.be.undefined; - }); - - const fakeCreateId = (rootNode: WorkspaceRootNode, uri: URI) => `${rootNode ? rootNode.id : 'no_root_node'}:${uri.path.toString()}`; - const fakeGetNode = (id: string | undefined): TreeNode | undefined => { - if (id) { - if (id.endsWith(childA.uri.path.toString())) { - return childA; - } else if (id.endsWith(childB.uri.path.toString())) { - return childB; - } else if (id.endsWith(workspaceRootFolder.uri.path.toString())) { - return workspaceRootFolder; - } else if (id.endsWith(childC.uri.path.toString())) { - return childC; - } - } - return undefined; - }; - - it('should return undefined if cannot find a node from the file tree', async () => { - navigatorModel['root'] = root; - (mockFileNavigatorTree.createId).callsFake(fakeCreateId); - sinon.stub(navigatorModel, 'getNode').callsFake(fakeGetNode); - - const ret = await navigatorModel.revealFile(childC.uri); - expect(ret).to.be.undefined; - }); - - it('should return the node if the node being revealed is part of the file tree', async () => { - navigatorModel['root'] = root; - (mockFileNavigatorTree.createId).callsFake(fakeCreateId); - sinon.stub(navigatorModel, 'getNode').callsFake(fakeGetNode); - - const ret = await navigatorModel.revealFile(childB.uri); - expect(ret).not.to.be.undefined; - expect(ret && ret.id).to.eq(childB.id); - }); - - it('should return the node and expand the node if the node being revealed is a folder as part of the file tree', async () => { - navigatorModel['root'] = root; - (mockFileNavigatorTree.createId).callsFake(fakeCreateId); - const stubExpand = sinon.stub(navigatorModel, 'expandNode'); - stubExpand.callsFake(() => { }); - sinon.stub(navigatorModel, 'getNode').callsFake(fakeGetNode); - - await navigatorModel.revealFile(Object.assign(childB, { expanded: false, children: [] }).uri); - expect(stubExpand.called).to.be.true; - }); - }); -}); diff --git a/packages/navigator/src/browser/navigator-model.ts b/packages/navigator/src/browser/navigator-model.ts index bbe6aa8975a4c..1b27193f3a5a6 100644 --- a/packages/navigator/src/browser/navigator-model.ts +++ b/packages/navigator/src/browser/navigator-model.ts @@ -149,7 +149,7 @@ export class FileNavigatorModel extends FileTreeModel { protected createMultipleRootNode(): WorkspaceNode { const workspace = this.workspaceService.workspace; let name = workspace - ? new URI(workspace.uri).path.name + ? workspace.resource.path.name : 'untitled'; name += ' (Workspace)'; return WorkspaceNode.createRoot(name); @@ -158,12 +158,12 @@ export class FileNavigatorModel extends FileTreeModel { /** * Move the given source file or directory to the given target directory. */ - async move(source: TreeNode, target: TreeNode): Promise { + async move(source: TreeNode, target: TreeNode): Promise { if (source.parent && WorkspaceRootNode.is(source)) { // do not support moving a root folder - return; + return undefined; } - await super.move(source, target); + return super.move(source, target); } /** diff --git a/packages/navigator/src/browser/navigator-tree.spec.ts b/packages/navigator/src/browser/navigator-tree.spec.ts deleted file mode 100644 index d62f77210af8f..0000000000000 --- a/packages/navigator/src/browser/navigator-tree.spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Ericsson and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; -let disableJSDOM = enableJSDOM(); - -import { Container } from 'inversify'; -import { Emitter } from '@theia/core'; -import { CompositeTreeNode, LabelProvider, TreeNode } from '@theia/core/lib/browser'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; -import { FileSystemNode } from '@theia/filesystem/lib/node/node-filesystem'; -import { DirNode, FileTree } from '@theia/filesystem/lib/browser'; -import { FileNavigatorTree, WorkspaceNode, WorkspaceRootNode } from './navigator-tree'; -import { FileNavigatorFilter } from './navigator-filter'; -import { expect } from 'chai'; -import URI from '@theia/core/lib/common/uri'; -import * as sinon from 'sinon'; - -disableJSDOM(); - -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable no-unused-expressions */ - -let root: CompositeTreeNode; -let workspaceRootFolder: DirNode; -let childA: TreeNode; -let childB: TreeNode; - -/** - * The setup function construct a navigator file tree depicted below: - * - * -- root (invisible root node) - * |__ workspaceRootFolder - * |__ childA - * |__ childB - */ -const setup = () => { - root = { id: 'WorkspaceNodeId', name: 'WorkspaceNode', parent: undefined, children: [] }; - workspaceRootFolder = { - parent: root, - uri: new URI('file:///home/rootFolder'), - selected: false, expanded: true, children: [], id: 'id_rootFolder', name: 'name_rootFolder', - fileStat: { uri: 'file:///home/rootFolder', isDirectory: true, lastModification: 0 } - }; - childA = { id: 'idA', name: 'nameA', parent: workspaceRootFolder }; - childB = { id: 'idB', name: 'nameB', parent: workspaceRootFolder }; - root.children = [workspaceRootFolder]; - workspaceRootFolder.children = [childA, childB]; -}; - -describe('FileNavigatorTree', () => { - let testContainer: Container; - - let mockFileNavigatorFilter: FileNavigatorFilter; - let mockFilesystem: FileSystem; - let mockLabelProvider: LabelProvider; - - const mockFilterChangeEmitter: Emitter = new Emitter(); - const mockLabelChangeEmitter: Emitter = new Emitter(); - - let navigatorTree: FileNavigatorTree; - - before(() => { - disableJSDOM = enableJSDOM(); - }); - after(() => { - disableJSDOM(); - }); - - beforeEach(() => { - mockFileNavigatorFilter = sinon.createStubInstance(FileNavigatorFilter); - mockFilesystem = sinon.createStubInstance(FileSystemNode); - mockLabelProvider = sinon.createStubInstance(LabelProvider); - - testContainer = new Container(); - testContainer.bind(FileNavigatorTree).toSelf().inSingletonScope(); - testContainer.bind(FileNavigatorFilter).toConstantValue(mockFileNavigatorFilter); - testContainer.bind(FileSystem).toConstantValue(mockFilesystem); - testContainer.bind(LabelProvider).toConstantValue(mockLabelProvider); - - sinon.stub(mockFileNavigatorFilter, 'onFilterChanged').value(mockFilterChangeEmitter.event); - sinon.stub(mockLabelProvider, 'onDidChange').value(mockLabelChangeEmitter.event); - setup(); - - navigatorTree = testContainer.get(FileNavigatorTree); - }); - - it('should refresh the tree on filter gets changed', () => { - const stubRefresh = sinon.stub(navigatorTree, 'refresh').callsFake(() => { }); - mockFilterChangeEmitter.fire(undefined); - expect(stubRefresh.called).to.be.true; - }); - - describe('resolveChildren() function', () => { - it('should return the children of the parent node if it is the root node of workspace', async () => { - const children = await navigatorTree.resolveChildren(root); - expect(children.length).to.eq(1); - expect(children[0]).to.deep.eq(workspaceRootFolder); - }); - - it('should return children filtered by FileNavigatorFilter', async () => { - const children = Promise.resolve([childA, childB]); - sinon.stub(FileTree.prototype, 'resolveChildren').returns(children); - await navigatorTree.resolveChildren(workspaceRootFolder); - expect((mockFileNavigatorFilter.filter).calledWith(children)).to.be.true; - }); - }); - - describe('createId() function', () => { - it('should return the concatenation of root id + node uri', () => { - const uri = new URI('file:///home/fileC'); - const ret = navigatorTree.createId(workspaceRootFolder, uri); - expect(ret).to.eq(`${workspaceRootFolder.id}:${uri.path.toString()}`); - }); - }); -}); - -describe('WorkspaceNode', () => { - describe('is() function', () => { - it('should return true if the node is a CompositeTreeNode with the name of "WorkspaceNode", otherwise false', () => { - expect(WorkspaceNode.is(undefined)).to.be.false; - - const noNode = { id: 'id', name: 'name', parent: undefined, children: [] }; - expect(WorkspaceNode.is(noNode)).to.be.false; - - // root of the entire navigator file tree - expect(WorkspaceNode.is(root)).to.be.true; - - // tree node - expect(WorkspaceNode.is(childA)).to.be.false; - }); - }); - - describe('createRoot() function', () => { - it('should return a node with the name of "WorkspaceNode" and id of "WorkspaceNodeId"', () => { - expect(WorkspaceNode.createRoot()).to.deep.eq({ - id: 'WorkspaceNodeId', - name: 'WorkspaceNode', - parent: undefined, - children: [], - visible: false, - selected: false - }); - }); - }); -}); - -describe('WorkspaceRootNode', () => { - describe('is() function', () => { - it('should return false if the node is a DirNode with the parent of WorkspaceNode, otherwise false', () => { - expect(WorkspaceRootNode.is(undefined)).to.be.false; - - expect(WorkspaceRootNode.is(workspaceRootFolder)).to.be.true; - - const noNode = { - parent: { id: 'parentId', name: 'parentName', parent: undefined, children: [] }, - uri: new URI('file:///home/folderB'), - selected: false, expanded: true, children: [], id: 'id', name: 'name', - fileStat: { uri: 'file:///home/folderB', isDirectory: true, lastModification: 0 } - }; - expect(WorkspaceRootNode.is(noNode)).to.be.false; - - expect(WorkspaceRootNode.is(childB)).to.be.false; - }); - }); - - describe('find() function', () => { - it('should return the node itself if the node is a WorkspaceRootNode', () => { - expect(WorkspaceRootNode.find(workspaceRootFolder)).to.deep.eq(workspaceRootFolder); - }); - - it('should return the ancestor of the node if the node itself is not a WorkspaceRootNode', () => { - expect(WorkspaceRootNode.find(undefined)).to.be.undefined; - - expect(WorkspaceRootNode.find(childA)).to.deep.eq(workspaceRootFolder); - }); - }); -}); diff --git a/packages/navigator/src/browser/navigator-tree.ts b/packages/navigator/src/browser/navigator-tree.ts index 61e2cc5475030..f8fd62cf82915 100644 --- a/packages/navigator/src/browser/navigator-tree.ts +++ b/packages/navigator/src/browser/navigator-tree.ts @@ -16,7 +16,7 @@ import { injectable, inject, postConstruct } from 'inversify'; import { FileTree, DirNode } from '@theia/filesystem/lib/browser'; -import { FileStat } from '@theia/filesystem/lib/common'; +import { FileStat } from '@theia/filesystem/lib/common/files'; import URI from '@theia/core/lib/common/uri'; import { TreeNode, CompositeTreeNode, SelectableTreeNode } from '@theia/core/lib/browser'; import { FileNavigatorFilter } from './navigator-filter'; diff --git a/packages/navigator/src/browser/navigator-widget.tsx b/packages/navigator/src/browser/navigator-widget.tsx index 0302dbfd20159..b883a9db6abc6 100644 --- a/packages/navigator/src/browser/navigator-widget.tsx +++ b/packages/navigator/src/browser/navigator-widget.tsx @@ -28,7 +28,6 @@ import { WorkspaceService, WorkspaceCommands } from '@theia/workspace/lib/browse import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { WorkspaceNode, WorkspaceRootNode } from './navigator-tree'; import { FileNavigatorModel } from './navigator-model'; -import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; import { isOSX, environment } from '@theia/core'; import * as React from 'react'; import { NavigatorContextKeyService } from './navigator-context-key-service'; @@ -59,8 +58,7 @@ export class FileNavigatorWidget extends FileTreeWidget { @inject(CommandService) protected readonly commandService: CommandService, @inject(SelectionService) protected readonly selectionService: SelectionService, @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, - @inject(ApplicationShell) protected readonly shell: ApplicationShell, - @inject(FileSystem) protected readonly fileSystem: FileSystem + @inject(ApplicationShell) protected readonly shell: ApplicationShell ) { super(props, model, contextMenuRenderer); this.id = FILE_NAVIGATOR_ID; @@ -133,23 +131,6 @@ export class FileNavigatorWidget extends FileTreeWidget { return undefined; } - protected deflateForStorage(node: TreeNode): object { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const copy = { ...node } as any; - if (copy.uri) { - copy.uri = copy.uri.toString(); - } - return super.deflateForStorage(copy); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - protected inflateFromStorage(node: any, parent?: TreeNode): TreeNode { - if (node.uri) { - node.uri = new URI(node.uri); - } - return super.inflateFromStorage(node, parent); - } - protected renderTree(model: TreeModel): React.ReactNode { return super.renderTree(model) || this.renderOpenWorkspaceDiv(); } @@ -175,11 +156,14 @@ export class FileNavigatorWidget extends FileTreeWidget { if (!raw) { return; } + const target = this.model.selectedFileStatNodes[0]; + if (!target) { + return; + } for (const file of raw.split('\n')) { - const uri = new URI(file); - if (this.model.copy(uri)) { - event.preventDefault(); - } + event.preventDefault(); + const source = new URI(file); + this.model.copy(source, target); } } } diff --git a/packages/output/src/browser/output-editor-model-factory.ts b/packages/output/src/browser/output-editor-model-factory.ts index 61bfe5ae08782..478f92bde2a57 100644 --- a/packages/output/src/browser/output-editor-model-factory.ts +++ b/packages/output/src/browser/output-editor-model-factory.ts @@ -34,10 +34,9 @@ export class OutputEditorModelFactory implements MonacoEditorModelFactory { readonly scheme = OutputUri.SCHEME; createModel( - resource: Resource, - options?: { encoding?: string | undefined } + resource: Resource ): MonacoEditorModel { - return new OutputEditorModel(resource, this.m2p, this.p2m, options); + return new OutputEditorModel(resource, this.m2p, this.p2m); } } diff --git a/packages/plugin-dev/src/browser/hosted-plugin-informer.ts b/packages/plugin-dev/src/browser/hosted-plugin-informer.ts index d165aad5d19a8..ea41981337b7a 100644 --- a/packages/plugin-dev/src/browser/hosted-plugin-informer.ts +++ b/packages/plugin-dev/src/browser/hosted-plugin-informer.ts @@ -20,8 +20,7 @@ import { StatusBarAlignment, StatusBarEntry, FrontendApplicationContribution } f import { WorkspaceService } from '@theia/workspace/lib/browser'; import { HostedPluginServer } from '../common/plugin-dev-protocol'; import { ConnectionStatusService, ConnectionStatus } from '@theia/core/lib/browser/connection-status-service'; -import URI from '@theia/core/lib/common/uri'; -import { FileStat } from '@theia/filesystem/lib/common'; +import { FileStat } from '@theia/filesystem/lib/common/files'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; /** @@ -90,7 +89,7 @@ export class HostedPluginInformer implements FrontendApplicationContribution { private updateTitle(root: FileStat | undefined): void { if (root) { - const uri = new URI(root.uri); + const uri = root.resource; document.title = HostedPluginInformer.DEVELOPMENT_HOST_TITLE + ' - ' + uri.displayName; } else { document.title = HostedPluginInformer.DEVELOPMENT_HOST_TITLE; diff --git a/packages/plugin-dev/src/browser/hosted-plugin-manager-client.ts b/packages/plugin-dev/src/browser/hosted-plugin-manager-client.ts index 4b6b0125cee1e..e19fb1256b28e 100644 --- a/packages/plugin-dev/src/browser/hosted-plugin-manager-client.ts +++ b/packages/plugin-dev/src/browser/hosted-plugin-manager-client.ts @@ -21,12 +21,13 @@ import { MessageService, Command, Emitter, Event, UriSelection } from '@theia/co import { LabelProvider, isNative, AbstractDialog } from '@theia/core/lib/browser'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { OpenFileDialogFactory, DirNode } from '@theia/filesystem/lib/browser'; import { HostedPluginServer } from '../common/plugin-dev-protocol'; import { DebugConfiguration as HostedDebugConfig } from '../common'; import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; import { HostedPluginPreferences } from './hosted-plugin-preferences'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; /** * Commands to control Hosted plugin instances. @@ -109,8 +110,10 @@ export class HostedPluginManagerClient { protected readonly labelProvider: LabelProvider; @inject(WindowService) protected readonly windowService: WindowService; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; + @inject(EnvVariablesServer) + protected readonly environments: EnvVariablesServer; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(DebugSessionManager) @@ -179,7 +182,7 @@ export class HostedPluginManagerClient { async startDebugSessionManager(): Promise { let outFiles: string[] | undefined = undefined; if (this.pluginLocation) { - const fsPath = await this.fileSystem.getFsPath(this.pluginLocation.toString()); + const fsPath = await this.fileService.fsPath(this.pluginLocation); if (fsPath) { outFiles = [new Path(fsPath).join('**', '*.js').toString()]; } @@ -257,7 +260,7 @@ export class HostedPluginManagerClient { * Creates directory choose dialog and set selected folder into pluginLocation field. */ async selectPluginPath(): Promise { - const workspaceFolder = (await this.workspaceService.roots)[0] || await this.fileSystem.getCurrentUserHome(); + const workspaceFolder = (await this.workspaceService.roots)[0] || await this.fileService.resolve(new URI(await this.environments.getHomeDirUri())); if (!workspaceFolder) { throw new Error('Unable to find the root'); } diff --git a/packages/plugin-dev/src/node/hosted-instance-manager.ts b/packages/plugin-dev/src/node/hosted-instance-manager.ts index 29ee92c7bdc94..c02f258084c07 100644 --- a/packages/plugin-dev/src/node/hosted-instance-manager.ts +++ b/packages/plugin-dev/src/node/hosted-instance-manager.ts @@ -132,10 +132,10 @@ export abstract class AbstractHostedInstanceManager implements HostedInstanceMan if (pluginUri.scheme === 'file') { processOptions = { ...PROCESS_OPTIONS }; // get filesystem path that work cross operating systems - processOptions.env.HOSTED_PLUGIN = FileUri.fsPath(pluginUri.toString()); + processOptions.env!.HOSTED_PLUGIN = FileUri.fsPath(pluginUri.toString()); // Disable all the other plugins on this instance - processOptions.env.THEIA_PLUGINS = ''; + processOptions.env!.THEIA_PLUGINS = ''; command = await this.getStartCommand(port, debugConfig); } else { throw new Error('Not supported plugin location: ' + pluginUri.toString()); @@ -234,7 +234,7 @@ export abstract class AbstractHostedInstanceManager implements HostedInstanceMan if (fs.existsSync(pckPath)) { const pck = require(pckPath); try { - return !!this.metadata.getScanner(pck); + return !!this.metadata.getScanner(pck); } catch (e) { console.error(e); return false; diff --git a/packages/plugin-ext-vscode/compile.tsconfig.json b/packages/plugin-ext-vscode/compile.tsconfig.json index 9a77f8339bea3..f8e5bcbf9b175 100644 --- a/packages/plugin-ext-vscode/compile.tsconfig.json +++ b/packages/plugin-ext-vscode/compile.tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../workspace/compile.tsconfig.json" + }, + { + "path": "../userstorage/compile.tsconfig.json" } ] } diff --git a/packages/plugin-ext-vscode/package.json b/packages/plugin-ext-vscode/package.json index 9c2885ee6148b..efcf3e0a079e9 100644 --- a/packages/plugin-ext-vscode/package.json +++ b/packages/plugin-ext-vscode/package.json @@ -8,6 +8,7 @@ "@theia/monaco": "^1.4.0", "@theia/plugin": "^1.4.0", "@theia/plugin-ext": "^1.4.0", + "@theia/userstorage": "^1.4.0", "@theia/workspace": "^1.4.0", "@types/request": "^2.0.3", "filenamify": "^4.1.0", @@ -51,4 +52,4 @@ "nyc": { "extends": "../../configs/nyc.json" } -} +} \ No newline at end of file diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-contribution.ts new file mode 100644 index 0000000000000..6a7e44abf59aa --- /dev/null +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-contribution.ts @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import { UserStorageUri } from '@theia/userstorage/lib/browser/user-storage-uri'; +import { FileServiceContribution, FileService } from '@theia/filesystem/lib/browser/file-service'; +import { Schemes } from '@theia/plugin-ext/lib/common/uri-components'; +import { DelegatingFileSystemProvider } from '@theia/filesystem/lib/common/delegating-file-system-provider'; + +@injectable() +export class PluginVSCodeContribution implements FileServiceContribution { + + registerFileSystemProviders(service: FileService): void { + this.mapSchemas(service, Schemes.vscodeRemote, 'file'); + this.mapSchemas(service, Schemes.userData, UserStorageUri.SCHEME); + } + + protected mapSchemas(service: FileService, from: string, to: string): void { + service.onWillActivateFileSystemProvider(event => { + if (event.scheme === from) { + event.waitUntil((async () => { + const provider = await service.activateProvider(to); + service.registerProvider(from, new DelegatingFileSystemProvider(provider, { + uriConverter: { + to: resource => resource.withScheme(to), + from: resource => resource.withScheme(from) + } + })); + })()); + } + }); + } + +} diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-frontend-module.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-frontend-module.ts index 0bce5c3e799c3..4ee8c9a100067 100644 --- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-frontend-module.ts +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-frontend-module.ts @@ -18,9 +18,13 @@ import { ContainerModule } from 'inversify'; import { CommandContribution } from '@theia/core'; import { PluginVscodeCommandsContribution } from './plugin-vscode-commands-contribution'; import { PluginVSCodeEnvironment } from '../common/plugin-vscode-environment'; +import { PluginVSCodeContribution } from './plugin-vscode-contribution'; +import { FileServiceContribution } from '@theia/filesystem/lib/browser/file-service'; export default new ContainerModule(bind => { bind(PluginVSCodeEnvironment).toSelf().inSingletonScope(); bind(PluginVscodeCommandsContribution).toSelf().inSingletonScope(); - bind(CommandContribution).toDynamicValue(context => context.container.get(PluginVscodeCommandsContribution)); + bind(CommandContribution).toService(PluginVscodeCommandsContribution); + bind(PluginVSCodeContribution).toSelf().inSingletonScope(); + bind(FileServiceContribution).toService(PluginVSCodeContribution); }); diff --git a/packages/plugin-ext/src/common/arrays.ts b/packages/plugin-ext/src/common/arrays.ts new file mode 100644 index 0000000000000..01f77bd1c4698 --- /dev/null +++ b/packages/plugin-ext/src/common/arrays.ts @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/base/common/arrays.ts + +/** + * @returns New array with all falsy values removed. The original array IS NOT modified. + */ +export function coalesce(array: ReadonlyArray): T[] { + return array.filter(e => !!e); +} + +/** + * @returns True if the provided object is an array and has at least one element. + */ +export function isNonEmptyArray(obj: T[] | undefined | null): obj is T[]; +export function isNonEmptyArray(obj: readonly T[] | undefined | null): obj is readonly T[]; +export function isNonEmptyArray(obj: T[] | readonly T[] | undefined | null): obj is T[] | readonly T[] { + return Array.isArray(obj) && obj.length > 0; +} + +export function flatten(arr: T[][]): T[] { + return ([]).concat(...arr); +} diff --git a/packages/plugin-ext/src/common/character-classifier.ts b/packages/plugin-ext/src/common/character-classifier.ts new file mode 100644 index 0000000000000..44e4609d4180f --- /dev/null +++ b/packages/plugin-ext/src/common/character-classifier.ts @@ -0,0 +1,73 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/editor/common/core/characterClassifier.ts + +import { toUint8 } from './uint'; + +/** + * A fast character classifier that uses a compact array for ASCII values. + */ +export class CharacterClassifier { + /** + * Maintain a compact (fully initialized ASCII map for quickly classifying ASCII characters - used more often in code). + */ + protected _asciiMap: Uint8Array; + + /** + * The entire map (sparse array). + */ + protected _map: Map; + + protected _defaultValue: number; + + constructor(_defaultValue: T) { + const defaultValue = toUint8(_defaultValue); + + this._defaultValue = defaultValue; + this._asciiMap = CharacterClassifier._createAsciiMap(defaultValue); + this._map = new Map(); + } + + private static _createAsciiMap(defaultValue: number): Uint8Array { + const asciiMap: Uint8Array = new Uint8Array(256); + for (let i = 0; i < 256; i++) { + asciiMap[i] = defaultValue; + } + return asciiMap; + } + + public set(charCode: number, _value: T): void { + const value = toUint8(_value); + + if (charCode >= 0 && charCode < 256) { + this._asciiMap[charCode] = value; + } else { + this._map.set(charCode, value); + } + } + + public get(charCode: number): T { + if (charCode >= 0 && charCode < 256) { + return this._asciiMap[charCode]; + } else { + return (this._map.get(charCode) || this._defaultValue); + } + } +} diff --git a/packages/plugin-ext/src/common/link-computer.ts b/packages/plugin-ext/src/common/link-computer.ts new file mode 100644 index 0000000000000..57ad7a775e98f --- /dev/null +++ b/packages/plugin-ext/src/common/link-computer.ts @@ -0,0 +1,354 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/editor/common/modes/linkComputer.ts + +/* eslint-disable max-len */ + +import { CharacterClassifier } from './character-classifier'; +import { CharCode } from '@theia/core/lib/common/char-code'; +import { DocumentLink as ILink } from './plugin-api-rpc-model'; + +export interface ILinkComputerTarget { + getLineCount(): number; + getLineContent(lineNumber: number): string; +} + +export const enum State { + Invalid = 0, + Start = 1, + H = 2, + HT = 3, + HTT = 4, + HTTP = 5, + F = 6, + FI = 7, + FIL = 8, + BeforeColon = 9, + AfterColon = 10, + AlmostThere = 11, + End = 12, + Accept = 13, + LastKnownState = 14 // marker, custom states may follow +} + +export type Edge = [State, number, State]; + +export class Uint8Matrix { + + private readonly _data: Uint8Array; + public readonly rows: number; + public readonly cols: number; + + constructor(rows: number, cols: number, defaultValue: number) { + const data = new Uint8Array(rows * cols); + for (let i = 0, len = rows * cols; i < len; i++) { + data[i] = defaultValue; + } + + this._data = data; + this.rows = rows; + this.cols = cols; + } + + public get(row: number, col: number): number { + return this._data[row * this.cols + col]; + } + + public set(row: number, col: number, value: number): void { + this._data[row * this.cols + col] = value; + } +} + +export class StateMachine { + + private readonly _states: Uint8Matrix; + private readonly _maxCharCode: number; + + constructor(edges: Edge[]) { + let maxCharCode = 0; + let maxState = State.Invalid; + for (let i = 0, len = edges.length; i < len; i++) { + const [from, chCode, to] = edges[i]; + if (chCode > maxCharCode) { + maxCharCode = chCode; + } + if (from > maxState) { + maxState = from; + } + if (to > maxState) { + maxState = to; + } + } + + maxCharCode++; + maxState++; + + const states = new Uint8Matrix(maxState, maxCharCode, State.Invalid); + for (let i = 0, len = edges.length; i < len; i++) { + const [from, chCode, to] = edges[i]; + states.set(from, chCode, to); + } + + this._states = states; + this._maxCharCode = maxCharCode; + } + + public nextState(currentState: State, chCode: number): State { + if (chCode < 0 || chCode >= this._maxCharCode) { + return State.Invalid; + } + return this._states.get(currentState, chCode); + } +} + +// State machine for http:// or https:// or file:// +let _stateMachine: StateMachine | null = null; +function getStateMachine(): StateMachine { + if (_stateMachine === null) { + _stateMachine = new StateMachine([ + [State.Start, CharCode.h, State.H], + [State.Start, CharCode.H, State.H], + [State.Start, CharCode.f, State.F], + [State.Start, CharCode.F, State.F], + + [State.H, CharCode.t, State.HT], + [State.H, CharCode.T, State.HT], + + [State.HT, CharCode.t, State.HTT], + [State.HT, CharCode.T, State.HTT], + + [State.HTT, CharCode.p, State.HTTP], + [State.HTT, CharCode.P, State.HTTP], + + [State.HTTP, CharCode.s, State.BeforeColon], + [State.HTTP, CharCode.S, State.BeforeColon], + [State.HTTP, CharCode.Colon, State.AfterColon], + + [State.F, CharCode.i, State.FI], + [State.F, CharCode.I, State.FI], + + [State.FI, CharCode.l, State.FIL], + [State.FI, CharCode.L, State.FIL], + + [State.FIL, CharCode.e, State.BeforeColon], + [State.FIL, CharCode.E, State.BeforeColon], + + [State.BeforeColon, CharCode.Colon, State.AfterColon], + + [State.AfterColon, CharCode.Slash, State.AlmostThere], + + [State.AlmostThere, CharCode.Slash, State.End], + ]); + } + return _stateMachine; +} + +const enum CharacterClass { + None = 0, + ForceTermination = 1, + CannotEndIn = 2 +} + +let _classifier: CharacterClassifier | null = null; +function getClassifier(): CharacterClassifier { + if (_classifier === null) { + _classifier = new CharacterClassifier(CharacterClass.None); + + const FORCE_TERMINATION_CHARACTERS = ' \t<>\'\"、。。、,.:;?!@#$%&*‘“〈《「『【〔([{「」}])〕】』」》〉”’`~…'; + for (let i = 0; i < FORCE_TERMINATION_CHARACTERS.length; i++) { + _classifier.set(FORCE_TERMINATION_CHARACTERS.charCodeAt(i), CharacterClass.ForceTermination); + } + + const CANNOT_END_WITH_CHARACTERS = '.,;'; + for (let i = 0; i < CANNOT_END_WITH_CHARACTERS.length; i++) { + _classifier.set(CANNOT_END_WITH_CHARACTERS.charCodeAt(i), CharacterClass.CannotEndIn); + } + } + return _classifier; +} + +export class LinkComputer { + + private static _createLink(classifier: CharacterClassifier, line: string, lineNumber: number, linkBeginIndex: number, linkEndIndex: number): ILink { + // Do not allow to end link in certain characters... + let lastIncludedCharIndex = linkEndIndex - 1; + do { + const chCode = line.charCodeAt(lastIncludedCharIndex); + const chClass = classifier.get(chCode); + if (chClass !== CharacterClass.CannotEndIn) { + break; + } + lastIncludedCharIndex--; + } while (lastIncludedCharIndex > linkBeginIndex); + + // Handle links enclosed in parens, square brackets and curlys. + if (linkBeginIndex > 0) { + const charCodeBeforeLink = line.charCodeAt(linkBeginIndex - 1); + const lastCharCodeInLink = line.charCodeAt(lastIncludedCharIndex); + + if ( + (charCodeBeforeLink === CharCode.OpenParen && lastCharCodeInLink === CharCode.CloseParen) + || (charCodeBeforeLink === CharCode.OpenSquareBracket && lastCharCodeInLink === CharCode.CloseSquareBracket) + || (charCodeBeforeLink === CharCode.OpenCurlyBrace && lastCharCodeInLink === CharCode.CloseCurlyBrace) + ) { + // Do not end in ) if ( is before the link start + // Do not end in ] if [ is before the link start + // Do not end in } if { is before the link start + lastIncludedCharIndex--; + } + } + + return { + range: { + startLineNumber: lineNumber, + startColumn: linkBeginIndex + 1, + endLineNumber: lineNumber, + endColumn: lastIncludedCharIndex + 2 + }, + url: line.substring(linkBeginIndex, lastIncludedCharIndex + 1) + }; + } + + public static computeLinks(model: ILinkComputerTarget, stateMachine: StateMachine = getStateMachine()): ILink[] { + const classifier = getClassifier(); + + const result: ILink[] = []; + for (let i = 1, lineCount = model.getLineCount(); i <= lineCount; i++) { + const line = model.getLineContent(i); + const len = line.length; + + let j = 0; + let linkBeginIndex = 0; + let linkBeginChCode = 0; + let state = State.Start; + let hasOpenParens = false; + let hasOpenSquareBracket = false; + let inSquareBrackets = false; + let hasOpenCurlyBracket = false; + + while (j < len) { + + let resetStateMachine = false; + const chCode = line.charCodeAt(j); + + if (state === State.Accept) { + let chClass: CharacterClass; + switch (chCode) { + case CharCode.OpenParen: + hasOpenParens = true; + chClass = CharacterClass.None; + break; + case CharCode.CloseParen: + chClass = (hasOpenParens ? CharacterClass.None : CharacterClass.ForceTermination); + break; + case CharCode.OpenSquareBracket: + inSquareBrackets = true; + hasOpenSquareBracket = true; + chClass = CharacterClass.None; + break; + case CharCode.CloseSquareBracket: + inSquareBrackets = false; + chClass = (hasOpenSquareBracket ? CharacterClass.None : CharacterClass.ForceTermination); + break; + case CharCode.OpenCurlyBrace: + hasOpenCurlyBracket = true; + chClass = CharacterClass.None; + break; + case CharCode.CloseCurlyBrace: + chClass = (hasOpenCurlyBracket ? CharacterClass.None : CharacterClass.ForceTermination); + break; + /* The following three rules make it that ' or " or ` are allowed inside links if the link began with a different one */ + case CharCode.SingleQuote: + chClass = (linkBeginChCode === CharCode.DoubleQuote || linkBeginChCode === CharCode.BackTick) ? CharacterClass.None : CharacterClass.ForceTermination; + break; + case CharCode.DoubleQuote: + chClass = (linkBeginChCode === CharCode.SingleQuote || linkBeginChCode === CharCode.BackTick) ? CharacterClass.None : CharacterClass.ForceTermination; + break; + case CharCode.BackTick: + chClass = (linkBeginChCode === CharCode.SingleQuote || linkBeginChCode === CharCode.DoubleQuote) ? CharacterClass.None : CharacterClass.ForceTermination; + break; + case CharCode.Asterisk: + // `*` terminates a link if the link began with `*` + chClass = (linkBeginChCode === CharCode.Asterisk) ? CharacterClass.ForceTermination : CharacterClass.None; + break; + case CharCode.Pipe: + // `|` terminates a link if the link began with `|` + chClass = (linkBeginChCode === CharCode.Pipe) ? CharacterClass.ForceTermination : CharacterClass.None; + break; + case CharCode.Space: + // ` ` allow space in between [ and ] + chClass = (inSquareBrackets ? CharacterClass.None : CharacterClass.ForceTermination); + break; + default: + chClass = classifier.get(chCode); + } + + // Check if character terminates link + if (chClass === CharacterClass.ForceTermination) { + result.push(LinkComputer._createLink(classifier, line, i, linkBeginIndex, j)); + resetStateMachine = true; + } + } else if (state === State.End) { + + let chClass: CharacterClass; + if (chCode === CharCode.OpenSquareBracket) { + // Allow for the authority part to contain ipv6 addresses which contain [ and ] + hasOpenSquareBracket = true; + chClass = CharacterClass.None; + } else { + chClass = classifier.get(chCode); + } + + // Check if character terminates link + if (chClass === CharacterClass.ForceTermination) { + resetStateMachine = true; + } else { + state = State.Accept; + } + } else { + state = stateMachine.nextState(state, chCode); + if (state === State.Invalid) { + resetStateMachine = true; + } + } + + if (resetStateMachine) { + state = State.Start; + hasOpenParens = false; + hasOpenSquareBracket = false; + hasOpenCurlyBracket = false; + + // Record where the link started + linkBeginIndex = j + 1; + linkBeginChCode = chCode; + } + + j++; + } + + if (state === State.Accept) { + result.push(LinkComputer._createLink(classifier, line, i, linkBeginIndex, len)); + } + + } + + return result; + } +} diff --git a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts index 496e604fa2a11..edcc42e3653e5 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -16,7 +16,6 @@ import * as theia from '@theia/plugin'; import { UriComponents } from './uri-components'; -import { FileStat } from '@theia/filesystem/lib/common'; // Should contains internal Plugin API types @@ -82,21 +81,6 @@ export interface SerializedDocumentFilter { pattern?: theia.GlobPattern; } -export interface FileWatcherSubscriberOptions { - globPattern: theia.GlobPattern; - ignoreCreateEvents?: boolean; - ignoreChangeEvents?: boolean; - ignoreDeleteEvents?: boolean; -} - -export interface FileChangeEvent { - subscriberId: string, - uri: UriComponents, - type: FileChangeEventType -} - -export type FileChangeEventType = 'created' | 'updated' | 'deleted'; - export enum CompletionTriggerKind { Invoke = 0, TriggerCharacter = 1, @@ -424,7 +408,7 @@ export interface DocumentSymbol { } export interface WorkspaceRootsChangeEvent { - roots: FileStat[]; + roots: string[]; } export interface WorkspaceFolder { @@ -540,18 +524,6 @@ export interface CallHierarchyOutgoingCall { fromRanges: Range[]; } -export interface CreateFilesEventDTO { - files: UriComponents[] -} - -export interface RenameFilesEventDTO { - files: { oldUri: UriComponents, newUri: UriComponents }[] -} - -export interface DeleteFilesEventDTO { - files: UriComponents[] -} - export interface SearchInWorkspaceResult { root: string; fileUri: string; diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index f6e0e6f0e5430..ea810307bba75 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -30,7 +30,7 @@ import { QuickInputButton } from '../plugin/types-impl'; import { UriComponents } from './uri-components'; -import { ConfigurationTarget, FileType, FileStat } from '../plugin/types-impl'; +import { ConfigurationTarget } from '../plugin/types-impl'; import { SerializedDocumentFilter, CompletionContext, @@ -50,8 +50,6 @@ import { TextEdit, DocumentSymbol, ReferenceContext, - FileWatcherSubscriberOptions, - FileChangeEvent, TextDocumentShowOptions, WorkspaceRootsChangeEvent, Location, @@ -66,9 +64,6 @@ import { SelectionRange, CallHierarchyDefinition, CallHierarchyReference, - CreateFilesEventDTO, - RenameFilesEventDTO, - DeleteFilesEventDTO, SearchInWorkspaceResult } from './plugin-api-rpc-model'; import { ExtPluginApi } from './plugin-ext-api-contribution'; @@ -80,6 +75,8 @@ import { SymbolInformation } from 'vscode-languageserver-types'; import { ArgumentProcessor } from '../plugin/command-registry'; import { MaybePromise } from '@theia/core/lib/common/types'; import { QuickTitleButton } from '@theia/core/lib/common/quick-open-model'; +import * as files from '@theia/filesystem/lib/common/files'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; export interface PreferenceData { [scope: number]: any; @@ -532,22 +529,12 @@ export interface WorkspaceMain { $registerTextDocumentContentProvider(scheme: string): Promise; $unregisterTextDocumentContentProvider(scheme: string): void; $onTextDocumentContentChange(uri: string, content: string): void; - $registerFileSystemWatcher(options: FileWatcherSubscriberOptions): Promise; - $unregisterFileSystemWatcher(watcherId: string): Promise; $updateWorkspaceFolders(start: number, deleteCount?: number, ...rootsToAdd: string[]): Promise; } export interface WorkspaceExt { $onWorkspaceFoldersChanged(event: WorkspaceRootsChangeEvent): void; $provideTextDocumentContent(uri: string): Promise; - $fileChanged(event: FileChangeEvent): void; - - $onWillCreateFiles(event: CreateFilesEventDTO): Promise; - $onDidCreateFiles(event: CreateFilesEventDTO): void; - $onWillRenameFiles(event: RenameFilesEventDTO): Promise; - $onDidRenameFiles(event: RenameFilesEventDTO): void; - $onWillDeleteFiles(event: DeleteFilesEventDTO): Promise; - $onDidDeleteFiles(event: DeleteFilesEventDTO): void; $onTextSearchResult(searchRequestId: number, done: boolean, result?: SearchInWorkspaceResult): void; } @@ -1376,27 +1363,52 @@ export interface DebugMain { } export interface FileSystemExt { - $stat(handle: number, resource: UriComponents): Promise; - $readDirectory(handle: number, resource: UriComponents): Promise<[string, FileType][]>; - $createDirectory(handle: number, uri: UriComponents): Promise; - $readFile(handle: number, resource: UriComponents, options?: { encoding?: string }): Promise; - $writeFile(handle: number, resource: UriComponents, content: string, options?: { encoding?: string }): Promise; - $delete(handle: number, resource: UriComponents, options: { recursive: boolean }): Promise; - $rename(handle: number, source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise; - $copy(handle: number, source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise; + $stat(handle: number, resource: UriComponents): Promise; + $readdir(handle: number, resource: UriComponents): Promise<[string, files.FileType][]>; + $readFile(handle: number, resource: UriComponents): Promise; + $writeFile(handle: number, resource: UriComponents, content: BinaryBuffer, opts: files.FileWriteOptions): Promise; + $rename(handle: number, resource: UriComponents, target: UriComponents, opts: files.FileOverwriteOptions): Promise; + $copy(handle: number, resource: UriComponents, target: UriComponents, opts: files.FileOverwriteOptions): Promise; + $mkdir(handle: number, resource: UriComponents): Promise; + $delete(handle: number, resource: UriComponents, opts: files.FileDeleteOptions): Promise; + $watch(handle: number, session: number, resource: UriComponents, opts: files.WatchOptions): void; + $unwatch(handle: number, session: number): void; + $open(handle: number, resource: UriComponents, opts: files.FileOpenOptions): Promise; + $close(handle: number, fd: number): Promise; + $read(handle: number, fd: number, pos: number, length: number): Promise; + $write(handle: number, fd: number, pos: number, data: BinaryBuffer): Promise; +} + +export interface IFileChangeDto { + resource: UriComponents; + type: files.FileChangeType; } export interface FileSystemMain { - $stat(uri: UriComponents): Promise - $readDirectory(uri: UriComponents): Promise<[string, FileType][]>; - $createDirectory(uri: UriComponents): Promise - $readFile(uri: UriComponents): Promise; - $writeFile(uri: UriComponents, content: string): Promise; - $delete(uri: UriComponents, options: { recursive: boolean }): Promise; - $rename(source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise; - $copy(source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise; - $registerFileSystemProvider(handle: number, scheme: string): void; + $registerFileSystemProvider(handle: number, scheme: string, capabilities: files.FileSystemProviderCapabilities): void; $unregisterProvider(handle: number): void; + $onFileSystemChange(handle: number, resource: IFileChangeDto[]): void; + + $stat(uri: UriComponents): Promise; + $readdir(resource: UriComponents): Promise<[string, files.FileType][]>; + $readFile(resource: UriComponents): Promise; + $writeFile(resource: UriComponents, content: BinaryBuffer): Promise; + $rename(resource: UriComponents, target: UriComponents, opts: files.FileOverwriteOptions): Promise; + $copy(resource: UriComponents, target: UriComponents, opts: files.FileOverwriteOptions): Promise; + $mkdir(resource: UriComponents): Promise; + $delete(resource: UriComponents, opts: files.FileDeleteOptions): Promise; +} + +export interface FileSystemEvents { + created: UriComponents[]; + changed: UriComponents[]; + deleted: UriComponents[]; +} + +export interface ExtHostFileSystemEventServiceShape { + $onFileEvent(events: FileSystemEvents): void; + $onWillRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined, timeout: number, token: CancellationToken): Promise; + $onDidRunFileOperation(operation: files.FileOperation, target: UriComponents, source: UriComponents | undefined): void; } export interface ClipboardMain { @@ -1453,6 +1465,7 @@ export const MAIN_RPC_CONTEXT = { TASKS_EXT: createProxyIdentifier('TasksExt'), DEBUG_EXT: createProxyIdentifier('DebugExt'), FILE_SYSTEM_EXT: createProxyIdentifier('FileSystemExt'), + ExtHostFileSystemEventService: createProxyIdentifier('ExtHostFileSystemEventService'), SCM_EXT: createProxyIdentifier('ScmExt'), DECORATIONS_EXT: createProxyIdentifier('DecorationsExt') }; diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 6c42c526dcc47..b6bac84e87bbd 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -23,7 +23,6 @@ import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-sch import { RecursivePartial } from '@theia/core/lib/common/types'; import { PreferenceSchema, PreferenceSchemaProperties } from '@theia/core/lib/common/preferences/preference-schema'; import { ProblemMatcherContribution, ProblemPatternContribution, TaskDefinition } from '@theia/task/lib/common'; -import { FileStat } from '@theia/filesystem/lib/common'; import { ColorDefinition } from '@theia/core/lib/browser/color-registry'; export const hostedServicePath = '/services/hostedPlugin'; @@ -752,8 +751,8 @@ export interface HostedPluginServer extends JsonRpcServer { } export interface WorkspaceStorageKind { - workspace?: FileStat | undefined; - roots: FileStat[]; + workspace?: string | undefined; + roots: string[]; } export type GlobalStorageKind = undefined; export type PluginStorageKind = GlobalStorageKind | WorkspaceStorageKind; diff --git a/packages/plugin-ext/src/common/rpc-protocol.ts b/packages/plugin-ext/src/common/rpc-protocol.ts index e849aa69ea810..eb335c80ba971 100644 --- a/packages/plugin-ext/src/common/rpc-protocol.ts +++ b/packages/plugin-ext/src/common/rpc-protocol.ts @@ -29,6 +29,7 @@ import { URI as VSCodeURI } from 'vscode-uri'; import URI from '@theia/core/lib/common/uri'; import { CancellationToken, CancellationTokenSource } from 'vscode-languageserver-protocol'; import { Range, Position } from '../plugin/types-impl'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; export interface MessageConnection { send(msg: {}): void; @@ -432,6 +433,12 @@ namespace ObjectsTransferrer { $type: SerializedObjectType.VSCODE_URI, data: uri.toString() } as SerializedObject; + } else if (value instanceof BinaryBuffer) { + const bytes = [...value.buffer.values()]; + return { + $type: SerializedObjectType.TEXT_BUFFER, + data: JSON.stringify({ bytes }) + }; } return value; @@ -451,6 +458,9 @@ namespace ObjectsTransferrer { const start = new Position(obj.start.line, obj.start.character); const end = new Position(obj.end.line, obj.end.character); return new Range(start, end); + case SerializedObjectType.TEXT_BUFFER: + const data: { bytes: number[] } = JSON.parse(value.data); + return BinaryBuffer.wrap(Uint8Array.from(data.bytes)); } } @@ -467,7 +477,8 @@ interface SerializedObject { enum SerializedObjectType { THEIA_URI, VSCODE_URI, - THEIA_RANGE + THEIA_RANGE, + TEXT_BUFFER } function isSerializedObject(obj: any): obj is SerializedObject { diff --git a/packages/plugin-ext/src/common/uint.ts b/packages/plugin-ext/src/common/uint.ts new file mode 100644 index 0000000000000..b5971f51ac171 --- /dev/null +++ b/packages/plugin-ext/src/common/uint.ts @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/base/common/uint.ts + +export const enum Constants { + /** + * Max unsigned integer that fits on 8 bits. + */ + MAX_UINT_8 = 255, // 2^8 - 1 +} + +export function toUint8(v: number): number { + if (v < 0) { + return 0; + } + if (v > Constants.MAX_UINT_8) { + return Constants.MAX_UINT_8; + } + return v | 0; +} diff --git a/packages/plugin-ext/src/common/uri-components.ts b/packages/plugin-ext/src/common/uri-components.ts index 830236050659f..20f3a46649354 100644 --- a/packages/plugin-ext/src/common/uri-components.ts +++ b/packages/plugin-ext/src/common/uri-components.ts @@ -13,6 +13,10 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import URI from '@theia/core/lib/common/uri'; @@ -26,23 +30,61 @@ export interface UriComponents { } // some well known URI schemas +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/base/common/network.ts#L9-L79 +// TODO move to network.ts file export namespace Schemes { - export const FILE = 'file'; - export const UNTITLED = 'untitled'; - export const HTTP: string = 'http'; - export const HTTPS: string = 'https'; - export const MAILTO: string = 'mailto'; - export const DATA: string = 'data'; - /** - * A schema is used for models that exist in memory - * only and that have no correspondence on a server or such. - */ - export const IN_MEMORY: string = 'inmemory'; - /** A schema is used for settings files. */ - export const VSCODE: string = 'vscode'; - /** A schema is used for internal private files. */ - export const INTERNAL: string = 'private'; - export const COMMAND: string = 'command'; + + /** + * A schema that is used for models that exist in memory + * only and that have no correspondence on a server or such. + */ + export const inMemory = 'inmemory'; + + /** + * A schema that is used for setting files + */ + export const vscode = 'vscode'; + + /** + * A schema that is used for internal private files + */ + export const internal = 'private'; + + /** + * A walk-through document. + */ + export const walkThrough = 'walkThrough'; + + /** + * An embedded code snippet. + */ + export const walkThroughSnippet = 'walkThroughSnippet'; + + export const http = 'http'; + + export const https = 'https'; + + export const file = 'file'; + + export const mailto = 'mailto'; + + export const untitled = 'untitled'; + + export const data = 'data'; + + export const command = 'command'; + + export const vscodeRemote = 'vscode-remote'; + + export const vscodeRemoteResource = 'vscode-remote-resource'; + + export const userData = 'vscode-userdata'; + + export const vscodeCustomEditor = 'vscode-custom-editor'; + + export const vscodeSettings = 'vscode-settings'; + + export const webviewPanel = 'webview-panel'; } export function theiaUritoUriComponents(uri: URI): UriComponents { diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index 8293be0e9949a..df8414a1bf3c1 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -47,7 +47,6 @@ import { Deferred } from '@theia/core/lib/common/promise-util'; import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager'; import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager'; import { WaitUntilEvent } from '@theia/core/lib/common/event'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { FileSearchService } from '@theia/file-search/lib/common/file-search-service'; import { Emitter, isCancelled } from '@theia/core'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; @@ -62,6 +61,7 @@ import URI from '@theia/core/lib/common/uri'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { environment } from '@theia/application-package/lib/environment'; import { JsonSchemaStore } from '@theia/core/lib/browser/json-schema-store'; +import { FileService, FileSystemProviderActivationEvent } from '@theia/filesystem/lib/browser/file-service'; export type PluginHost = 'frontend' | string; export type DebugActivationEvent = 'onDebugResolve' | 'onDebugInitialConfigurations' | 'onDebugAdapterProtocolTracker'; @@ -115,8 +115,8 @@ export class HostedPluginSupport { @inject(DebugConfigurationManager) protected readonly debugConfigurationManager: DebugConfigurationManager; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; @inject(FileSearchService) protected readonly fileSearchService: FileSearchService; @@ -196,6 +196,7 @@ export class HostedPluginSupport { this.viewRegistry.onDidExpandView(id => this.activateByView(id)); this.taskProviderRegistry.onWillProvideTaskProvider(event => this.ensureTaskActivation(event)); this.taskResolverRegistry.onWillProvideTaskResolver(event => this.ensureTaskActivation(event)); + this.fileService.onWillActivateFileSystemProvider(event => this.ensureFileSystemActivation(event)); this.widgets.onDidCreateWidget(({ factoryId, widget }) => { if (factoryId === WebviewWidget.FACTORY_ID && widget instanceof WebviewWidget) { const storeState = widget.storeState.bind(widget); @@ -440,8 +441,8 @@ export class HostedPluginSupport { this.server.getExtPluginAPI(), this.pluginServer.getAllStorageValues(undefined), this.pluginServer.getAllStorageValues({ - workspace: this.workspaceService.workspace, - roots: this.workspaceService.tryGetRoots() + workspace: this.workspaceService.workspace?.resource.toString(), + roots: this.workspaceService.tryGetRoots().map(root => root.resource.toString()) }), this.webviewEnvironment.resourceRoot(), this.webviewEnvironment.cspSource(), @@ -505,18 +506,18 @@ export class HostedPluginSupport { protected async getStoragePath(): Promise { const roots = await this.workspaceService.roots; - return this.pluginPathsService.getHostStoragePath(this.workspaceService.workspace, roots); + return this.pluginPathsService.getHostStoragePath(this.workspaceService.workspace?.resource.toString(), roots.map(root => root.resource.toString())); } protected async getHostGlobalStoragePath(): Promise { const configDirUri = await this.envServer.getConfigDirUri(); - const globalStorageFolderUri = new URI(configDirUri).resolve('globalStorage').toString(); + const globalStorageFolderUri = new URI(configDirUri).resolve('globalStorage'); // Make sure that folder by the path exists - if (!await this.fileSystem.exists(globalStorageFolderUri)) { - await this.fileSystem.createFolder(globalStorageFolderUri); + if (!await this.fileService.exists(globalStorageFolderUri)) { + await this.fileService.createFolder(globalStorageFolderUri); } - const globalStorageFolderFsPath = await this.fileSystem.getFsPath(globalStorageFolderUri); + const globalStorageFolderFsPath = await this.fileService.fsPath(globalStorageFolderUri); if (!globalStorageFolderFsPath) { throw new Error(`Could not resolve the FS path for URI: ${globalStorageFolderUri}`); } @@ -551,6 +552,14 @@ export class HostedPluginSupport { await this.activateByEvent(`onCommand:${commandId}`); } + activateByFileSystem(event: FileSystemProviderActivationEvent): Promise { + return this.activateByEvent(`onFileSystem:${event.scheme}`); + } + + protected ensureFileSystemActivation(event: FileSystemProviderActivationEvent): void { + event.waitUntil(this.activateByFileSystem(event)); + } + protected ensureCommandHandlerRegistration(event: WillExecuteCommandEvent): void { const activation = this.activateByCommand(event.commandId); if (this.commands.getCommand(event.commandId) && @@ -625,7 +634,7 @@ export class HostedPluginSupport { promises.push((async () => { try { const result = await this.fileSearchService.find('', { - rootUris: this.workspaceService.tryGetRoots().map(r => r.uri), + rootUris: this.workspaceService.tryGetRoots().map(r => r.resource.toString()), includePatterns, limit: 1 }, tokenSource.token); diff --git a/packages/plugin-ext/src/main/browser/debug/debug-main.ts b/packages/plugin-ext/src/main/browser/debug/debug-main.ts index b6c3fcc968202..58b6a0128276f 100644 --- a/packages/plugin-ext/src/main/browser/debug/debug-main.ts +++ b/packages/plugin-ext/src/main/browser/debug/debug-main.ts @@ -47,9 +47,9 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa import { PluginDebugSessionFactory } from './plugin-debug-session-factory'; import { PluginWebSocketChannel } from '../../../common/connection'; import { PluginDebugAdapterContributionRegistrator, PluginDebugService } from './plugin-debug-service'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin'; import { DebugFunctionBreakpoint } from '@theia/debug/lib/browser/model/debug-function-breakpoint'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export class DebugMainImpl implements DebugMain, Disposable { private readonly debugExt: DebugExt; @@ -66,7 +66,7 @@ export class DebugMainImpl implements DebugMain, Disposable { private readonly debugPreferences: DebugPreferences; private readonly sessionContributionRegistrator: PluginDebugSessionContributionRegistrator; private readonly adapterContributionRegistrator: PluginDebugAdapterContributionRegistrator; - private readonly fileSystem: FileSystem; + private readonly fileService: FileService; private readonly pluginService: HostedPluginSupport; private readonly debuggerContributions = new Map(); @@ -86,7 +86,7 @@ export class DebugMainImpl implements DebugMain, Disposable { this.debugPreferences = container.get(DebugPreferences); this.adapterContributionRegistrator = container.get(PluginDebugService); this.sessionContributionRegistrator = container.get(PluginDebugSessionContributionRegistry); - this.fileSystem = container.get(FileSystem); + this.fileService = container.get(FileService); this.pluginService = container.get(HostedPluginSupport); const fireDidChangeBreakpoints = ({ added, removed, changed }: BreakpointsChangeEvent) => { @@ -140,7 +140,7 @@ export class DebugMainImpl implements DebugMain, Disposable { const connection = await this.connectionMain.ensureConnection(sessionId); return new PluginWebSocketChannel(connection); }, - this.fileSystem, + this.fileService, terminalOptionsExt ); diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts index 10f88a403e2e0..4ab6ffd2b8057 100644 --- a/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-session-factory.ts @@ -26,9 +26,9 @@ import { DebugSessionOptions } from '@theia/debug/lib/browser/debug-session-opti import { DebugSession } from '@theia/debug/lib/browser/debug-session'; import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection'; import { IWebSocket } from 'vscode-ws-jsonrpc/lib/socket/socket'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { TerminalWidgetOptions, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { TerminalOptionsExt } from '../../../common/plugin-api-rpc'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export class PluginDebugSession extends DebugSession { constructor( @@ -40,9 +40,9 @@ export class PluginDebugSession extends DebugSession { protected readonly breakpoints: BreakpointManager, protected readonly labelProvider: LabelProvider, protected readonly messages: MessageClient, - protected readonly fileSystem: FileSystem, + protected readonly fileService: FileService, protected readonly terminalOptionsExt: TerminalOptionsExt | undefined) { - super(id, options, connection, terminalServer, editorManager, breakpoints, labelProvider, messages, fileSystem); + super(id, options, connection, terminalServer, editorManager, breakpoints, labelProvider, messages, fileService); } protected async doCreateTerminal(terminalWidgetOptions: TerminalWidgetOptions): Promise { @@ -65,7 +65,7 @@ export class PluginDebugSessionFactory extends DefaultDebugSessionFactory { protected readonly outputChannelManager: OutputChannelManager, protected readonly debugPreferences: DebugPreferences, protected readonly connectionFactory: (sessionId: string) => Promise, - protected readonly fileSystem: FileSystem, + protected readonly fileService: FileService, protected readonly terminalOptionsExt: TerminalOptionsExt | undefined ) { super(); @@ -86,7 +86,7 @@ export class PluginDebugSessionFactory extends DefaultDebugSessionFactory { this.breakpoints, this.labelProvider, this.messages, - this.fileSystem, + this.fileService, this.terminalOptionsExt); } } diff --git a/packages/plugin-ext/src/main/browser/dialogs-main.ts b/packages/plugin-ext/src/main/browser/dialogs-main.ts index d04e51453aa9c..0abafda7bd388 100644 --- a/packages/plugin-ext/src/main/browser/dialogs-main.ts +++ b/packages/plugin-ext/src/main/browser/dialogs-main.ts @@ -19,15 +19,18 @@ import { RPCProtocol } from '../../common/rpc-protocol'; import { OpenDialogOptionsMain, SaveDialogOptionsMain, DialogsMain, UploadDialogOptionsMain } from '../../common/plugin-api-rpc'; import { DirNode, OpenFileDialogProps, SaveFileDialogProps, OpenFileDialogFactory, SaveFileDialogFactory, SaveFileDialog } from '@theia/filesystem/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import { UriSelection } from '@theia/core/lib/common/selection'; import URI from '@theia/core/lib/common/uri'; import { FileUploadService } from '@theia/filesystem/lib/browser/file-upload-service'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat } from '@theia/filesystem/lib/common/files'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; export class DialogsMainImpl implements DialogsMain { private workspaceService: WorkspaceService; - private fileSystem: FileSystem; + private fileService: FileService; + private environments: EnvVariablesServer; private openFileDialogFactory: OpenFileDialogFactory; private saveFileDialogFactory: SaveFileDialogFactory; @@ -35,7 +38,8 @@ export class DialogsMainImpl implements DialogsMain { constructor(rpc: RPCProtocol, container: interfaces.Container) { this.workspaceService = container.get(WorkspaceService); - this.fileSystem = container.get(FileSystem); + this.fileService = container.get(FileService); + this.environments = container.get(EnvVariablesServer); this.openFileDialogFactory = container.get(OpenFileDialogFactory); this.saveFileDialogFactory = container.get(SaveFileDialogFactory); @@ -43,16 +47,24 @@ export class DialogsMainImpl implements DialogsMain { } protected async getRootUri(defaultUri: string | undefined): Promise { - let rootStat; + let rootStat: FileStat | undefined; // Try to use default URI as root if (defaultUri) { - rootStat = await this.fileSystem.getFileStat(defaultUri); + try { + rootStat = await this.fileService.resolve(new URI(defaultUri)); + } catch { + rootStat = undefined; + } } // Try to use as root the parent folder of existing file URI/non existing URI if (rootStat && !rootStat.isDirectory || !rootStat) { - rootStat = await this.fileSystem.getFileStat(new URI(defaultUri).parent.toString()); + try { + rootStat = await this.fileService.resolve(new URI(defaultUri).parent); + } catch { + rootStat = undefined; + } } // Try to use workspace service root if there is no preconfigured URI @@ -62,7 +74,10 @@ export class DialogsMainImpl implements DialogsMain { // Try to use current user home if root folder is still not taken if (!rootStat) { - rootStat = await this.fileSystem.getCurrentUserHome(); + const homeDirUri = await this.environments.getHomeDirUri(); + try { + rootStat = await this.fileService.resolve(new URI(homeDirUri)); + } catch { } } return rootStat; @@ -137,7 +152,10 @@ export class DialogsMainImpl implements DialogsMain { // File name field should be empty unless the URI is a file let fileNameValue = ''; if (options.defaultUri) { - const defaultURIStat = await this.fileSystem.getFileStat(options.defaultUri); + let defaultURIStat: FileStat | undefined; + try { + defaultURIStat = await this.fileService.resolve(new URI(options.defaultUri)); + } catch { } if (defaultURIStat && !defaultURIStat.isDirectory || !defaultURIStat) { fileNameValue = new URI(options.defaultUri).path.base; } @@ -178,7 +196,7 @@ export class DialogsMainImpl implements DialogsMain { throw new Error('Failed to resolve base directory where files should be uploaded'); } - const uploadResult = await this.uploadService.upload(rootStat.uri); + const uploadResult = await this.uploadService.upload(rootStat.resource.toString()); if (uploadResult) { return uploadResult.uploaded; diff --git a/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts b/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts index eefc929797bb2..19c7240dbb782 100644 --- a/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts +++ b/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts @@ -16,11 +16,11 @@ import { Emitter, Event } from '@theia/core/lib/common/event'; import { injectable, inject } from 'inversify'; -import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; -import { Resource, ResourceResolver, ResourceVersion } from '@theia/core'; +import { Resource, ResourceResolver, ResourceVersion, ResourceSaveOptions } from '@theia/core/lib/common/resource'; import URI from '@theia/core/lib/common/uri'; import { Schemes } from '../../../common/uri-components'; import { FileResource, FileResourceResolver } from '@theia/filesystem/lib/browser'; +import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; let index = 0; @@ -33,7 +33,7 @@ export class UntitledResourceResolver implements ResourceResolver { protected readonly resources = new Map(); async resolve(uri: URI): Promise { - if (uri.scheme !== Schemes.UNTITLED) { + if (uri.scheme !== Schemes.untitled) { throw new Error('The given uri is not untitled file uri: ' + uri); } else { const untitledResource = this.resources.get(uri.toString()); @@ -45,7 +45,7 @@ export class UntitledResourceResolver implements ResourceResolver { } } - async createUntitledResource(fileResourceResolver: FileResourceResolver, content?: string, language?: string, uri?: URI): Promise { + async createUntitledResource(fileResourceResolver: FileResourceResolver, content?: string, language?: string, uri?: URI): Promise { let extension; if (language) { for (const lang of monaco.languages.getLanguages()) { @@ -57,7 +57,7 @@ export class UntitledResourceResolver implements ResourceResolver { } } } - return new UntitledResource(this.resources, uri ? uri : new URI().withScheme(Schemes.UNTITLED).withPath(`/Untitled-${index++}${extension ? extension : ''}`), + return new UntitledResource(this.resources, uri ? uri : new URI().withScheme(Schemes.untitled).withPath(`/Untitled-${index++}${extension ? extension : ''}`), fileResourceResolver, content); } } @@ -91,7 +91,7 @@ export class UntitledResource implements Resource { } } - async saveContents(content: string, options?: { encoding?: string, overwriteEncoding?: string }): Promise { + async saveContents(content: string, options?: { encoding?: string, overwriteEncoding?: boolean }): Promise { if (!this.fileResource) { this.fileResource = await this.fileResourceResolver.resolve(new URI(this.uri.path.toString())); if (this.fileResource.onDidChangeContents) { @@ -101,8 +101,8 @@ export class UntitledResource implements Resource { await this.fileResource.saveContents(content, options); } - async saveContentChanges(changes: TextDocumentContentChangeEvent[], options?: { encoding?: string, overwriteEncoding?: string }): Promise { - if (!this.fileResource) { + async saveContentChanges(changes: TextDocumentContentChangeEvent[], options?: ResourceSaveOptions): Promise { + if (!this.fileResource || !this.fileResource.saveContentChanges) { throw new Error('FileResource is not available for: ' + this.uri.path.toString()); } await this.fileResource.saveContentChanges(changes, options); @@ -124,6 +124,13 @@ export class UntitledResource implements Resource { } return undefined; } + + get encoding(): string | undefined { + if (this.fileResource) { + return this.fileResource.encoding; + } + return undefined; + } } export function createUntitledURI(language?: string): URI { @@ -138,5 +145,5 @@ export function createUntitledURI(language?: string): URI { } } } - return new URI().withScheme(Schemes.UNTITLED).withPath(`/Untitled-${index++}${extension ? extension : ''}`); + return new URI().withScheme(Schemes.untitled).withPath(`/Untitled-${index++}${extension ? extension : ''}`); } diff --git a/packages/plugin-ext/src/main/browser/file-system-main-impl.ts b/packages/plugin-ext/src/main/browser/file-system-main-impl.ts new file mode 100644 index 0000000000000..17e406e866852 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/file-system-main-impl.ts @@ -0,0 +1,253 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/api/browser/mainThreadFileSystem.ts + +/* eslint-disable max-len */ +/* eslint-disable @typescript-eslint/tslint/config */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { URI } from 'vscode-uri'; +import { interfaces } from 'inversify'; +import CoreURI from '@theia/core/lib/common/uri'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; +import { Disposable } from '@theia/core/lib/common/disposable'; +import { Event, Emitter } from '@theia/core/lib/common/event'; +import { MAIN_RPC_CONTEXT, FileSystemMain, FileSystemExt, IFileChangeDto } from '../../common/plugin-api-rpc'; +import { RPCProtocol } from '../../common/rpc-protocol'; +import { UriComponents } from '../../common/uri-components'; +import { + FileSystemProviderCapabilities, Stat, FileType, FileSystemProviderErrorCode, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, FileWriteOptions, WatchOptions, + FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability, + FileStat, FileChange, FileOperationError, FileOperationResult +} from '@theia/filesystem/lib/common/files'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; + +type IDisposable = Disposable; + +export class FileSystemMainImpl implements FileSystemMain, Disposable { + + private readonly _proxy: FileSystemExt; + private readonly _fileProvider = new Map(); + private readonly _fileService: FileService; + + constructor(rpc: RPCProtocol, container: interfaces.Container) { + this._proxy = rpc.getProxy(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT); + this._fileService = container.get(FileService); + } + + dispose(): void { + this._fileProvider.forEach(value => value.dispose()); + this._fileProvider.clear(); + } + + $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): void { + this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy)); + } + + $unregisterProvider(handle: number): void { + const provider = this._fileProvider.get(handle); + if (provider) { + provider.dispose(); + this._fileProvider.delete(handle); + } + } + + $onFileSystemChange(handle: number, changes: IFileChangeDto[]): void { + const fileProvider = this._fileProvider.get(handle); + if (!fileProvider) { + throw new Error('Unknown file provider'); + } + fileProvider.$onFileSystemChange(changes); + } + + // --- consumer fs, vscode.workspace.fs + + $stat(uri: UriComponents): Promise { + return this._fileService.resolve(new CoreURI(URI.revive(uri)), { resolveMetadata: true }).then(stat => ({ + ctime: stat.ctime, + mtime: stat.mtime, + size: stat.size, + type: FileStat.asFileType(stat) + })).catch(FileSystemMainImpl._handleError); + } + + $readdir(uri: UriComponents): Promise<[string, FileType][]> { + return this._fileService.resolve(new CoreURI(URI.revive(uri)), { resolveMetadata: false }).then(stat => { + if (!stat.isDirectory) { + const err = new Error(stat.name); + err.name = FileSystemProviderErrorCode.FileNotADirectory; + throw err; + } + return !stat.children ? [] : stat.children.map(child => [child.name, FileStat.asFileType(child)] as [string, FileType]); + }).catch(FileSystemMainImpl._handleError); + } + + $readFile(uri: UriComponents): Promise { + return this._fileService.readFile(new CoreURI(URI.revive(uri))).then(file => file.value).catch(FileSystemMainImpl._handleError); + } + + $writeFile(uri: UriComponents, content: BinaryBuffer): Promise { + return this._fileService.writeFile(new CoreURI(URI.revive(uri)), content) + .then(() => undefined).catch(FileSystemMainImpl._handleError); + } + + $rename(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise { + return this._fileService.move(new CoreURI(URI.revive(source)), new CoreURI(URI.revive(target)), { + ...opts, + fromUserGesture: false + }).then(() => undefined).catch(FileSystemMainImpl._handleError); + } + + $copy(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise { + return this._fileService.copy(new CoreURI(URI.revive(source)), new CoreURI(URI.revive(target)), { + ...opts, + fromUserGesture: false + }).then(() => undefined).catch(FileSystemMainImpl._handleError); + } + + $mkdir(uri: UriComponents): Promise { + return this._fileService.createFolder(new CoreURI(URI.revive(uri))) + .then(() => undefined).catch(FileSystemMainImpl._handleError); + } + + $delete(uri: UriComponents, opts: FileDeleteOptions): Promise { + return this._fileService.delete(new CoreURI(URI.revive(uri)), opts).catch(FileSystemMainImpl._handleError); + } + + private static _handleError(err: any): never { + if (err instanceof FileOperationError) { + switch (err.fileOperationResult) { + case FileOperationResult.FILE_NOT_FOUND: + err.name = FileSystemProviderErrorCode.FileNotFound; + break; + case FileOperationResult.FILE_IS_DIRECTORY: + err.name = FileSystemProviderErrorCode.FileIsADirectory; + break; + case FileOperationResult.FILE_PERMISSION_DENIED: + err.name = FileSystemProviderErrorCode.NoPermissions; + break; + case FileOperationResult.FILE_MOVE_CONFLICT: + err.name = FileSystemProviderErrorCode.FileExists; + break; + } + } + + throw err; + } + +} + +class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability { + + private readonly _onDidChange = new Emitter(); + private readonly _registration: IDisposable; + + readonly onDidChangeFile: Event = this._onDidChange.event; + + readonly capabilities: FileSystemProviderCapabilities; + readonly onDidChangeCapabilities: Event = Event.None; + + constructor( + fileService: FileService, + scheme: string, + capabilities: FileSystemProviderCapabilities, + private readonly _handle: number, + private readonly _proxy: FileSystemExt + ) { + this.capabilities = capabilities; + this._registration = fileService.registerProvider(scheme, this); + } + + dispose(): void { + this._registration.dispose(); + this._onDidChange.dispose(); + } + + watch(resource: CoreURI, opts: WatchOptions) { + const session = Math.random(); + this._proxy.$watch(this._handle, session, resource['codeUri'], opts); + return Disposable.create(() => { + this._proxy.$unwatch(this._handle, session); + }); + } + + $onFileSystemChange(changes: IFileChangeDto[]): void { + this._onDidChange.fire(changes.map(RemoteFileSystemProvider._createFileChange)); + } + + private static _createFileChange(dto: IFileChangeDto): FileChange { + return { resource: new CoreURI(URI.revive(dto.resource)), type: dto.type }; + } + + // --- forwarding calls + + stat(resource: CoreURI): Promise { + return this._proxy.$stat(this._handle, resource['codeUri']).then(undefined, err => { + throw err; + }); + } + + readFile(resource: CoreURI): Promise { + return this._proxy.$readFile(this._handle, resource['codeUri']).then(buffer => buffer.buffer); + } + + writeFile(resource: CoreURI, content: Uint8Array, opts: FileWriteOptions): Promise { + return this._proxy.$writeFile(this._handle, resource['codeUri'], BinaryBuffer.wrap(content), opts); + } + + delete(resource: CoreURI, opts: FileDeleteOptions): Promise { + return this._proxy.$delete(this._handle, resource['codeUri'], opts); + } + + mkdir(resource: CoreURI): Promise { + return this._proxy.$mkdir(this._handle, resource['codeUri']); + } + + readdir(resource: CoreURI): Promise<[string, FileType][]> { + return this._proxy.$readdir(this._handle, resource['codeUri']); + } + + rename(resource: CoreURI, target: CoreURI, opts: FileOverwriteOptions): Promise { + return this._proxy.$rename(this._handle, resource['codeUri'], target['codeUri'], opts); + } + + copy(resource: CoreURI, target: CoreURI, opts: FileOverwriteOptions): Promise { + return this._proxy.$copy(this._handle, resource['codeUri'], target['codeUri'], opts); + } + + open(resource: CoreURI, opts: FileOpenOptions): Promise { + return this._proxy.$open(this._handle, resource['codeUri'], opts); + } + + close(fd: number): Promise { + return this._proxy.$close(this._handle, fd); + } + + read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + return this._proxy.$read(this._handle, fd, pos, length).then(readData => { + data.set(readData.buffer, offset); + return readData.byteLength; + }); + } + + write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + return this._proxy.$write(this._handle, fd, pos, BinaryBuffer.wrap(data).slice(offset, offset + length)); + } +} diff --git a/packages/plugin-ext/src/main/browser/file-system-main.ts b/packages/plugin-ext/src/main/browser/file-system-main.ts deleted file mode 100644 index 42f6e1dc9cb31..0000000000000 --- a/packages/plugin-ext/src/main/browser/file-system-main.ts +++ /dev/null @@ -1,201 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2019 Red Hat, Inc. and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { interfaces, injectable } from 'inversify'; -import { URI as Uri } from 'vscode-uri'; -import { Disposable, ResourceResolver, DisposableCollection } from '@theia/core'; -import { Resource, ResourceProvider } from '@theia/core/lib/common/resource'; -import URI from '@theia/core/lib/common/uri'; -import { MAIN_RPC_CONTEXT, FileSystemMain, FileSystemExt } from '../../common/plugin-api-rpc'; -import { RPCProtocol } from '../../common/rpc-protocol'; -import { UriComponents } from '../../common/uri-components'; -import { FileStat, FileType } from '../../plugin/types-impl'; - -export class FileSystemMainImpl implements FileSystemMain, Disposable { - - private readonly proxy: FileSystemExt; - private readonly resourceResolver: FSResourceResolver; - private readonly resourceProvider: ResourceProvider; - private readonly providers = new Map(); - private readonly providersBySchema = new Map(); - private readonly toDispose = new DisposableCollection(); - - constructor(rpc: RPCProtocol, container: interfaces.Container) { - this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT); - this.resourceResolver = container.get(FSResourceResolver); - this.resourceProvider = container.get(ResourceProvider); - } - - dispose(): void { - this.toDispose.dispose(); - } - - async $registerFileSystemProvider(handle: number, scheme: string): Promise { - const toDispose = new DisposableCollection( - this.resourceResolver.registerResourceProvider(handle, scheme, this.proxy), - Disposable.create(() => { - this.providers.delete(handle); - this.providersBySchema.delete(scheme); - }) - ); - this.providers.set(handle, toDispose); - if (this.providersBySchema.has(scheme)) { - throw new Error(`Resource Provider for scheme '${scheme}' is already registered`); - } - this.providersBySchema.set(scheme, handle); - this.toDispose.push(toDispose); - } - - $unregisterProvider(handle: number): void { - const disposable = this.providers.get(handle); - if (disposable) { - disposable.dispose(); - } - } - - private getHandle(uri: UriComponents): number { - const handle = this.providersBySchema.get(uri.scheme); - if (handle === undefined) { - throw new Error(`'No available file system provider for schema ${uri.scheme}`); - } - return handle; - } - - // currently only support registered file system providers (and not real file system) - async $stat(uriComponents: UriComponents): Promise { - const uri = Uri.revive(uriComponents); - const handle = this.getHandle(uri); - return this.proxy.$stat(handle, uri); - } - - // currently only support registered file system providers (and not real file system) - async $readDirectory(uriComponents: UriComponents): Promise<[string, FileType][]> { - const uri = Uri.revive(uriComponents); - const handle = this.getHandle(uri); - return this.proxy.$readDirectory(handle, uri); - } - - // currently only support registered file system providers (and not real file system) - async $createDirectory(uriComponents: UriComponents): Promise { - const uri = Uri.revive(uriComponents); - const handle = this.getHandle(uri); - return this.proxy.$createDirectory(handle, uri); - } - - async $readFile(uriComponents: UriComponents): Promise { - const uri = Uri.revive(uriComponents); - const resource = await this.resourceProvider(new URI(uri)); - return resource.readContents(); - } - - async $writeFile(uriComponents: UriComponents, content: string): Promise { - const uri = Uri.revive(uriComponents); - const resource = await this.resourceProvider(new URI(uri)); - if (!resource.saveContents) { - throw new Error(`'No write operation available on the resource for URI ${uriComponents}`); - } - return resource.saveContents(content); - } - - // currently only support registered file system providers (and not real file system) - async $delete(uriComponents: UriComponents, options: { recursive: boolean }): Promise { - const uri = Uri.revive(uriComponents); - const handle = this.getHandle(uri); - return this.proxy.$delete(handle, uri, options); - } - - // currently only support registered file system providers (and not real file system) - async $rename(source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise { - const sourceUri = Uri.revive(source); - const targetUri = Uri.revive(target); - const sourceHandle = this.getHandle(sourceUri); - if (sourceHandle !== this.getHandle(targetUri)) { - throw new Error(`'No matching file system provider for ${sourceUri} and ${targetUri}`); - } - return this.proxy.$rename(sourceHandle, sourceUri, targetUri, options); - } - - // currently only support registered file system providers (and not real file system) - async $copy(source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise { - const sourceUri = Uri.revive(source); - const targetUri = Uri.revive(target); - const sourceHandle = this.getHandle(sourceUri); - if (sourceHandle !== this.getHandle(targetUri)) { - throw new Error(`'No matching file system provider for ${sourceUri} and ${targetUri}`); - } - return this.proxy.$copy(sourceHandle, sourceUri, targetUri, options); - } -} - -@injectable() -export class FSResourceResolver implements ResourceResolver, Disposable { - - // resource providers by schemas - private providers = new Map(); - private toDispose = new DisposableCollection(); - - resolve(uri: URI): Resource { - const provider = this.providers.get(uri.scheme); - if (provider) { - return provider.create(uri); - } - throw new Error(`Unable to find a Resource Provider for scheme '${uri.scheme}'`); - } - - dispose(): void { - this.toDispose.dispose(); - } - - registerResourceProvider(handle: number, scheme: string, proxy: FileSystemExt): Disposable { - if (this.providers.has(scheme)) { - throw new Error(`Resource Provider for scheme '${scheme}' is already registered`); - } - - const provider = new FSResourceProvider(handle, proxy); - this.providers.set(scheme, provider); - - const disposable = Disposable.create(() => { - this.providers.delete(scheme); - }); - this.toDispose.push(disposable); - return disposable; - } -} - -class FSResourceProvider { - - constructor(private handle: number, private proxy: FileSystemExt) { } - - create(uri: URI): Resource { - return new FSResource(this.handle, uri, this.proxy); - } -} - -/** Resource that delegates reading/saving a content to a plugin's FileSystemProvider. */ -export class FSResource implements Resource { - - constructor(private handle: number, public uri: URI, private proxy: FileSystemExt) { } - - readContents(options?: { encoding?: string }): Promise { - return this.proxy.$readFile(this.handle, Uri.parse(this.uri.toString()), options); - } - - saveContents(content: string, options?: { encoding?: string }): Promise { - return this.proxy.$writeFile(this.handle, Uri.parse(this.uri.toString()), content, options); - } - - dispose(): void { } -} diff --git a/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts b/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts deleted file mode 100644 index a49f26dc24bee..0000000000000 --- a/packages/plugin-ext/src/main/browser/in-plugin-filesystem-watcher-manager.ts +++ /dev/null @@ -1,125 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { injectable, inject, postConstruct } from 'inversify'; -import { parse, ParsedPattern, IRelativePattern } from '@theia/callhierarchy/lib/common/glob'; -import { FileSystemWatcher, FileChangeEvent, FileChangeType, FileChange } from '@theia/filesystem/lib/browser/filesystem-watcher'; -import { WorkspaceExt } from '../../common/plugin-api-rpc'; -import { FileWatcherSubscriberOptions } from '../../common/plugin-api-rpc-model'; -import { RelativePattern } from '../../plugin/types-impl'; -import { theiaUritoUriComponents } from '../../common/uri-components'; - -/** - * Actual implementation of file watching system of the plugin API. - * Holds subscriptions (with its settings) from within plugins - * and process all file system events in all workspace roots whether they matches to any subscription. - * Only if event matches it will be sent into plugin side to specific subscriber. - */ -@injectable() -export class InPluginFileSystemWatcherManager { - - private readonly subscribers = new Map(); - private nextSubscriberId = 0; - - @inject(FileSystemWatcher) - private readonly fileSystemWatcher: FileSystemWatcher; - - @postConstruct() - protected init(): void { - this.fileSystemWatcher.onFilesChanged(event => this.onFilesChangedEventHandler(event)); - } - - // Filter file system changes according to subscribers settings here to avoid unneeded traffic. - protected onFilesChangedEventHandler(changes: FileChangeEvent): void { - for (const change of changes) { - switch (change.type) { - case FileChangeType.UPDATED: - for (const [id, subscriber] of this.subscribers) { - if (!subscriber.ignoreChangeEvents && this.uriMatches(subscriber, change)) { - subscriber.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'updated' }); - } - } - break; - case FileChangeType.ADDED: - for (const [id, subscriber] of this.subscribers) { - if (!subscriber.ignoreCreateEvents && this.uriMatches(subscriber, change)) { - subscriber.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'created' }); - } - } - break; - case FileChangeType.DELETED: - for (const [id, subscriber] of this.subscribers) { - if (!subscriber.ignoreDeleteEvents && this.uriMatches(subscriber, change)) { - subscriber.proxy.$fileChanged({ subscriberId: id, uri: theiaUritoUriComponents(change.uri), type: 'deleted' }); - } - } - break; - } - } - } - - private uriMatches(subscriber: FileWatcherSubscriber, fileChange: FileChange): boolean { - return subscriber.matcher(fileChange.uri.path.toString()); - } - - /** - * Registers new file system events subscriber. - * - * @param options subscription options - * @returns generated subscriber id - */ - registerFileWatchSubscription(options: FileWatcherSubscriberOptions, proxy: WorkspaceExt): string { - const subscriberId = this.getNextId(); - - let globPatternMatcher: ParsedPattern; - if (typeof options.globPattern === 'string') { - globPatternMatcher = parse(options.globPattern); - } else { - const relativePattern: IRelativePattern = new RelativePattern(options.globPattern.base, options.globPattern.pattern); - globPatternMatcher = parse(relativePattern); - } - - const subscriber: FileWatcherSubscriber = { - id: subscriberId, - matcher: globPatternMatcher, - ignoreCreateEvents: options.ignoreCreateEvents === true, - ignoreChangeEvents: options.ignoreChangeEvents === true, - ignoreDeleteEvents: options.ignoreDeleteEvents === true, - proxy - }; - this.subscribers.set(subscriberId, subscriber); - - return subscriberId; - } - - unregisterFileWatchSubscription(subscriptionId: string): void { - this.subscribers.delete(subscriptionId); - } - - private getNextId(): string { - return 'ipfsw' + this.nextSubscriberId++; - } - -} - -interface FileWatcherSubscriber { - id: string; - matcher: ParsedPattern; - ignoreCreateEvents: boolean; - ignoreChangeEvents: boolean; - ignoreDeleteEvents: boolean; - proxy: WorkspaceExt -} diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 8e111ac76e671..ec3ffb9fbde8a 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -34,7 +34,7 @@ import { WebviewsMainImpl } from './webviews-main'; import { TasksMainImpl } from './tasks-main'; import { StorageMainImpl } from './plugin-storage'; import { DebugMainImpl } from './debug/debug-main'; -import { FileSystemMainImpl } from './file-system-main'; +import { FileSystemMainImpl } from './file-system-main-impl'; import { ScmMainImpl } from './scm-main'; import { DecorationsMainImpl } from './decorations/decorations-main'; import { ClipboardMainImpl } from './clipboard-main'; @@ -48,6 +48,7 @@ import { MonacoBulkEditService } from '@theia/monaco/lib/browser/monaco-bulk-edi import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service'; import { UntitledResourceResolver } from './editor/untitled-resource'; import { FileResourceResolver } from '@theia/filesystem/lib/browser'; +import { MainFileSystemEventService } from './main-file-system-event-service'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const commandRegistryMain = new CommandRegistryMainImpl(rpc, container); @@ -125,7 +126,15 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const debugMain = new DebugMainImpl(rpc, connectionMain, container); rpc.set(PLUGIN_RPC_CONTEXT.DEBUG_MAIN, debugMain); - rpc.set(PLUGIN_RPC_CONTEXT.FILE_SYSTEM_MAIN, new FileSystemMainImpl(rpc, container)); + const fs = new FileSystemMainImpl(rpc, container); + const fsEventService = new MainFileSystemEventService(rpc, container); + const disposeFS = fs.dispose.bind(fs); + fs.dispose = () => { + fsEventService.dispose(); + disposeFS(); + }; + + rpc.set(PLUGIN_RPC_CONTEXT.FILE_SYSTEM_MAIN, fs); const scmMain = new ScmMainImpl(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.SCM_MAIN, scmMain); diff --git a/packages/plugin-ext/src/main/browser/main-file-system-event-service.ts b/packages/plugin-ext/src/main/browser/main-file-system-event-service.ts new file mode 100644 index 0000000000000..d01cc0df912eb --- /dev/null +++ b/packages/plugin-ext/src/main/browser/main-file-system-event-service.ts @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts + +import { interfaces } from 'inversify'; +import { RPCProtocol } from '../../common/rpc-protocol'; +import { MAIN_RPC_CONTEXT, FileSystemEvents } from '../../common/plugin-api-rpc'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileChangeType } from '@theia/filesystem/lib/common/files'; + +export class MainFileSystemEventService { + + private readonly toDispose = new DisposableCollection(); + + constructor( + rpc: RPCProtocol, + container: interfaces.Container + ) { + const proxy = rpc.getProxy(MAIN_RPC_CONTEXT.ExtHostFileSystemEventService); + const fileService = container.get(FileService); + + // file system events - (changes the editor and other make) + const events: FileSystemEvents = { + created: [], + changed: [], + deleted: [] + }; + this.toDispose.push(fileService.onDidFilesChange(event => { + for (const change of event.changes) { + switch (change.type) { + case FileChangeType.ADDED: + events.created.push(change.resource['codeUri']); + break; + case FileChangeType.UPDATED: + events.changed.push(change.resource['codeUri']); + break; + case FileChangeType.DELETED: + events.deleted.push(change.resource['codeUri']); + break; + } + } + + proxy.$onFileEvent(events); + events.created.length = 0; + events.changed.length = 0; + events.deleted.length = 0; + })); + + // BEFORE file operation + fileService.addFileOperationParticipant({ + participate: (target, source, operation, timeout, token) => proxy.$onWillRunFileOperation(operation, target['codeUri'], source?.['codeUri'], timeout, token) + }); + + // AFTER file operation + this.toDispose.push(fileService.onDidRunUserOperation(e => proxy.$onDidRunFileOperation(e.operation, e.target['codeUri'], e.source?.['codeUri']))); + } + + dispose(): void { + this.toDispose.dispose(); + } +} diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index 0625126b7d9df..5f141786f9ba5 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -47,7 +47,6 @@ import { PluginDebugSessionContributionRegistry } from './debug/plugin-debug-ses import { PluginDebugService } from './debug/plugin-debug-service'; import { DebugService } from '@theia/debug/lib/common/debug-service'; import { PluginSharedStyle } from './plugin-shared-style'; -import { FSResourceResolver } from './file-system-main'; import { SelectionProviderCommandContribution } from './selection-provider-command'; import { ViewColumnService } from './view-column-service'; import { ViewContextKeyService } from './view/view-context-key-service'; @@ -57,7 +56,6 @@ import { RPCProtocol } from '../../common/rpc-protocol'; import { LanguagesMainFactory, OutputChannelRegistryFactory } from '../../common'; import { LanguagesMainImpl } from './languages-main'; import { OutputChannelRegistryMainImpl } from './output-channel-registry-main'; -import { InPluginFileSystemWatcherManager } from './in-plugin-filesystem-watcher-manager'; import { WebviewWidget } from './webview/webview'; import { WebviewEnvironment } from './webview/webview-environment'; import { WebviewThemeDataProvider } from './webview/webview-theme-data-provider'; @@ -194,11 +192,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(KeybindingsContributionPointHandler).toSelf().inSingletonScope(); bind(PluginContributionHandler).toSelf().inSingletonScope(); - bind(InPluginFileSystemWatcherManager).toSelf().inSingletonScope(); bind(TextContentResourceResolver).toSelf().inSingletonScope(); bind(ResourceResolver).toService(TextContentResourceResolver); - bind(FSResourceResolver).toSelf().inSingletonScope(); - bind(ResourceResolver).toService(FSResourceResolver); bindContributionProvider(bind, MainPluginApiProvider); bind(PluginDebugService).toSelf().inSingletonScope(); diff --git a/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts b/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts index 5564f349cd7c5..25c509d0f5c60 100644 --- a/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts +++ b/packages/plugin-ext/src/main/browser/plugin-icon-theme-service.ts @@ -23,7 +23,6 @@ import debounce = require('lodash.debounce'); import * as jsoncparser from 'jsonc-parser'; import { injectable, inject, postConstruct } from 'inversify'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import { IconThemeService, IconTheme, IconThemeDefinition } from '@theia/core/lib/browser/icon-theme-service'; import { IconThemeContribution, DeployedPlugin, UiTheme, getPluginId } from '../../common/plugin-protocol'; import URI from '@theia/core/lib/common/uri'; @@ -32,9 +31,11 @@ import { Emitter } from '@theia/core/lib/common/event'; import { RecursivePartial } from '@theia/core/lib/common/types'; import { LabelProviderContribution, DidChangeLabelEvent, LabelProvider, URIIconReference } from '@theia/core/lib/browser/label-provider'; import { ThemeType } from '@theia/core/lib/browser/theming'; -import { FileStatNode, DirNode, FileSystemWatcher, FileChangeEvent } from '@theia/filesystem/lib/browser'; +import { FileStatNode, DirNode } from '@theia/filesystem/lib/browser'; import { WorkspaceRootNode } from '@theia/navigator/lib/browser/navigator-tree'; import { Endpoint } from '@theia/core/lib/browser/endpoint'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat, FileChangeType } from '@theia/filesystem/lib/common/files'; export interface PluginIconDefinition { iconPath: string; @@ -97,11 +98,8 @@ export class PluginIconThemeDefinition implements IconThemeDefinition, IconTheme @injectable() export class PluginIconTheme extends PluginIconThemeDefinition implements IconTheme, Disposable { - @inject(FileSystem) - protected readonly fileSystem: FileSystem; - - @inject(FileSystemWatcher) - protected readonly fsWatcher: FileSystemWatcher; + @inject(FileService) + protected readonly fileService: FileService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @@ -192,18 +190,19 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh this.icons.clear(); })); - const { content } = await this.fileSystem.resolveContent(this.uri); + const uri = new URI(this.uri); + const result = await this.fileService.read(uri); + const content = result.value; const json: RecursivePartial = jsoncparser.parse(content, undefined, { disallowComments: false }); this.hidesExplorerArrows = !!json.hidesExplorerArrows; - const uri = new URI(this.uri); - const toUnwatch = await this.fsWatcher.watchFileChanges(uri); + const toUnwatch = this.fileService.watch(uri); if (this.toUnload.disposed) { toUnwatch.dispose(); } else { this.toUnload.push(toUnwatch); - this.toUnload.push(this.fsWatcher.onFilesChanged(e => { - if (FileChangeEvent.isChanged(e, uri)) { + this.toUnload.push(this.fileService.onDidFilesChange(e => { + if (e.contains(uri, FileChangeType.ADDED) || e.contains(uri, FileChangeType.UPDATED)) { this.reload(); } })); @@ -484,13 +483,13 @@ export class PluginIconTheme extends PluginIconThemeDefinition implements IconTh return this.getFolderClassNames(element); } if (FileStatNode.is(element)) { - return this.getFileClassNames(element, element.fileStat.uri); + return this.getFileClassNames(element, element.fileStat.resource.toString()); } if (FileStat.is(element)) { if (element.isDirectory) { return this.getFolderClassNames(element); } - return this.getFileClassNames(element, element.uri); + return this.getFileClassNames(element, element.resource.toString()); } if (URIIconReference.is(element)) { if (element.id === 'folder') { diff --git a/packages/plugin-ext/src/main/browser/plugin-storage.ts b/packages/plugin-ext/src/main/browser/plugin-storage.ts index f7487342d4118..4462b6fc32f6a 100644 --- a/packages/plugin-ext/src/main/browser/plugin-storage.ts +++ b/packages/plugin-ext/src/main/browser/plugin-storage.ts @@ -47,8 +47,8 @@ export class StorageMainImpl implements StorageMain { return undefined; } return { - workspace: this.workspaceService.workspace, - roots: this.workspaceService.tryGetRoots() + workspace: this.workspaceService.workspace?.resource.toString(), + roots: this.workspaceService.tryGetRoots().map(root => root.resource.toString()) }; } diff --git a/packages/plugin-ext/src/main/browser/preference-registry-main.ts b/packages/plugin-ext/src/main/browser/preference-registry-main.ts index 8dd0c4baf880f..ae943398d0321 100644 --- a/packages/plugin-ext/src/main/browser/preference-registry-main.ts +++ b/packages/plugin-ext/src/main/browser/preference-registry-main.ts @@ -31,11 +31,11 @@ import { import { RPCProtocol } from '../../common/rpc-protocol'; import { ConfigurationTarget } from '../../plugin/types-impl'; import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { FileStat } from '@theia/filesystem/lib/common/filesystem'; +import { FileStat } from '@theia/filesystem/lib/common/files'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; export function getPreferences(preferenceProviderProvider: PreferenceProviderProvider, rootFolders: FileStat[]): PreferenceData { - const folders = rootFolders.map(root => root.uri.toString()); + const folders = rootFolders.map(root => root.resource.toString()); // eslint-disable-next-line @typescript-eslint/no-explicit-any return PreferenceScope.getScopes().reduce((result: { [key: number]: any }, scope: PreferenceScope) => { result[scope] = {}; diff --git a/packages/plugin-ext/src/main/browser/text-editor-model-service.ts b/packages/plugin-ext/src/main/browser/text-editor-model-service.ts index 1630176e44141..2a38f48c2c1c1 100644 --- a/packages/plugin-ext/src/main/browser/text-editor-model-service.ts +++ b/packages/plugin-ext/src/main/browser/text-editor-model-service.ts @@ -80,7 +80,7 @@ export class EditorModelService { const saves = []; for (const model of this.monacoModelService.models) { const { uri } = model.textEditorModel; - if (model.dirty && (includeUntitled || uri.scheme !== Schemes.UNTITLED)) { + if (model.dirty && (includeUntitled || uri.scheme !== Schemes.untitled)) { saves.push((async () => { try { await model.save(); diff --git a/packages/plugin-ext/src/main/browser/text-editors-main.ts b/packages/plugin-ext/src/main/browser/text-editors-main.ts index 0ca9cd21c660a..bd69867109a4c 100644 --- a/packages/plugin-ext/src/main/browser/text-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/text-editors-main.ts @@ -117,11 +117,14 @@ export class TextEditorsMainImpl implements TextEditorsMain, Disposable { return Promise.resolve(this.editorsAndDocuments.getEditor(id)!.applyEdits(modelVersionId, edits, opts)); } - $tryApplyWorkspaceEdit(dto: WorkspaceEditDto): Promise { + async $tryApplyWorkspaceEdit(dto: WorkspaceEditDto): Promise { const edits = toMonacoWorkspaceEdit(dto); - return new Promise(resolve => { - this.bulkEditService.apply(edits).then(() => resolve(true), err => resolve(false)); - }); + try { + const { success } = await this.bulkEditService.apply(edits); + return success; + } catch { + return false; + } } $tryInsertSnippet(id: string, template: string, ranges: Range[], opts: UndoStopOptions): Promise { diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index 97f5c2376fceb..ecfba6c3a4baa 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -82,10 +82,10 @@ export const WebviewWidgetExternalEndpoint = Symbol('WebviewWidgetExternalEndpoi export class WebviewWidget extends BaseWidget implements StatefulWidget { private static readonly standardSupportedLinkSchemes = new Set([ - Schemes.HTTP, - Schemes.HTTPS, - Schemes.MAILTO, - Schemes.VSCODE + Schemes.http, + Schemes.https, + Schemes.mailto, + Schemes.vscode ]); static FACTORY_ID = 'plugin-webview'; @@ -414,7 +414,7 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { } return link; } - if (!!this.contentOptions.enableCommandUris && link.scheme === Schemes.COMMAND) { + if (!!this.contentOptions.enableCommandUris && link.scheme === Schemes.command) { return link; } return undefined; diff --git a/packages/plugin-ext/src/main/browser/workspace-main.ts b/packages/plugin-ext/src/main/browser/workspace-main.ts index 53d4d1bff3721..48ceab40b4d0a 100644 --- a/packages/plugin-ext/src/main/browser/workspace-main.ts +++ b/packages/plugin-ext/src/main/browser/workspace-main.ts @@ -19,20 +19,17 @@ import { interfaces, injectable } from 'inversify'; import { WorkspaceExt, StorageExt, MAIN_RPC_CONTEXT, WorkspaceMain, WorkspaceFolderPickOptionsMain } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; import { URI as Uri } from 'vscode-uri'; -import { UriComponents, theiaUritoUriComponents } from '../../common/uri-components'; +import { UriComponents } from '../../common/uri-components'; import { QuickOpenModel, QuickOpenItem, QuickOpenMode } from '@theia/core/lib/browser/quick-open/quick-open-model'; import { MonacoQuickOpenService } from '@theia/monaco/lib/browser/monaco-quick-open-service'; -import { FileStat } from '@theia/filesystem/lib/common'; import { FileSearchService } from '@theia/file-search/lib/common/file-search-service'; import URI from '@theia/core/lib/common/uri'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { Resource } from '@theia/core/lib/common/resource'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Emitter, Event, ResourceResolver, CancellationToken } from '@theia/core'; -import { FileWatcherSubscriberOptions } from '../../common/plugin-api-rpc-model'; -import { InPluginFileSystemWatcherManager } from './in-plugin-filesystem-watcher-manager'; import { PluginServer } from '../../common/plugin-protocol'; -import { FileSystemPreferences, FileSystemWatcher } from '@theia/filesystem/lib/browser'; +import { FileSystemPreferences } from '@theia/filesystem/lib/browser'; import { SearchInWorkspaceService } from '@theia/search-in-workspace/lib/browser/search-in-workspace-service'; export class WorkspaceMainImpl implements WorkspaceMain, Disposable { @@ -47,9 +44,7 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable { private searchInWorkspaceService: SearchInWorkspaceService; - private inPluginFileSystemWatcherManager: InPluginFileSystemWatcherManager; - - private roots: FileStat[]; + private roots: string[]; private resourceResolver: TextContentResourceResolver; @@ -59,8 +54,6 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable { private fsPreferences: FileSystemPreferences; - private fileSystemWatcher: FileSystemWatcher; - protected readonly toDispose = new DisposableCollection(); protected workspaceSearch: Set = new Set(); @@ -75,41 +68,10 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable { this.pluginServer = container.get(PluginServer); this.workspaceService = container.get(WorkspaceService); this.fsPreferences = container.get(FileSystemPreferences); - this.fileSystemWatcher = container.get(FileSystemWatcher); - this.inPluginFileSystemWatcherManager = container.get(InPluginFileSystemWatcherManager); - this.processWorkspaceFoldersChanged(this.workspaceService.tryGetRoots()); + this.processWorkspaceFoldersChanged(this.workspaceService.tryGetRoots().map(root => root.resource.toString())); this.toDispose.push(this.workspaceService.onWorkspaceChanged(roots => { - this.processWorkspaceFoldersChanged(roots); - })); - - this.toDispose.push(this.fileSystemWatcher.onWillCreate(event => { - event.waitUntil(this.proxy.$onWillCreateFiles({ files: [theiaUritoUriComponents(event.uri)] })); - })); - this.toDispose.push(this.fileSystemWatcher.onDidCreate(event => { - this.proxy.$onDidCreateFiles({ files: [theiaUritoUriComponents(event.uri)] }); - })); - this.toDispose.push(this.fileSystemWatcher.onWillMove(event => { - event.waitUntil(this.proxy.$onWillRenameFiles({ - files: [{ - oldUri: theiaUritoUriComponents(event.sourceUri), - newUri: theiaUritoUriComponents(event.targetUri), - }], - })); - })); - this.toDispose.push(this.fileSystemWatcher.onDidMove(event => { - this.proxy.$onDidRenameFiles({ - files: [{ - oldUri: theiaUritoUriComponents(event.sourceUri), - newUri: theiaUritoUriComponents(event.targetUri), - }], - }); - })); - this.toDispose.push(this.fileSystemWatcher.onWillDelete(event => { - event.waitUntil(this.proxy.$onWillDeleteFiles({ files: [theiaUritoUriComponents(event.uri)] })); - })); - this.toDispose.push(this.fileSystemWatcher.onDidDelete(event => { - this.proxy.$onDidDeleteFiles({ files: [theiaUritoUriComponents(event.uri)] }); + this.processWorkspaceFoldersChanged(roots.map(root => root.resource.toString())); })); } @@ -117,7 +79,7 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable { this.toDispose.dispose(); } - protected async processWorkspaceFoldersChanged(roots: FileStat[]): Promise { + protected async processWorkspaceFoldersChanged(roots: string[]): Promise { if (this.isAnyRootChanged(roots) === false) { return; } @@ -125,19 +87,19 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable { this.proxy.$onWorkspaceFoldersChanged({ roots }); const keyValueStorageWorkspacesData = await this.pluginServer.getAllStorageValues({ - workspace: this.workspaceService.workspace, - roots: this.workspaceService.tryGetRoots() + workspace: this.workspaceService.workspace?.resource.toString(), + roots: this.workspaceService.tryGetRoots().map(root => root.resource.toString()) }); this.storageProxy.$updatePluginsWorkspaceData(keyValueStorageWorkspacesData); } - private isAnyRootChanged(roots: FileStat[]): boolean { + private isAnyRootChanged(roots: string[]): boolean { if (!this.roots || this.roots.length !== roots.length) { return true; } - return this.roots.some((root, index) => root.uri !== roots[index].uri); + return this.roots.some((root, index) => root !== roots[index]); } $pickWorkspaceFolder(options: WorkspaceFolderPickOptionsMain): Promise { @@ -155,7 +117,7 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable { let returnValue: theia.WorkspaceFolder | undefined; const items = this.roots.map(root => { - const rootUri = Uri.parse(root.uri); + const rootUri = Uri.parse(root); const rootPathName = rootUri.path.substring(rootUri.path.lastIndexOf('/') + 1); return new QuickOpenItem({ label: rootPathName, @@ -200,7 +162,7 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable { async $startFileSearch(includePattern: string, includeFolderUri: string | undefined, excludePatternOrDisregardExcludes?: string | false, maxResults?: number): Promise { const roots: FileSearchService.RootOptions = {}; - const rootUris = includeFolderUri ? [includeFolderUri] : this.roots.map(r => r.uri); + const rootUris = includeFolderUri ? [includeFolderUri] : this.roots; for (const rootUri of rootUris) { roots[rootUri] = {}; } @@ -242,8 +204,7 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable { return new Promise(resolve => { let matches = 0; const what: string = query.pattern; - const rootUris = this.roots.map(r => r.uri); - this.searchInWorkspaceService.searchWithCallback(what, rootUris, { + this.searchInWorkspaceService.searchWithCallback(what, this.roots, { onResult: (searchId, result) => { if (canceledRequest) { return; @@ -296,17 +257,6 @@ export class WorkspaceMainImpl implements WorkspaceMain, Disposable { }); } - async $registerFileSystemWatcher(options: FileWatcherSubscriberOptions): Promise { - const handle = this.inPluginFileSystemWatcherManager.registerFileWatchSubscription(options, this.proxy); - this.toDispose.push(Disposable.create(() => this.inPluginFileSystemWatcherManager.unregisterFileWatchSubscription(handle))); - return handle; - } - - $unregisterFileSystemWatcher(watcherId: string): Promise { - this.inPluginFileSystemWatcherManager.unregisterFileWatchSubscription(watcherId); - return Promise.resolve(); - } - async $registerTextDocumentContentProvider(scheme: string): Promise { this.resourceResolver.registerContentProvider(scheme, this.proxy); this.toDispose.push(Disposable.create(() => this.resourceResolver.unregisterContentProvider(scheme))); diff --git a/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts b/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts index dfee0cdb4d24a..3363f89658752 100644 --- a/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts +++ b/packages/plugin-ext/src/main/common/plugin-paths-protocol.ts @@ -14,8 +14,6 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { FileStat } from '@theia/filesystem/lib/common'; - export const pluginPathsServicePath = '/services/plugin-paths'; // Service to create plugin configuration folders for different purpose. @@ -24,5 +22,5 @@ export interface PluginPathsService { /** Returns hosted log path. Create directory by this path if it is not exist on the file system. */ getHostLogPath(): Promise; /** Returns storage path for given workspace */ - getHostStoragePath(workspace: FileStat | undefined, roots: FileStat[]): Promise; + getHostStoragePath(workspaceUri: string | undefined, rootUris: string[]): Promise; } diff --git a/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts b/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts index 12532d521f7a1..a97638b758654 100644 --- a/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts +++ b/packages/plugin-ext/src/main/node/paths/plugin-paths-service.ts @@ -15,11 +15,11 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; +import URI from '@theia/core/lib/common/uri'; import * as path from 'path'; +import * as fs from 'fs-extra'; import { readdir, remove } from 'fs-extra'; import * as crypto from 'crypto'; -import URI from '@theia/core/lib/common/uri'; import { ILogger } from '@theia/core'; import { FileUri } from '@theia/core/lib/node'; import { PluginPaths } from './const'; @@ -37,9 +37,6 @@ export class PluginPathsServiceImpl implements PluginPathsService { @inject(ILogger) protected readonly logger: ILogger; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; - @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer; @@ -54,53 +51,51 @@ export class PluginPathsServiceImpl implements PluginPathsService { } const pluginDirPath = path.join(parentLogsDir, this.generateTimeFolderName(), 'host'); - await this.fileSystem.createFolder(pluginDirPath); + await fs.mkdirs(pluginDirPath); // no `await` as We should never wait for the cleanup this.cleanupOldLogs(parentLogsDir); - return new URI(pluginDirPath).path.toString(); + return pluginDirPath; } - async getHostStoragePath(workspace: FileStat | undefined, roots: FileStat[]): Promise { + async getHostStoragePath(workspaceUri: string | undefined, rootUris: string[]): Promise { const parentStorageDir = await this.getWorkspaceStorageDirPath(); if (!parentStorageDir) { throw new Error('Unable to get parent storage directory'); } - if (!workspace) { + if (!workspaceUri) { return undefined; } - if (!await this.fileSystem.exists(parentStorageDir)) { - await this.fileSystem.createFolder(parentStorageDir); - } + await fs.mkdirs(parentStorageDir); - const storageDirName = await this.buildWorkspaceId(workspace, roots); + const storageDirName = await this.buildWorkspaceId(workspaceUri, rootUris); const storageDirPath = path.join(parentStorageDir, storageDirName); - if (!await this.fileSystem.exists(storageDirPath)) { - await this.fileSystem.createFolder(storageDirPath); - } + await fs.mkdirs(storageDirPath); - return new URI(storageDirPath).path.toString(); + return storageDirPath; } - protected async buildWorkspaceId(workspace: FileStat, roots: FileStat[]): Promise { + protected async buildWorkspaceId(workspaceUri: string, rootUris: string[]): Promise { const untitledWorkspace = await getTemporaryWorkspaceFileUri(this.envServer); - if (untitledWorkspace.toString() === workspace.uri) { + if (untitledWorkspace.toString() === workspaceUri) { // if workspace is temporary // then let create a storage path for each set of workspace roots - const rootsStr = roots.map(root => root.uri).sort().join(','); + const rootsStr = rootUris.sort().join(','); return crypto.createHash('md5').update(rootsStr).digest('hex'); } else { - const uri = new URI(workspace.uri); - let displayName = uri.displayName; - - if ((!workspace || !workspace.isDirectory) && (displayName.endsWith(`.${THEIA_EXT}`) || displayName.endsWith(`.${VSCODE_EXT}`))) { + let stat; + try { + stat = await fs.stat(FileUri.fsPath(workspaceUri)); + } catch { /* no-op */ } + let displayName = new URI(workspaceUri).displayName; + if ((!stat || !stat.isDirectory()) && (displayName.endsWith(`.${THEIA_EXT}`) || displayName.endsWith(`.${VSCODE_EXT}`))) { displayName = displayName.slice(0, displayName.lastIndexOf('.')); } - return crypto.createHash('md5').update(uri.toString()).digest('hex'); + return crypto.createHash('md5').update(workspaceUri).digest('hex'); } } diff --git a/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts b/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts new file mode 100644 index 0000000000000..27eadbb36275b --- /dev/null +++ b/packages/plugin-ext/src/plugin/file-system-event-service-ext-impl.ts @@ -0,0 +1,256 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/** + * **IMPORTANT** this code is running in the plugin host process and should be closed as possible to VS Code counterpart: + * https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/api/common/extHostFileSystemEventService.ts + * One should be able to diff them to see differences. + */ + +/* eslint-disable max-len */ +/* eslint-disable no-shadow */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/tslint/config */ + +import { Emitter, WaitUntilEvent, AsyncEmitter } from '@theia/core/lib/common/event'; +import { IRelativePattern, parse } from '@theia/callhierarchy/lib/common/glob'; +import { URI, UriComponents } from 'vscode-uri'; +import { EditorsAndDocumentsExtImpl as ExtHostDocumentsAndEditors } from './editors-and-documents'; +import type * as vscode from '@theia/plugin'; +import * as typeConverter from './type-converters'; +import { Disposable, WorkspaceEdit } from './types-impl'; +import { FileOperation } from '@theia/filesystem/lib/common/files'; +import { flatten } from '../common/arrays'; +import { CancellationToken } from '@theia/core/lib/common/cancellation'; +import { + Plugin, TextEditorsMain as MainThreadTextEditorsShape, PLUGIN_RPC_CONTEXT, FileSystemEvents, ExtHostFileSystemEventServiceShape, + ResourceFileEditDto as IWorkspaceFileEditDto, ResourceTextEditDto as IWorkspaceTextEditDto +} from '../common/plugin-api-rpc'; +import { RPCProtocol } from '../common/rpc-protocol'; + +type Event = vscode.Event; +type IExtensionDescription = Plugin; +type IWaitUntil = WaitUntilEvent; + +class FileSystemWatcher implements vscode.FileSystemWatcher { + + private readonly _onDidCreate = new Emitter(); + private readonly _onDidChange = new Emitter(); + private readonly _onDidDelete = new Emitter(); + private _disposable: Disposable; + private _config: number; + + get ignoreCreateEvents(): boolean { + return Boolean(this._config & 0b001); + } + + get ignoreChangeEvents(): boolean { + return Boolean(this._config & 0b010); + } + + get ignoreDeleteEvents(): boolean { + return Boolean(this._config & 0b100); + } + + constructor(dispatcher: Event, globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean) { + + this._config = 0; + if (ignoreCreateEvents) { + this._config += 0b001; + } + if (ignoreChangeEvents) { + this._config += 0b010; + } + if (ignoreDeleteEvents) { + this._config += 0b100; + } + + const parsedPattern = parse(globPattern); + + const subscription = dispatcher(events => { + if (!ignoreCreateEvents) { + for (const created of events.created) { + const uri = URI.revive(created); + if (parsedPattern(uri.fsPath)) { + this._onDidCreate.fire(uri); + } + } + } + if (!ignoreChangeEvents) { + for (const changed of events.changed) { + const uri = URI.revive(changed); + if (parsedPattern(uri.fsPath)) { + this._onDidChange.fire(uri); + } + } + } + if (!ignoreDeleteEvents) { + for (const deleted of events.deleted) { + const uri = URI.revive(deleted); + if (parsedPattern(uri.fsPath)) { + this._onDidDelete.fire(uri); + } + } + } + }); + + this._disposable = Disposable.from(this._onDidCreate, this._onDidChange, this._onDidDelete, subscription); + } + + dispose() { + this._disposable.dispose(); + } + + get onDidCreate(): Event { + return this._onDidCreate.event; + } + + get onDidChange(): Event { + return this._onDidChange.event; + } + + get onDidDelete(): Event { + return this._onDidDelete.event; + } +} + +interface IExtensionListener { + extension: IExtensionDescription; + (e: E): any; +} + +export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServiceShape { + + private readonly _onFileSystemEvent = new Emitter(); + + private readonly _onDidRenameFile = new Emitter(); + private readonly _onDidCreateFile = new Emitter(); + private readonly _onDidDeleteFile = new Emitter(); + private readonly _onWillRenameFile = new AsyncEmitter(); + private readonly _onWillCreateFile = new AsyncEmitter(); + private readonly _onWillDeleteFile = new AsyncEmitter(); + + readonly onDidRenameFile: Event = this._onDidRenameFile.event; + readonly onDidCreateFile: Event = this._onDidCreateFile.event; + readonly onDidDeleteFile: Event = this._onDidDeleteFile.event; + + constructor( + rpc: RPCProtocol, + private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _mainThreadTextEditors: MainThreadTextEditorsShape = rpc.getProxy(PLUGIN_RPC_CONTEXT.TEXT_EDITORS_MAIN) + ) { + // + } + + // --- file events + + createFileSystemWatcher(globPattern: string | IRelativePattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): vscode.FileSystemWatcher { + return new FileSystemWatcher(this._onFileSystemEvent.event, globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); + } + + $onFileEvent(events: FileSystemEvents) { + this._onFileSystemEvent.fire(events); + } + + // --- file operations + + $onDidRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined): void { + switch (operation) { + case FileOperation.MOVE: + this._onDidRenameFile.fire(Object.freeze({ files: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }] })); + break; + case FileOperation.DELETE: + this._onDidDeleteFile.fire(Object.freeze({ files: [URI.revive(target)] })); + break; + case FileOperation.CREATE: + this._onDidCreateFile.fire(Object.freeze({ files: [URI.revive(target)] })); + break; + default: + // ignore, dont send + } + } + + getOnWillRenameFileEvent(extension: IExtensionDescription): Event { + return this._createWillExecuteEvent(extension, this._onWillRenameFile); + } + + getOnWillCreateFileEvent(extension: IExtensionDescription): Event { + return this._createWillExecuteEvent(extension, this._onWillCreateFile); + } + + getOnWillDeleteFileEvent(extension: IExtensionDescription): Event { + return this._createWillExecuteEvent(extension, this._onWillDeleteFile); + } + + private _createWillExecuteEvent(extension: IExtensionDescription, emitter: AsyncEmitter): Event { + return (listener, thisArg, disposables) => { + const wrappedListener: IExtensionListener = function wrapped(e: E) { listener.call(thisArg, e); }; + wrappedListener.extension = extension; + return emitter.event(wrappedListener, undefined, disposables); + }; + } + + async $onWillRunFileOperation(operation: FileOperation, target: UriComponents, source: UriComponents | undefined, timeout: number, token: CancellationToken): Promise { + switch (operation) { + case FileOperation.MOVE: + await this._fireWillEvent(this._onWillRenameFile, { files: [{ oldUri: URI.revive(source!), newUri: URI.revive(target) }] }, timeout, token); + break; + case FileOperation.DELETE: + await this._fireWillEvent(this._onWillDeleteFile, { files: [URI.revive(target)] }, timeout, token); + break; + case FileOperation.CREATE: + await this._fireWillEvent(this._onWillCreateFile, { files: [URI.revive(target)] }, timeout, token); + break; + default: + // ignore, dont send + } + } + + private async _fireWillEvent(emitter: AsyncEmitter, data: Omit, timeout: number, token: CancellationToken): Promise { + + const edits: WorkspaceEdit[] = []; + await emitter.fire(data, token, async (thenable, listener) => { + // ignore all results except for WorkspaceEdits. Those are stored in an array. + const now = Date.now(); + const result = await Promise.resolve(thenable); + if (result instanceof WorkspaceEdit) { + edits.push(result); + } + + if (Date.now() - now > timeout) { + console.warn('SLOW file-participant', (>listener).extension?.model.id); + } + }); + + if (token.isCancellationRequested) { + return; + } + + if (edits.length > 0) { + // flatten all WorkspaceEdits collected via waitUntil-call + // and apply them in one go. + const allEdits = new Array>(); + for (const edit of edits) { + const { edits } = typeConverter.fromWorkspaceEdit(edit, this._extHostDocumentsAndEditors); + allEdits.push(edits); + } + return this._mainThreadTextEditors.$tryApplyWorkspaceEdit({ edits: flatten(allEdits) }); + } + } +} diff --git a/packages/plugin-ext/src/plugin/file-system-ext-impl.ts b/packages/plugin-ext/src/plugin/file-system-ext-impl.ts new file mode 100644 index 0000000000000..95ca8d64d290f --- /dev/null +++ b/packages/plugin-ext/src/plugin/file-system-ext-impl.ts @@ -0,0 +1,396 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/** + * **IMPORTANT** this code is running in the plugin host process and should be closed as possible to VS Code counterpart: + * https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/api/common/extHostFileSystem.ts + * One should be able to diff them to see differences. + */ + +/* eslint-disable arrow-body-style */ +/* eslint-disable @typescript-eslint/quotes */ +/* eslint-disable @typescript-eslint/tslint/config */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { URI, UriComponents } from 'vscode-uri'; +import { RPCProtocol } from '../common/rpc-protocol'; +import { PLUGIN_RPC_CONTEXT, FileSystemExt, FileSystemMain, IFileChangeDto } from '../common/plugin-api-rpc'; +import * as vscode from '@theia/plugin'; +import * as files from '@theia/filesystem/lib/common/files'; +import { FileChangeType, FileSystemError } from './types-impl'; +import * as typeConverter from './type-converters'; +import { LanguagesExtImpl } from './languages'; +import { Schemes as Schemas } from '../common/uri-components'; +import { State, StateMachine, LinkComputer, Edge } from '../common/link-computer'; +import { commonPrefixLength } from '@theia/core/lib/common/strings'; +import { CharCode } from '@theia/core/lib/common/char-code'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; + +type IDisposable = vscode.Disposable; + +class FsLinkProvider { + + private _schemes: string[] = []; + private _stateMachine?: StateMachine; + + add(scheme: string): void { + this._stateMachine = undefined; + this._schemes.push(scheme); + } + + delete(scheme: string): void { + const idx = this._schemes.indexOf(scheme); + if (idx >= 0) { + this._schemes.splice(idx, 1); + this._stateMachine = undefined; + } + } + + private _initStateMachine(): void { + if (!this._stateMachine) { + + // sort and compute common prefix with previous scheme + // then build state transitions based on the data + const schemes = this._schemes.sort(); + const edges: Edge[] = []; + let prevScheme: string | undefined; + let prevState: State; + let lastState = State.LastKnownState; + let nextState = State.LastKnownState; + for (const scheme of schemes) { + + // skip the common prefix of the prev scheme + // and continue with its last state + let pos = !prevScheme ? 0 : commonPrefixLength(prevScheme, scheme); + if (pos === 0) { + prevState = State.Start; + } else { + prevState = nextState; + } + + for (; pos < scheme.length; pos++) { + // keep creating new (next) states until the + // end (and the BeforeColon-state) is reached + if (pos + 1 === scheme.length) { + // Save the last state here, because we need to continue for the next scheme + lastState = nextState; + nextState = State.BeforeColon; + } else { + nextState += 1; + } + edges.push([prevState, scheme.toUpperCase().charCodeAt(pos), nextState]); + edges.push([prevState, scheme.toLowerCase().charCodeAt(pos), nextState]); + prevState = nextState; + } + + prevScheme = scheme; + // Restore the last state + nextState = lastState; + } + + // all link must match this pattern `:/` + edges.push([State.BeforeColon, CharCode.Colon, State.AfterColon]); + edges.push([State.AfterColon, CharCode.Slash, State.End]); + + this._stateMachine = new StateMachine(edges); + } + } + + provideDocumentLinks(document: vscode.TextDocument): vscode.ProviderResult { + this._initStateMachine(); + + const result: vscode.DocumentLink[] = []; + const links = LinkComputer.computeLinks({ + getLineContent(lineNumber: number): string { + return document.lineAt(lineNumber - 1).text; + }, + getLineCount(): number { + return document.lineCount; + } + }, this._stateMachine); + + for (const link of links) { + const docLink = typeConverter.DocumentLink.to(link); + if (docLink.target) { + result.push(docLink); + } + } + return result; + } +} + +class ConsumerFileSystem implements vscode.FileSystem { + + constructor(private _proxy: FileSystemMain) { } + + stat(uri: vscode.Uri): Promise { + return this._proxy.$stat(uri).catch(ConsumerFileSystem._handleError); + } + readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { + return this._proxy.$readdir(uri).catch(ConsumerFileSystem._handleError); + } + createDirectory(uri: vscode.Uri): Promise { + return this._proxy.$mkdir(uri).catch(ConsumerFileSystem._handleError); + } + async readFile(uri: vscode.Uri): Promise { + return this._proxy.$readFile(uri).then(buff => buff.buffer).catch(ConsumerFileSystem._handleError); + } + writeFile(uri: vscode.Uri, content: Uint8Array): Promise { + return this._proxy.$writeFile(uri, BinaryBuffer.wrap(content)).catch(ConsumerFileSystem._handleError); + } + delete(uri: vscode.Uri, options?: { recursive?: boolean; useTrash?: boolean; }): Promise { + return this._proxy.$delete(uri, { ...{ recursive: false, useTrash: false }, ...options }).catch(ConsumerFileSystem._handleError); + } + rename(oldUri: vscode.Uri, newUri: vscode.Uri, options?: { overwrite?: boolean; }): Promise { + return this._proxy.$rename(oldUri, newUri, { ...{ overwrite: false }, ...options }).catch(ConsumerFileSystem._handleError); + } + copy(source: vscode.Uri, destination: vscode.Uri, options?: { overwrite?: boolean }): Promise { + return this._proxy.$copy(source, destination, { ...{ overwrite: false }, ...options }).catch(ConsumerFileSystem._handleError); + } + private static _handleError(err: any): never { + // generic error + if (!(err instanceof Error)) { + throw new FileSystemError(String(err)); + } + + // no provider (unknown scheme) error + if (err.name === 'ENOPRO') { + throw FileSystemError.Unavailable(err.message); + } + + // file system error + switch (err.name) { + case files.FileSystemProviderErrorCode.FileExists: throw FileSystemError.FileExists(err.message); + case files.FileSystemProviderErrorCode.FileNotFound: throw FileSystemError.FileNotFound(err.message); + case files.FileSystemProviderErrorCode.FileNotADirectory: throw FileSystemError.FileNotADirectory(err.message); + case files.FileSystemProviderErrorCode.FileIsADirectory: throw FileSystemError.FileIsADirectory(err.message); + case files.FileSystemProviderErrorCode.NoPermissions: throw FileSystemError.NoPermissions(err.message); + case files.FileSystemProviderErrorCode.Unavailable: throw FileSystemError.Unavailable(err.message); + + default: throw new FileSystemError(err.message, err.name as files.FileSystemProviderErrorCode); + } + } +} + +export class FileSystemExtImpl implements FileSystemExt { + + private readonly _proxy: FileSystemMain; + private readonly _linkProvider = new FsLinkProvider(); + private readonly _fsProvider = new Map(); + private readonly _usedSchemes = new Set(); + private readonly _watches = new Map(); + + private _linkProviderRegistration?: IDisposable; + private _handlePool: number = 0; + + readonly fileSystem: vscode.FileSystem; + + constructor(rpc: RPCProtocol, private _extHostLanguageFeatures: LanguagesExtImpl) { + this._proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.FILE_SYSTEM_MAIN); + this.fileSystem = new ConsumerFileSystem(this._proxy); + + // register used schemes + Object.keys(Schemas).forEach(scheme => this._usedSchemes.add(scheme)); + } + + dispose(): void { + if (this._linkProviderRegistration) { + this._linkProviderRegistration.dispose(); + } + } + + private _registerLinkProviderIfNotYetRegistered(): void { + if (!this._linkProviderRegistration) { + this._linkProviderRegistration = this._extHostLanguageFeatures.registerDocumentLinkProvider('*', this._linkProvider, { + id: 'theia.fs-ext-impl', + name: 'fs-ext-impl' + }); + } + } + + registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean } = {}) { + + if (this._usedSchemes.has(scheme)) { + throw new Error(`a provider for the scheme '${scheme}' is already registered`); + } + + // + this._registerLinkProviderIfNotYetRegistered(); + + const handle = this._handlePool++; + this._linkProvider.add(scheme); + this._usedSchemes.add(scheme); + this._fsProvider.set(handle, provider); + + let capabilities = files.FileSystemProviderCapabilities.FileReadWrite; + if (options.isCaseSensitive) { + capabilities += files.FileSystemProviderCapabilities.PathCaseSensitive; + } + if (options.isReadonly) { + capabilities += files.FileSystemProviderCapabilities.Readonly; + } + if (typeof provider.copy === 'function') { + capabilities += files.FileSystemProviderCapabilities.FileFolderCopy; + } + if (typeof provider.open === 'function' && typeof provider.close === 'function' + && typeof provider.read === 'function' && typeof provider.write === 'function' + ) { + capabilities += files.FileSystemProviderCapabilities.FileOpenReadWriteClose; + } + + this._proxy.$registerFileSystemProvider(handle, scheme, capabilities); + + const subscription = provider.onDidChangeFile(event => { + const mapped: IFileChangeDto[] = []; + for (const e of event) { + const { uri: resource, type } = e; + if (resource.scheme !== scheme) { + // dropping events for wrong scheme + continue; + } + let newType: files.FileChangeType | undefined; + switch (type) { + case FileChangeType.Changed: + newType = files.FileChangeType.UPDATED; + break; + case FileChangeType.Created: + newType = files.FileChangeType.ADDED; + break; + case FileChangeType.Deleted: + newType = files.FileChangeType.DELETED; + break; + default: + throw new Error('Unknown FileChangeType'); + } + mapped.push({ resource, type: newType }); + } + this._proxy.$onFileSystemChange(handle, mapped); + }); + + return { + dispose: () => { + subscription.dispose(); + this._linkProvider.delete(scheme); + this._usedSchemes.delete(scheme); + this._fsProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + } + }; + } + + private static _asIStat(stat: vscode.FileStat): files.Stat { + const { type, ctime, mtime, size } = stat; + return { type, ctime, mtime, size }; + } + + $stat(handle: number, resource: UriComponents): Promise { + return Promise.resolve(this._getFsProvider(handle).stat(URI.revive(resource))).then(FileSystemExtImpl._asIStat); + } + + $readdir(handle: number, resource: UriComponents): Promise<[string, files.FileType][]> { + return Promise.resolve(this._getFsProvider(handle).readDirectory(URI.revive(resource))); + } + + $readFile(handle: number, resource: UriComponents): Promise { + return Promise.resolve(this._getFsProvider(handle).readFile(URI.revive(resource))).then(data => BinaryBuffer.wrap(data)); + } + + $writeFile(handle: number, resource: UriComponents, content: BinaryBuffer, opts: files.FileWriteOptions): Promise { + return Promise.resolve(this._getFsProvider(handle).writeFile(URI.revive(resource), content.buffer, opts)); + } + + $delete(handle: number, resource: UriComponents, opts: files.FileDeleteOptions): Promise { + return Promise.resolve(this._getFsProvider(handle).delete(URI.revive(resource), opts)); + } + + $rename(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): Promise { + return Promise.resolve(this._getFsProvider(handle).rename(URI.revive(oldUri), URI.revive(newUri), opts)); + } + + $copy(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): Promise { + const provider = this._getFsProvider(handle); + if (!provider.copy) { + throw new Error('FileSystemProvider does not implement "copy"'); + } + return Promise.resolve(provider.copy(URI.revive(oldUri), URI.revive(newUri), opts)); + } + + $mkdir(handle: number, resource: UriComponents): Promise { + return Promise.resolve(this._getFsProvider(handle).createDirectory(URI.revive(resource))); + } + + $watch(handle: number, session: number, resource: UriComponents, opts: files.WatchOptions): void { + const subscription = this._getFsProvider(handle).watch(URI.revive(resource), opts); + this._watches.set(session, subscription); + } + + $unwatch(_handle: number, session: number): void { + const subscription = this._watches.get(session); + if (subscription) { + subscription.dispose(); + this._watches.delete(session); + } + } + + $open(handle: number, resource: UriComponents, opts: files.FileOpenOptions): Promise { + const provider = this._getFsProvider(handle); + if (!provider.open) { + throw new Error('FileSystemProvider does not implement "open"'); + } + return Promise.resolve(provider.open(URI.revive(resource), opts)); + } + + $close(handle: number, fd: number): Promise { + const provider = this._getFsProvider(handle); + if (!provider.close) { + throw new Error('FileSystemProvider does not implement "close"'); + } + return Promise.resolve(provider.close(fd)); + } + + $read(handle: number, fd: number, pos: number, length: number): Promise { + const provider = this._getFsProvider(handle); + if (!provider.read) { + throw new Error('FileSystemProvider does not implement "read"'); + } + const data = BinaryBuffer.alloc(length); + return Promise.resolve(provider.read(fd, pos, data.buffer, 0, length)).then(read => { + return data.slice(0, read); // don't send zeros + }); + } + + $write(handle: number, fd: number, pos: number, data: BinaryBuffer): Promise { + const provider = this._getFsProvider(handle); + if (!provider.write) { + throw new Error('FileSystemProvider does not implement "write"'); + } + return Promise.resolve(provider.write(fd, pos, data.buffer, 0, data.byteLength)); + } + + private _getFsProvider(handle: number): vscode.FileSystemProvider { + const provider = this._fsProvider.get(handle); + if (!provider) { + const err = new Error(); + err.name = 'ENOPRO'; + err.message = `no provider`; + throw err; + } + return provider; + } +} diff --git a/packages/plugin-ext/src/plugin/file-system.ts b/packages/plugin-ext/src/plugin/file-system.ts deleted file mode 100644 index 6cfdcb9a63444..0000000000000 --- a/packages/plugin-ext/src/plugin/file-system.ts +++ /dev/null @@ -1,142 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2019 Red Hat, Inc. and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { URI } from 'vscode-uri'; -import * as theia from '@theia/plugin'; -import { PLUGIN_RPC_CONTEXT, FileSystemExt, FileSystemMain } from '../common/plugin-api-rpc'; -import { RPCProtocol } from '../common/rpc-protocol'; -import { UriComponents, Schemes } from '../common/uri-components'; -import { Disposable, FileStat, FileType } from './types-impl'; -import { InPluginFileSystemProxy } from './in-plugin-filesystem-proxy'; - -export class FileSystemExtImpl implements FileSystemExt { - - private readonly proxy: FileSystemMain; - private readonly usedSchemes = new Set(); - private readonly fsProviders = new Map(); - private readonly fileSystem: InPluginFileSystemProxy; - - private handlePool: number = 0; - - constructor(rpc: RPCProtocol) { - this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.FILE_SYSTEM_MAIN); - this.usedSchemes.add(Schemes.FILE); - this.usedSchemes.add(Schemes.UNTITLED); - this.usedSchemes.add(Schemes.VSCODE); - this.usedSchemes.add(Schemes.IN_MEMORY); - this.usedSchemes.add(Schemes.INTERNAL); - this.usedSchemes.add(Schemes.HTTP); - this.usedSchemes.add(Schemes.HTTPS); - this.usedSchemes.add(Schemes.MAILTO); - this.usedSchemes.add(Schemes.DATA); - this.usedSchemes.add(Schemes.COMMAND); - this.fileSystem = new InPluginFileSystemProxy(this.proxy); - } - - get fs(): theia.FileSystem { - return this.fileSystem; - } - - registerFileSystemProvider(scheme: string, provider: theia.FileSystemProvider): theia.Disposable { - if (this.usedSchemes.has(scheme)) { - throw new Error(`A provider for the scheme '${scheme}' is already registered`); - } - - const handle = this.handlePool++; - this.usedSchemes.add(scheme); - this.fsProviders.set(handle, provider); - - this.proxy.$registerFileSystemProvider(handle, scheme); - - return new Disposable(() => { - this.usedSchemes.delete(scheme); - this.fsProviders.delete(handle); - this.proxy.$unregisterProvider(handle); - }); - } - - private safeGetProvider(handle: number): theia.FileSystemProvider { - const provider = this.fsProviders.get(handle); - if (!provider) { - const err = new Error(); - err.name = 'ENOPRO'; - err.message = 'no provider'; - throw err; - } - return provider; - } - - // forwarding calls - - $stat(handle: number, resource: UriComponents): Promise { - const fileSystemProvider = this.safeGetProvider(handle); - const uri = URI.revive(resource); - return Promise.resolve(fileSystemProvider.stat(uri)); - } - - $readDirectory(handle: number, resource: UriComponents): Promise<[string, FileType][]> { - const fileSystemProvider = this.safeGetProvider(handle); - const uri = URI.revive(resource); - return Promise.resolve(fileSystemProvider.readDirectory(uri)); - } - - $createDirectory(handle: number, resource: UriComponents): Promise { - const fileSystemProvider = this.safeGetProvider(handle); - const uri = URI.revive(resource); - return Promise.resolve(fileSystemProvider.createDirectory(uri)); - } - - $readFile(handle: number, resource: UriComponents, options?: { encoding?: string }): Promise { - const fileSystemProvider = this.safeGetProvider(handle); - - return Promise.resolve(fileSystemProvider.readFile(URI.revive(resource))).then(data => { - const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength); - const encoding = options === null ? undefined : options && options.encoding; - return buffer.toString(encoding); - } - ); - } - - $writeFile(handle: number, resource: UriComponents, content: string, options?: { encoding?: string }): Promise { - const fileSystemProvider = this.safeGetProvider(handle); - const uri = URI.revive(resource); - const encoding = options === null ? undefined : options && options.encoding; - const buffer = Buffer.from(content, encoding); - const opts = { create: true, overwrite: true }; - return Promise.resolve(fileSystemProvider.writeFile(uri, buffer, opts)); - } - - $delete(handle: number, resource: UriComponents, options: { recursive: boolean }): Promise { - const fileSystemProvider = this.safeGetProvider(handle); - const uri = URI.revive(resource); - return Promise.resolve(fileSystemProvider.delete(uri, options)); - } - - $rename(handle: number, source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise { - const fileSystemProvider = this.safeGetProvider(handle); - const sourceUri = URI.revive(source); - const targetUri = URI.revive(target); - return Promise.resolve(fileSystemProvider.rename(sourceUri, targetUri, options)); - } - - $copy(handle: number, source: UriComponents, target: UriComponents, options: { overwrite: boolean }): Promise { - const fileSystemProvider = this.safeGetProvider(handle); - const sourceUri = URI.revive(source); - const targetUri = URI.revive(target); - return Promise.resolve(fileSystemProvider.copy && fileSystemProvider.copy(sourceUri, targetUri, options)); - } - -} diff --git a/packages/plugin-ext/src/plugin/in-plugin-filesystem-proxy.ts b/packages/plugin-ext/src/plugin/in-plugin-filesystem-proxy.ts deleted file mode 100644 index 8293ac146edcd..0000000000000 --- a/packages/plugin-ext/src/plugin/in-plugin-filesystem-proxy.ts +++ /dev/null @@ -1,106 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2020 Red Hat, Inc. and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import * as theia from '@theia/plugin'; -import { TextEncoder, TextDecoder } from 'util'; -import { FileSystemMain } from '../common/plugin-api-rpc'; -import { UriComponents } from '../common/uri-components'; -import { FileSystemError, FileType } from './types-impl'; -import { FileStat, Uri } from '@theia/plugin'; - -/** - * This class is managing FileSystem proxy - */ -export class InPluginFileSystemProxy implements theia.FileSystem { - - private proxy: FileSystemMain; - - constructor(proxy: FileSystemMain) { - this.proxy = proxy; - } - - async stat(uri: Uri): Promise { - try { - return this.proxy.$stat(uri); - } catch (error) { - throw this.handleError(error); - } - } - async readDirectory(uri: UriComponents): Promise<[string, FileType][]> { - try { - return this.proxy.$readDirectory(uri); - } catch (error) { - throw this.handleError(error); - } - } - async createDirectory(uri: Uri): Promise { - try { - return this.proxy.$createDirectory(uri); - } catch (error) { - throw this.handleError(error); - } - - } - async readFile(uri: UriComponents): Promise { - try { - const val = await this.proxy.$readFile(uri); - return new TextEncoder().encode(val); - } catch (error) { - throw this.handleError(error); - } - } - async writeFile(uri: UriComponents, content: Uint8Array): Promise { - const encoded = new TextDecoder().decode(content); - - try { - return this.proxy.$writeFile(uri, encoded); - } catch (error) { - throw this.handleError(error); - } - } - async delete(uri: Uri, options?: { recursive?: boolean, useTrash?: boolean }): Promise { - try { - return this.proxy.$delete(uri, { ...{ recursive: false }, ...options }); - } catch (error) { - throw this.handleError(error); - } - } - async rename(source: Uri, target: Uri, options?: { overwrite?: boolean }): Promise { - try { - return this.proxy.$rename(source, target, { ...{ overwrite: false }, ...options }); - } catch (error) { - throw this.handleError(error); - } - } - async copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): Promise { - try { - return this.proxy.$copy(source, target, { ...{ overwrite: false }, ...options }); - } catch (error) { - throw this.handleError(error); - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - handleError(error: any): Error { - if (!(error instanceof Error)) { - return new FileSystemError(String(error)); - } - - // file system error - return new FileSystemError(error.message, error.name); - } - -} diff --git a/packages/plugin-ext/src/plugin/in-plugin-filesystem-watcher-proxy.ts b/packages/plugin-ext/src/plugin/in-plugin-filesystem-watcher-proxy.ts deleted file mode 100644 index cf1322a4fecc7..0000000000000 --- a/packages/plugin-ext/src/plugin/in-plugin-filesystem-watcher-proxy.ts +++ /dev/null @@ -1,150 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import * as theia from '@theia/plugin'; -import { Emitter, Event } from '@theia/core/lib/common/event'; -import { WorkspaceMain } from '../common/plugin-api-rpc'; -import { FileWatcherSubscriberOptions, FileChangeEventType } from '../common/plugin-api-rpc-model'; -import { URI } from 'vscode-uri'; - -/** - * This class is responsible for file watchers subscription registering and file system events proxying. - * It contains no logic, only communicates with main side to add / remove subscription and - * delivers file system events to corresponding subscribers. - */ -export class InPluginFileSystemWatcherProxy { - - private proxy: WorkspaceMain; - private subscribers: Map>; - - constructor(proxy: WorkspaceMain) { - this.proxy = proxy; - this.subscribers = new Map>(); - } - - createFileSystemWatcher( - globPattern: theia.GlobPattern, - ignoreCreateEvents?: boolean, - ignoreChangeEvents?: boolean, - ignoreDeleteEvents?: boolean): theia.FileSystemWatcher { - - const perSubscriberEventEmitter = new Emitter(); - const subscriberPrivateData: SubscriberData = { - event: perSubscriberEventEmitter.event - }; - const fileWatcherSubscriberOptions: FileWatcherSubscriberOptions = { globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents }; - // ids are generated by server side to be able handle several subscribers. - this.proxy.$registerFileSystemWatcher(fileWatcherSubscriberOptions).then((id: string) => { - // this is safe, because actual subscription happens on server side and response is - // sent right after actual subscription, so no events are possible in between. - this.subscribers.set(id, perSubscriberEventEmitter); - subscriberPrivateData.unsubscribe = () => this.proxy.$unregisterFileSystemWatcher(id); - }); - return new FileSystemWatcher(subscriberPrivateData, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); - } - - onFileSystemEvent(id: string, uri: URI, type: FileChangeEventType): void { - const perSubscriberEventEmitter: Emitter | undefined = this.subscribers.get(id); - if (perSubscriberEventEmitter) { - perSubscriberEventEmitter.fire({ uri, type }); - } else { - // shouldn't happen - // if it happens then a message was lost, unsubscribe to make state consistent - this.proxy.$unregisterFileSystemWatcher(id); - } - } -} - -class FileSystemWatcher implements theia.FileSystemWatcher { - private subscriberData: SubscriberData; - - private onDidCreateEmitter: Emitter; - private onDidChangeEmitter: Emitter; - private onDidDeleteEmitter: Emitter; - - constructor( - subscriberData: SubscriberData, - private isIgnoreCreateEvents: boolean = false, - private isIgnoreChangeEvents: boolean = false, - private isIgnoreDeleteEvents: boolean = false - ) { - this.onDidCreateEmitter = new Emitter(); - this.onDidChangeEmitter = new Emitter(); - this.onDidDeleteEmitter = new Emitter(); - - this.subscriberData = subscriberData; - subscriberData.event((event: FileSystemEvent) => { - // Here ignore event flags are not analyzed because all the logic is - // moved to server side to avoid unneeded data transfer via network. - // The flags are present just to be read only accessible for user. - switch (event.type) { - case 'updated': - this.onDidChangeEmitter.fire(event.uri); - break; - case 'created': - this.onDidCreateEmitter.fire(event.uri); - break; - case 'deleted': - this.onDidDeleteEmitter.fire(event.uri); - break; - } - }); - } - - get ignoreCreateEvents(): boolean { - return this.isIgnoreCreateEvents; - } - - get ignoreChangeEvents(): boolean { - return this.isIgnoreChangeEvents; - } - - get ignoreDeleteEvents(): boolean { - return this.isIgnoreDeleteEvents; - } - - get onDidCreate(): Event { - return this.onDidCreateEmitter.event; - } - - get onDidChange(): Event { - return this.onDidChangeEmitter.event; - } - - get onDidDelete(): Event { - return this.onDidDeleteEmitter.event; - } - - dispose(): void { - this.onDidCreateEmitter.dispose(); - this.onDidChangeEmitter.dispose(); - this.onDidDeleteEmitter.dispose(); - if (this.subscriberData.unsubscribe) { - this.subscriberData.unsubscribe(); - } - } - -} - -interface FileSystemEvent { - uri: URI, - type: FileChangeEventType -} - -interface SubscriberData { - event: Event - unsubscribe?: () => void; -} diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index 4e9dca520437f..a88a36ada46e2 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -422,7 +422,7 @@ export class LanguagesExtImpl implements LanguagesExt { return this.withAdapter(handle, LinkProviderAdapter, adapter => adapter.resolveLink(link, token), undefined); } - registerLinkProvider(selector: theia.DocumentSelector, provider: theia.DocumentLinkProvider, pluginInfo: PluginInfo): theia.Disposable { + registerDocumentLinkProvider(selector: theia.DocumentSelector, provider: theia.DocumentLinkProvider, pluginInfo: PluginInfo): theia.Disposable { const callId = this.addNewAdapter(new LinkProviderAdapter(provider, this.documents)); this.proxy.$registerDocumentLinkProvider(callId, pluginInfo, this.transformDocumentSelector(selector)); return this.createDisposable(callId); diff --git a/packages/plugin-ext/src/plugin/languages/link-provider.ts b/packages/plugin-ext/src/plugin/languages/link-provider.ts index 8527fea79e884..d64815145b1af 100644 --- a/packages/plugin-ext/src/plugin/languages/link-provider.ts +++ b/packages/plugin-ext/src/plugin/languages/link-provider.ts @@ -44,7 +44,7 @@ export class LinkProviderAdapter { } const result: DocumentLink[] = []; for (const link of links) { - const data = Converter.fromDocumentLink(link); + const data = Converter.DocumentLink.from(link); const id = this.cacheId++; ObjectIdentifier.mixin(data, id); this.cache.set(id, link); @@ -65,7 +65,7 @@ export class LinkProviderAdapter { } return Promise.resolve(this.provider.resolveDocumentLink(item, token)).then(value => { if (value) { - return Converter.fromDocumentLink(value); + return Converter.DocumentLink.from(value); } return undefined; }); diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 513d384a34d2a..8ee10d3e9f69d 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -131,7 +131,7 @@ import { PreferenceRegistryExtImpl } from './preference-registry'; import { OutputChannelRegistryExtImpl } from './output-channel-registry'; import { TerminalServiceExtImpl, TerminalExtImpl } from './terminal-ext'; import { LanguagesExtImpl } from './languages'; -import { fromDocumentSelector, pluginToPluginInfo } from './type-converters'; +import { fromDocumentSelector, pluginToPluginInfo, fromGlobPattern } from './type-converters'; import { DialogsExtImpl } from './dialogs'; import { NotificationExtImpl } from './notification'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; @@ -141,7 +141,7 @@ import { TreeViewsExtImpl } from './tree/tree-views'; import { ConnectionExtImpl } from './connection-ext'; import { TasksExtImpl } from './tasks/tasks'; import { DebugExtImpl } from './node/debug/debug'; -import { FileSystemExtImpl } from './file-system'; +import { FileSystemExtImpl } from './file-system-ext-impl'; import { QuickPick, QuickPickItem } from '@theia/plugin'; import { ScmExtImpl } from './scm'; import { DecorationProvider, LineChange } from '@theia/plugin'; @@ -149,6 +149,7 @@ import { DecorationsExtImpl } from './decorations'; import { TextEditorExt } from './text-editor'; import { ClipboardExt } from './clipboard-ext'; import { WebviewsExtImpl } from './webviews'; +import { ExtHostFileSystemEventService } from './file-system-event-service-ext-impl'; export function createAPIFactory( rpc: RPCProtocol, @@ -177,7 +178,8 @@ export function createAPIFactory( const treeViewsExt = rpc.set(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT, new TreeViewsExtImpl(rpc, commandRegistry)); const tasksExt = rpc.set(MAIN_RPC_CONTEXT.TASKS_EXT, new TasksExtImpl(rpc)); const connectionExt = rpc.set(MAIN_RPC_CONTEXT.CONNECTION_EXT, new ConnectionExtImpl(rpc)); - const fileSystemExt = rpc.set(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT, new FileSystemExtImpl(rpc)); + const fileSystemExt = rpc.set(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT, new FileSystemExtImpl(rpc, languagesExt)); + const extHostFileSystemEvent = rpc.set(MAIN_RPC_CONTEXT.ExtHostFileSystemEventService, new ExtHostFileSystemEventService(rpc, editorsAndDocumentsExt)); const scmExt = rpc.set(MAIN_RPC_CONTEXT.SCM_EXT, new ScmExtImpl(rpc, commandRegistry)); const decorationsExt = rpc.set(MAIN_RPC_CONTEXT.DECORATIONS_EXT, new DecorationsExtImpl(rpc)); rpc.set(MAIN_RPC_CONTEXT.DEBUG_EXT, debugExt); @@ -389,7 +391,7 @@ export function createAPIFactory( const workspace: typeof theia.workspace = { get fs(): theia.FileSystem { - return fileSystemExt.fs; + return fileSystemExt.fileSystem; }, get rootPath(): string | undefined { @@ -422,24 +424,15 @@ export function createAPIFactory( onDidSaveTextDocument(listener, thisArg?, disposables?) { return documents.onDidSaveTextDocument(listener, thisArg, disposables); }, - onWillCreateFiles(listener, thisArg?, disposables?) { - return workspaceExt.onWillCreateFiles(listener, thisArg, disposables); - }, - onDidCreateFiles(listener, thisArg?, disposables?) { - return workspaceExt.onDidCreateFiles(listener, thisArg, disposables); - }, - onWillRenameFiles(listener, thisArg?, disposables?) { - return workspaceExt.onWillRenameFiles(listener, thisArg, disposables); - }, - onDidRenameFiles(listener, thisArg?, disposables?) { - return workspaceExt.onDidRenameFiles(listener, thisArg, disposables); - }, - onWillDeleteFiles(listener, thisArg?, disposables?) { - return workspaceExt.onWillDeleteFiles(listener, thisArg, disposables); - }, - onDidDeleteFiles(listener, thisArg?, disposables?) { - return workspaceExt.onDidDeleteFiles(listener, thisArg, disposables); - }, + onDidCreateFiles: (listener, thisArg, disposables) => extHostFileSystemEvent.onDidCreateFile(listener, thisArg, disposables), + onDidDeleteFiles: (listener, thisArg, disposables) => extHostFileSystemEvent.onDidDeleteFile(listener, thisArg, disposables), + onDidRenameFiles: (listener, thisArg, disposables) => extHostFileSystemEvent.onDidRenameFile(listener, thisArg, disposables), + onWillCreateFiles: (listener: (e: theia.FileWillCreateEvent) => any, thisArg?: any, disposables?: theia.Disposable[]) => + extHostFileSystemEvent.getOnWillCreateFileEvent(plugin)(listener, thisArg, disposables), + onWillDeleteFiles: (listener: (e: theia.FileWillDeleteEvent) => any, thisArg?: any, disposables?: theia.Disposable[]) => + extHostFileSystemEvent.getOnWillDeleteFileEvent(plugin)(listener, thisArg, disposables), + onWillRenameFiles: (listener: (e: theia.FileWillRenameEvent) => any, thisArg?: any, disposables?: theia.Disposable[]) => + extHostFileSystemEvent.getOnWillRenameFileEvent(plugin)(listener, thisArg, disposables), getConfiguration(section?, resource?): theia.WorkspaceConfiguration { return preferenceRegistryExt.getConfiguration(section, resource); }, @@ -466,12 +459,8 @@ export function createAPIFactory( const data = await documents.openDocument(uri); return data && data.document; }, - createFileSystemWatcher(globPattern: theia.GlobPattern, - ignoreCreateEvents?: boolean, - ignoreChangeEvents?: boolean, - ignoreDeleteEvents?: boolean): theia.FileSystemWatcher { - return workspaceExt.createFileSystemWatcher(globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); - }, + createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): theia.FileSystemWatcher => + extHostFileSystemEvent.createFileSystemWatcher(fromGlobPattern(pattern), ignoreCreate, ignoreChange, ignoreDelete), findFiles(include: theia.GlobPattern, exclude?: theia.GlobPattern | null, maxResults?: number, token?: CancellationToken): PromiseLike { return workspaceExt.findFiles(include, exclude, maxResults, token); }, @@ -615,7 +604,7 @@ export function createAPIFactory( return languagesExt.registerOnTypeFormattingEditProvider(selector, provider, [firstTriggerCharacter].concat(moreTriggerCharacters), pluginToPluginInfo(plugin)); }, registerDocumentLinkProvider(selector: theia.DocumentSelector, provider: theia.DocumentLinkProvider): theia.Disposable { - return languagesExt.registerLinkProvider(selector, provider, pluginToPluginInfo(plugin)); + return languagesExt.registerDocumentLinkProvider(selector, provider, pluginToPluginInfo(plugin)); }, registerCodeActionsProvider(selector: theia.DocumentSelector, provider: theia.CodeActionProvider, metadata?: theia.CodeActionProviderMetadata): theia.Disposable { return languagesExt.registerCodeActionsProvider(selector, provider, plugin.model, pluginToPluginInfo(plugin), metadata); diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 427a32e804df6..2d0c6bdae3737 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -404,11 +404,27 @@ export function fromDefinitionLink(definitionLink: theia.DefinitionLink): model. }; } -export function fromDocumentLink(definitionLink: theia.DocumentLink): model.DocumentLink { - return { - range: fromRange(definitionLink.range), - url: definitionLink.target && definitionLink.target.toString() - }; +export namespace DocumentLink { + + export function from(link: theia.DocumentLink): model.DocumentLink { + return { + range: fromRange(link.range), + url: link.target, + tooltip: link.tooltip + }; + } + + export function to(link: model.DocumentLink): theia.DocumentLink { + let target: URI | undefined = undefined; + if (link.url) { + try { + target = typeof link.url === 'string' ? URI.parse(link.url, true) : URI.revive(link.url); + } catch (err) { + // ignore + } + } + return new types.DocumentLink(toRange(link.range), target); + } } export function fromDocumentHighlightKind(kind?: theia.DocumentHighlightKind): model.DocumentHighlightKind | undefined { diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 258c12cd14eaa..8bee5fea5d4cd 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -20,6 +20,7 @@ *--------------------------------------------------------------------------------------------*/ /* eslint-disable no-null/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { UUID } from '@phosphor/coreutils/lib/uuid'; import { illegalArgument } from '../common/errors'; @@ -27,9 +28,10 @@ import * as theia from '@theia/plugin'; import * as crypto from 'crypto'; import { URI } from 'vscode-uri'; import { relative } from '../common/paths-util'; -import { startsWithIgnoreCase } from '@theia/callhierarchy/lib/common/strings'; +import { startsWithIgnoreCase } from '@theia/core/lib/common/strings'; import { MarkdownString, isMarkdownString } from './markdown-string'; import { SymbolKind } from '../common/plugin-api-rpc-model'; +import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from '@theia/filesystem/lib/common/files'; export class Disposable { private disposable: undefined | (() => void); @@ -921,11 +923,15 @@ export class DocumentHighlight { export type Definition = Location | Location[]; export class DocumentLink { + range: Range; - target: URI; - constructor(range: Range, target: URI) { - if (target && !(target instanceof URI)) { + target?: URI; + + tooltip?: string; + + constructor(range: Range, target: URI | undefined) { + if (target && !(URI.isUri(target))) { throw illegalArgument('target'); } if (!Range.isRange(range) || range.isEmpty) { @@ -1256,12 +1262,6 @@ export class DocumentSymbol { } } -export enum FileChangeType { - Changed = 1, - Created = 2, - Deleted = 3, -} - export enum CommentThreadCollapsibleState { Collapsed = 0, Expanded = 1 @@ -1286,41 +1286,61 @@ export enum CommentMode { Preview = 1 } +// #region file api + +export enum FileChangeType { + Changed = 1, + Created = 2, + Deleted = 3, +} + export class FileSystemError extends Error { static FileExists(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'EntryExists', FileSystemError.FileExists); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.FileExists, FileSystemError.FileExists); } static FileNotFound(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'EntryNotFound', FileSystemError.FileNotFound); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.FileNotFound, FileSystemError.FileNotFound); } static FileNotADirectory(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'EntryNotADirectory', FileSystemError.FileNotADirectory); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.FileNotADirectory, FileSystemError.FileNotADirectory); } static FileIsADirectory(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'EntryIsADirectory', FileSystemError.FileIsADirectory); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.FileIsADirectory, FileSystemError.FileIsADirectory); } static NoPermissions(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'NoPermissions', FileSystemError.NoPermissions); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.NoPermissions, FileSystemError.NoPermissions); } static Unavailable(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'Unavailable', FileSystemError.Unavailable); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.Unavailable, FileSystemError.Unavailable); } - constructor(uriOrMessage?: string | URI, code?: string, terminator?: Function) { + readonly code: string; + + constructor(uriOrMessage?: string | URI, code: FileSystemProviderErrorCode = FileSystemProviderErrorCode.Unknown, terminator?: Function) { super(URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); - this.name = code ? `${code} (FileSystemError)` : 'FileSystemError'; - if (typeof Object.setPrototypeOf === 'function') { - Object.setPrototypeOf(this, FileSystemError.prototype); + this.code = terminator?.name ?? 'Unknown'; + + // mark the error as file system provider error so that + // we can extract the error code on the receiving side + markAsFileSystemProviderError(this, code); + + // workaround when extending builtin objects and when compiling to ES5, see: + // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work + if (typeof (Object).setPrototypeOf === 'function') { + (Object).setPrototypeOf(this, FileSystemError.prototype); } if (typeof Error.captureStackTrace === 'function' && typeof terminator === 'function') { + // nice stack traces Error.captureStackTrace(this, terminator); } } } +// #endregion + export enum FileType { Unknown = 0, File = 1, diff --git a/packages/plugin-ext/src/plugin/window-state.ts b/packages/plugin-ext/src/plugin/window-state.ts index 34dc45e58d425..11dc4c4b2d2c3 100644 --- a/packages/plugin-ext/src/plugin/window-state.ts +++ b/packages/plugin-ext/src/plugin/window-state.ts @@ -57,7 +57,7 @@ export class WindowStateExtImpl implements WindowStateExt { if (!target.scheme.trim().length) { throw new Error('Invalid scheme - cannot be empty'); } - if (Schemes.HTTP !== target.scheme && Schemes.HTTPS !== target.scheme) { + if (Schemes.http !== target.scheme && Schemes.https !== target.scheme) { throw new Error(`Invalid scheme '${target.scheme}'`); } diff --git a/packages/plugin-ext/src/plugin/workspace.ts b/packages/plugin-ext/src/plugin/workspace.ts index f74a733c983c4..e9995ff3e7637 100644 --- a/packages/plugin-ext/src/plugin/workspace.ts +++ b/packages/plugin-ext/src/plugin/workspace.ts @@ -21,7 +21,7 @@ import * as paths from 'path'; import * as theia from '@theia/plugin'; -import { Event, Emitter, WaitUntilEvent } from '@theia/core/lib/common/event'; +import { Event, Emitter } from '@theia/core/lib/common/event'; import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { WorkspaceExt, @@ -32,19 +32,9 @@ import { } from '../common/plugin-api-rpc'; import { Path } from '@theia/core/lib/common/path'; import { RPCProtocol } from '../common/rpc-protocol'; -import { - WorkspaceRootsChangeEvent, - FileChangeEvent, - CreateFilesEventDTO, - RenameFilesEventDTO, - DeleteFilesEventDTO, - SearchInWorkspaceResult, - Range -} from '../common/plugin-api-rpc-model'; +import { WorkspaceRootsChangeEvent, SearchInWorkspaceResult, Range } from '../common/plugin-api-rpc-model'; import { EditorsAndDocumentsExtImpl } from './editors-and-documents'; -import { InPluginFileSystemWatcherProxy } from './in-plugin-filesystem-watcher-proxy'; import { URI } from 'vscode-uri'; -import { FileStat } from '@theia/filesystem/lib/common'; import { normalize } from '@theia/callhierarchy/lib/common/paths'; import { relative } from '../common/paths-util'; import { Schemes } from '../common/uri-components'; @@ -55,29 +45,10 @@ import * as Converter from './type-converters'; export class WorkspaceExtImpl implements WorkspaceExt { private proxy: WorkspaceMain; - private fileSystemWatcherManager: InPluginFileSystemWatcherProxy; private workspaceFoldersChangedEmitter = new Emitter(); public readonly onDidChangeWorkspaceFolders: Event = this.workspaceFoldersChangedEmitter.event; - private willCreateFilesEmitter = new Emitter(); - public readonly onWillCreateFiles = this.willCreateFilesEmitter.event; - - private didCreateFileEmitter = new Emitter(); - public readonly onDidCreateFiles = this.didCreateFileEmitter.event; - - private willRenameFilesEmitter = new Emitter(); - public readonly onWillRenameFiles = this.willRenameFilesEmitter.event; - - private didRenameFilesEmitter = new Emitter(); - public readonly onDidRenameFiles = this.didRenameFilesEmitter.event; - - private willDeleteFilesEmitter = new Emitter(); - public readonly onWillDeleteFiles = this.willDeleteFilesEmitter.event; - - private didDeleteFilesEmitter = new Emitter(); - public readonly onDidDeleteFiles = this.didDeleteFilesEmitter.event; - private folders: theia.WorkspaceFolder[] | undefined; private documentContentProviders = new Map(); private searchInWorkspaceEmitter: Emitter<{ result?: theia.TextSearchResult, searchId: number }> = new Emitter<{ result?: theia.TextSearchResult, searchId: number }>(); @@ -87,7 +58,6 @@ export class WorkspaceExtImpl implements WorkspaceExt { private editorsAndDocuments: EditorsAndDocumentsExtImpl, private messageService: MessageRegistryExt) { this.proxy = rpc.getProxy(Ext.WORKSPACE_MAIN); - this.fileSystemWatcherManager = new InPluginFileSystemWatcherProxy(this.proxy); } get rootPath(): string | undefined { @@ -159,8 +129,8 @@ export class WorkspaceExtImpl implements WorkspaceExt { return folder1.filter(folder => map.has(folder.uri.toString())); } - private toWorkspaceFolder(root: FileStat, index: number): theia.WorkspaceFolder { - const uri = URI.parse(root.uri); + private toWorkspaceFolder(root: string, index: number): theia.WorkspaceFolder { + const uri = URI.parse(root); const path = new Path(uri.path); return { uri: uri, @@ -250,19 +220,11 @@ export class WorkspaceExtImpl implements WorkspaceExt { return this.proxy.$findTextInFiles(query, options || {}, nextSearchID, token); } - createFileSystemWatcher(globPattern: theia.GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): theia.FileSystemWatcher { - return this.fileSystemWatcherManager.createFileSystemWatcher(globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); - } - - $fileChanged(event: FileChangeEvent): void { - this.fileSystemWatcherManager.onFileSystemEvent(event.subscriberId, URI.revive(event.uri), event.type); - } - registerTextDocumentContentProvider(scheme: string, provider: theia.TextDocumentContentProvider): theia.Disposable { // `file` and `untitled` schemas are reserved by `workspace.openTextDocument` API: // `file`-scheme for opening a file // `untitled`-scheme for opening a new file that should be saved - if (scheme === Schemes.FILE || scheme === Schemes.UNTITLED || this.documentContentProviders.has(scheme)) { + if (scheme === Schemes.file || scheme === Schemes.untitled || this.documentContentProviders.has(scheme)) { throw new Error(`Text Content Document Provider for scheme '${scheme}' is already registered`); } @@ -430,56 +392,4 @@ export class WorkspaceExtImpl implements WorkspaceExt { return true; } - // #region files api - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async $onWillCreateFiles(event: CreateFilesEventDTO): Promise { - await WaitUntilEvent.fire(this.willCreateFilesEmitter, { - files: event.files.map(URI.revive), - }); - return []; - } - - $onDidCreateFiles(event: CreateFilesEventDTO): void { - this.didCreateFileEmitter.fire({ - files: event.files.map(URI.revive), - }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async $onWillRenameFiles(event: RenameFilesEventDTO): Promise { - await WaitUntilEvent.fire(this.willRenameFilesEmitter, { - files: event.files.map(file => ({ - oldUri: URI.revive(file.oldUri), - newUri: URI.revive(file.newUri), - })), - }); - return []; - } - - $onDidRenameFiles(event: RenameFilesEventDTO): void { - this.didRenameFilesEmitter.fire({ - files: event.files.map(file => ({ - oldUri: URI.revive(file.oldUri), - newUri: URI.revive(file.newUri), - })), - }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async $onWillDeleteFiles(event: DeleteFilesEventDTO): Promise { - await WaitUntilEvent.fire(this.willDeleteFilesEmitter, { - files: event.files.map(URI.revive), - }); - return []; - } - - $onDidDeleteFiles(event: DeleteFilesEventDTO): void { - this.didDeleteFilesEmitter.fire({ - files: event.files.map(URI.revive), - }); - } - - // #endregion files api - } diff --git a/packages/plugin/src/theia-proposed.d.ts b/packages/plugin/src/theia-proposed.d.ts index d89f6eb1b180c..ef289331098d2 100644 --- a/packages/plugin/src/theia-proposed.d.ts +++ b/packages/plugin/src/theia-proposed.d.ts @@ -361,4 +361,16 @@ declare module '@theia/plugin' { limitHit?: boolean; } //#endregion + + //#region read/write in chunks: https://github.com/microsoft/vscode/issues/84515 + + export interface FileSystemProvider { + open?(resource: Uri, options: { create: boolean; }): number | Thenable; + close?(fd: number): void | Thenable; + read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): number | Thenable; + write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): number | Thenable; + } + + //#endregion + } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index a59f916945cfc..774b807f36f4c 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -1582,6 +1582,171 @@ declare module '@theia/plugin' { waitUntil(thenable: PromiseLike): void; } + /** + * An event that is fired when files are going to be created. + * + * To make modifications to the workspace before the files are created, + * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a + * thenable that resolves to a [workspace edit](#WorkspaceEdit). + */ + export interface FileWillCreateEvent { + + /** + * The files that are going to be created. + */ + readonly files: ReadonlyArray; + + /** + * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * + * *Note:* This function can only be called during event dispatch and not + * in an asynchronous manner: + * + * ```ts + * workspace.onWillCreateFiles(event => { + * // async, will *throw* an error + * setTimeout(() => event.waitUntil(promise)); + * + * // sync, OK + * event.waitUntil(promise); + * }) + * ``` + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + + /** + * Allows to pause the event until the provided thenable resolves. + * + * *Note:* This function can only be called during event dispatch. + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + } + + /** + * An event that is fired after files are created. + */ + export interface FileCreateEvent { + + /** + * The files that got created. + */ + readonly files: ReadonlyArray; + } + + /** + * An event that is fired when files are going to be deleted. + * + * To make modifications to the workspace before the files are deleted, + * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a + * thenable that resolves to a [workspace edit](#WorkspaceEdit). + */ + export interface FileWillDeleteEvent { + + /** + * The files that are going to be deleted. + */ + readonly files: ReadonlyArray; + + /** + * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * + * *Note:* This function can only be called during event dispatch and not + * in an asynchronous manner: + * + * ```ts + * workspace.onWillCreateFiles(event => { + * // async, will *throw* an error + * setTimeout(() => event.waitUntil(promise)); + * + * // sync, OK + * event.waitUntil(promise); + * }) + * ``` + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + + /** + * Allows to pause the event until the provided thenable resolves. + * + * *Note:* This function can only be called during event dispatch. + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + } + + /** + * An event that is fired after files are deleted. + */ + export interface FileDeleteEvent { + + /** + * The files that got deleted. + */ + readonly files: ReadonlyArray; + } + + /** + * An event that is fired when files are going to be renamed. + * + * To make modifications to the workspace before the files are renamed, + * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a + * thenable that resolves to a [workspace edit](#WorkspaceEdit). + */ + export interface FileWillRenameEvent { + + /** + * The files that are going to be renamed. + */ + readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + + /** + * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * + * *Note:* This function can only be called during event dispatch and not + * in an asynchronous manner: + * + * ```ts + * workspace.onWillCreateFiles(event => { + * // async, will *throw* an error + * setTimeout(() => event.waitUntil(promise)); + * + * // sync, OK + * event.waitUntil(promise); + * }) + * ``` + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + + /** + * Allows to pause the event until the provided thenable resolves. + * + * *Note:* This function can only be called during event dispatch. + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + } + + /** + * An event that is fired after files are renamed. + */ + export interface FileRenameEvent { + + /** + * The files that got renamed. + */ + readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + } + export interface TextDocumentChangeEvent { document: TextDocument; @@ -4355,512 +4520,380 @@ declare module '@theia/plugin' { readonly index: number; } - /** - * Enumeration of file types. The types `File` and `Directory` can also be - * a symbolic links, in that use `FileType.File | FileType.SymbolicLink` and - * `FileType.Directory | FileType.SymbolicLink`. - */ + /** + * Enumeration of file types. The types `File` and `Directory` can also be + * a symbolic link, in that case use `FileType.File | FileType.SymbolicLink` and + * `FileType.Directory | FileType.SymbolicLink`. + */ export enum FileType { - /** - * The file type is unknown. - */ + /** + * The file type is unknown. + */ Unknown = 0, - /** - * A regular file. - */ + /** + * A regular file. + */ File = 1, - /** - * A directory. - */ + /** + * A directory. + */ Directory = 2, - /** - * A symbolic link to a file. - */ + /** + * A symbolic link to a file. + */ SymbolicLink = 64 } - /** - * The `FileStat`-type represents metadata about a file - */ + /** + * The `FileStat`-type represents metadata about a file + */ export interface FileStat { - /** - * The type of the file, e.g. is a regular file, a directory, or symbolic link - * to a file. - */ + /** + * The type of the file, e.g. is a regular file, a directory, or symbolic link + * to a file. + * + * *Note:* This value might be a bitmask, e.g. `FileType.File | FileType.SymbolicLink`. + */ type: FileType; - /** - * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. - */ + /** + * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ ctime: number; - /** - * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. - */ + /** + * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * + * *Note:* If the file changed, it is important to provide an updated `mtime` that advanced + * from the previous value. Otherwise there may be optimizations in place that will not show + * the updated file contents in an editor for example. + */ mtime: number; - /** - * The size in bytes. - */ + /** + * The size in bytes. + * + * *Note:* If the file changed, it is important to provide an updated `size`. Otherwise there + * may be optimizations in place that will not show the updated file contents in an editor for + * example. + */ size: number; } - /** - * A type that filesystem providers should use to signal errors. - * - * This class has factory methods for common error-cases, like `EntryNotFound` when - * a file or folder doesn't exist, use them like so: `throw vscode.FileSystemError.EntryNotFound(someUri);` - */ + /** + * A type that filesystem providers should use to signal errors. + * + * This class has factory methods for common error-cases, like `FileNotFound` when + * a file or folder doesn't exist, use them like so: `throw vscode.FileSystemError.FileNotFound(someUri);` + */ export class FileSystemError extends Error { - /** - * Create an error to signal that a file or folder wasn't found. - * @param messageOrUri Message or uri. - */ + /** + * Create an error to signal that a file or folder wasn't found. + * @param messageOrUri Message or uri. + */ static FileNotFound(messageOrUri?: string | Uri): FileSystemError; - /** - * Create an error to signal that a file or folder already exists, e.g. when - * creating but not overwriting a file. - * @param messageOrUri Message or uri. - */ + /** + * Create an error to signal that a file or folder already exists, e.g. when + * creating but not overwriting a file. + * @param messageOrUri Message or uri. + */ static FileExists(messageOrUri?: string | Uri): FileSystemError; - /** - * Create an error to signal that a file is not a folder. - * @param messageOrUri Message or uri. - */ + /** + * Create an error to signal that a file is not a folder. + * @param messageOrUri Message or uri. + */ static FileNotADirectory(messageOrUri?: string | Uri): FileSystemError; - /** - * Create an error to signal that a file is a folder. - * @param messageOrUri Message or uri. - */ + /** + * Create an error to signal that a file is a folder. + * @param messageOrUri Message or uri. + */ static FileIsADirectory(messageOrUri?: string | Uri): FileSystemError; - /** - * Create an error to signal that an operation lacks required permissions. - * @param messageOrUri Message or uri. - */ + /** + * Create an error to signal that an operation lacks required permissions. + * @param messageOrUri Message or uri. + */ static NoPermissions(messageOrUri?: string | Uri): FileSystemError; - /** - * Create an error to signal that the file system is unavailable or too busy to - * complete a request. - * @param messageOrUri Message or uri. - */ + /** + * Create an error to signal that the file system is unavailable or too busy to + * complete a request. + * @param messageOrUri Message or uri. + */ static Unavailable(messageOrUri?: string | Uri): FileSystemError; - /** - * Creates a new filesystem error. - * - * @param messageOrUri Message or uri. - */ + /** + * Creates a new filesystem error. + * + * @param messageOrUri Message or uri. + */ constructor(messageOrUri?: string | Uri); + + /** + * A code that identifies this error. + * + * Possible values are names of errors, like [`FileNotFound`](#FileSystemError.FileNotFound), + * or `Unknown` for unspecified errors. + */ + readonly code: string; } - /** - * Enumeration of file change types. - */ + /** + * Enumeration of file change types. + */ export enum FileChangeType { - /** - * The contents or metadata of a file have changed. - */ + /** + * The contents or metadata of a file have changed. + */ Changed = 1, - /** - * A file has been created. - */ + /** + * A file has been created. + */ Created = 2, - /** - * A file has been deleted. - */ + /** + * A file has been deleted. + */ Deleted = 3, } - /** - * The event filesystem providers must use to signal a file change. - */ + /** + * The event filesystem providers must use to signal a file change. + */ export interface FileChangeEvent { - /** - * The type of change. - */ - type: FileChangeType; + /** + * The type of change. + */ + readonly type: FileChangeType; - /** - * The uri of the file that has changed. - */ - uri: Uri; + /** + * The uri of the file that has changed. + */ + readonly uri: Uri; } - /** - * The filesystem provider defines what the editor needs to read, write, discover, - * and to manage files and folders. It allows extensions to serve files from remote places, - * like ftp-servers, and to seamlessly integrate those into the editor. - * - * * *Note 1:* The filesystem provider API works with [uris](#Uri) and assumes hierarchical - * paths, e.g. `foo:/my/path` is a child of `foo:/my/` and a parent of `foo:/my/path/deeper`. - * * *Note 2:* There is an activation event `onFileSystem:` that fires when a file - * or folder is being accessed. - * * *Note 3:* The word 'file' is often used to denote all [kinds](#FileType) of files, e.g. - * folders, symbolic links, and regular files. - */ + /** + * The filesystem provider defines what the editor needs to read, write, discover, + * and to manage files and folders. It allows extensions to serve files from remote places, + * like ftp-servers, and to seamlessly integrate those into the editor. + * + * * *Note 1:* The filesystem provider API works with [uris](#Uri) and assumes hierarchical + * paths, e.g. `foo:/my/path` is a child of `foo:/my/` and a parent of `foo:/my/path/deeper`. + * * *Note 2:* There is an activation event `onFileSystem:` that fires when a file + * or folder is being accessed. + * * *Note 3:* The word 'file' is often used to denote all [kinds](#FileType) of files, e.g. + * folders, symbolic links, and regular files. + */ export interface FileSystemProvider { - /** - * An event to signal that a resource has been created, changed, or deleted. This - * event should fire for resources that are being [watched](#FileSystemProvider.watch) - * by clients of this provider. - */ + /** + * An event to signal that a resource has been created, changed, or deleted. This + * event should fire for resources that are being [watched](#FileSystemProvider.watch) + * by clients of this provider. + * + * *Note:* It is important that the metadata of the file that changed provides an + * updated `mtime` that advanced from the previous value in the [stat](#FileStat) and a + * correct `size` value. Otherwise there may be optimizations in place that will not show + * the change in an editor for example. + */ readonly onDidChangeFile: Event; - /** - * Subscribe to events in the file or folder denoted by `uri`. - * - * The editor will call this function for files and folders. In the latter case, the - * options differ from defaults, e.g. what files/folders to exclude from watching - * and if subfolders, sub-subfolder, etc. should be watched (`recursive`). - * - * @param uri The uri of the file to be watched. - * @param options Configures the watch. - * @returns A disposable that tells the provider to stop watching the `uri`. - */ + /** + * Subscribe to events in the file or folder denoted by `uri`. + * + * The editor will call this function for files and folders. In the latter case, the + * options differ from defaults, e.g. what files/folders to exclude from watching + * and if subfolders, sub-subfolder, etc. should be watched (`recursive`). + * + * @param uri The uri of the file to be watched. + * @param options Configures the watch. + * @returns A disposable that tells the provider to stop watching the `uri`. + */ watch(uri: Uri, options: { recursive: boolean; excludes: string[] }): Disposable; - /** - * Retrieve metadata about a file. - * - * Note that the metadata for symbolic links should be the metadata of the file they refer to. - * Still, the [SymbolicLink](#FileType.SymbolicLink)-type must be used in addition to the actual type, e.g. - * `FileType.SymbolicLink | FileType.Directory`. - * - * @param uri The uri of the file to retrieve metadata about. - * @return The file metadata about the file. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. - */ - stat(uri: Uri): FileStat | PromiseLike; - - /** - * Retrieve all entries of a [directory](#FileType.Directory). - * - * @param uri The uri of the folder. - * @return An array of name/type-tuples or a thenable that resolves to such. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. - */ - readDirectory(uri: Uri): [string, FileType][] | PromiseLike<[string, FileType][]>; - - /** - * Create a new directory (Note, that new files are created via `write`-calls). - * - * @param uri The uri of the new folder. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when the parent of `uri` doesn't exist, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `uri` already exists. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. - */ - createDirectory(uri: Uri): void | PromiseLike; - - /** - * Read the entire contents of a file. - * - * @param uri The uri of the file. - * @return An array of bytes or a thenable that resolves to such. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. - */ - readFile(uri: Uri): Uint8Array | PromiseLike; - - /** - * Write data to a file, replacing its entire contents. - * - * @param uri The uri of the file. - * @param content The new content of the file. - * @param options Defines if missing files should or must be created. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist and `create` is not set. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when the parent of `uri` doesn't exist and `create` is set, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `uri` already exists, `create` is set but `overwrite` is not set. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. - */ - writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void | PromiseLike; - - /** - * Delete a file. - * - * @param uri The resource that is to be deleted. - * @param options Defines if deletion of folders is recursive. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. - */ - delete(uri: Uri, options: { recursive: boolean }): void | PromiseLike; - - /** - * Rename a file or folder. - * - * @param oldUri The existing file. - * @param newUri The new location. - * @param options Defines if existing files should be overwritten. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `oldUri` doesn't exist. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when parent of `newUri` doesn't exist, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `newUri` exists and when the `overwrite` option is not `true`. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. - */ - rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): void | PromiseLike; - - /** - * Copy files or folders. Implementing this function is optional but it will speedup - * the copy operation. - * - * @param source The existing file. - * @param destination The destination location. - * @param options Defines if existing files should be overwriten. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `source` doesn't exist. - * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when parent of `destination` doesn't exist, e.g. no mkdirp-logic required. - * @throws [`FileExists`](#FileSystemError.FileExists) when `destination` exists and when the `overwrite` option is not `true`. - * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. - */ - copy?(source: Uri, destination: Uri, options: { overwrite: boolean }): void | PromiseLike; - } - - /** - * The file system interface exposes the editor's built-in and contributed - * [file system providers](#FileSystemProvider). It allows extensions to work - * with files from the local disk as well as files from remote places, like the - * remote extension host or ftp-servers. - * - * *Note* that an instance of this interface is avaiable as [`workspace.fs`](#workspace.fs). - */ - export interface FileSystem { - - /** - * Retrieve metadata about a file. - * - * @param uri The uri of the file to retrieve metadata about. - * @return The file metadata about the file. - */ - stat(uri: Uri): PromiseLike; - - /** - * Retrieve all entries of a [directory](#FileType.Directory). - * - * @param uri The uri of the folder. - * @return An array of name/type-tuples or a PromiseLike that resolves to such. - */ - readDirectory(uri: Uri): PromiseLike<[string, FileType][]>; - - /** - * Create a new directory (Note, that new files are created via `write`-calls). - * - * *Note* that missing directories are created automatically, e.g this call has - * `mkdirp` semantics. - * - * @param uri The uri of the new folder. - */ - createDirectory(uri: Uri): PromiseLike; - - /** - * Read the entire contents of a file. - * - * @param uri The uri of the file. - * @return An array of bytes or a PromiseLike that resolves to such. - */ - readFile(uri: Uri): PromiseLike; - - /** - * Write data to a file, replacing its entire contents. - * - * @param uri The uri of the file. - * @param content The new content of the file. - */ - writeFile(uri: Uri, content: Uint8Array): PromiseLike; - - /** - * Delete a file. - * - * @param uri The resource that is to be deleted. - * @param options Defines if trash can should be used and if deletion of folders is recursive - */ - delete(uri: Uri, options?: { recursive?: boolean, useTrash?: boolean }): PromiseLike; - - /** - * Rename a file or folder. - * - * @param source The existing file. - * @param target The new location. - * @param options Defines if existing files should be overwritten. - */ - rename(source: Uri, target: Uri, options?: { overwrite?: boolean }): PromiseLike; + /** + * Retrieve metadata about a file. + * + * Note that the metadata for symbolic links should be the metadata of the file they refer to. + * Still, the [SymbolicLink](#FileType.SymbolicLink)-type must be used in addition to the actual type, e.g. + * `FileType.SymbolicLink | FileType.Directory`. + * + * @param uri The uri of the file to retrieve metadata about. + * @return The file metadata about the file. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. + */ + stat(uri: Uri): FileStat | Promise; - /** - * Copy files or folders. - * - * @param source The existing file. - * @param target The destination location. - * @param options Defines if existing files should be overwritten. - */ - copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): PromiseLike; - } + /** + * Retrieve all entries of a [directory](#FileType.Directory). + * + * @param uri The uri of the folder. + * @return An array of name/type-tuples or a thenable that resolves to such. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. + */ + readDirectory(uri: Uri): [string, FileType][] | Promise<[string, FileType][]>; - /** - * An event that is fired when files are going to be created. - * - * To make modifications to the workspace before the files are created, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). - */ - export interface FileWillCreateEvent { + /** + * Create a new directory (Note, that new files are created via `write`-calls). + * + * @param uri The uri of the new folder. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when the parent of `uri` doesn't exist, e.g. no mkdirp-logic required. + * @throws [`FileExists`](#FileSystemError.FileExists) when `uri` already exists. + * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + */ + createDirectory(uri: Uri): void | Promise; - /** - * The files that are going to be created. - */ - readonly files: ReadonlyArray; + /** + * Read the entire contents of a file. + * + * @param uri The uri of the file. + * @return An array of bytes or a thenable that resolves to such. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. + */ + readFile(uri: Uri): Uint8Array | Promise; - /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). - * - * *Note:* This function can only be called during event dispatch and not - * in an asynchronous manner: - * - * ```ts - * workspace.onWillCreateFiles(event => { - * // async, will *throw* an error - * setTimeout(() => event.waitUntil(promise)); - * - * // sync, OK - * event.waitUntil(promise); - * }) - * ``` - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; + /** + * Write data to a file, replacing its entire contents. + * + * @param uri The uri of the file. + * @param content The new content of the file. + * @param options Defines if missing files should or must be created. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist and `create` is not set. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when the parent of `uri` doesn't exist and `create` is set, e.g. no mkdirp-logic required. + * @throws [`FileExists`](#FileSystemError.FileExists) when `uri` already exists, `create` is set but `overwrite` is not set. + * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + */ + writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void | Promise; - /** - * Allows to pause the event until the provided thenable resolves. - * - * *Note:* This function can only be called during event dispatch. - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - } + /** + * Delete a file. + * + * @param uri The resource that is to be deleted. + * @param options Defines if deletion of folders is recursive. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `uri` doesn't exist. + * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + */ + delete(uri: Uri, options: { recursive: boolean }): void | Promise; - /** - * An event that is fired after files are created. - */ - export interface FileCreateEvent { + /** + * Rename a file or folder. + * + * @param oldUri The existing file. + * @param newUri The new location. + * @param options Defines if existing files should be overwritten. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `oldUri` doesn't exist. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when parent of `newUri` doesn't exist, e.g. no mkdirp-logic required. + * @throws [`FileExists`](#FileSystemError.FileExists) when `newUri` exists and when the `overwrite` option is not `true`. + * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + */ + rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): void | Promise; - /** - * The files that got created. - */ - readonly files: ReadonlyArray; + /** + * Copy files or folders. Implementing this function is optional but it will speedup + * the copy operation. + * + * @param source The existing file. + * @param destination The destination location. + * @param options Defines if existing files should be overwritten. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when `source` doesn't exist. + * @throws [`FileNotFound`](#FileSystemError.FileNotFound) when parent of `destination` doesn't exist, e.g. no mkdirp-logic required. + * @throws [`FileExists`](#FileSystemError.FileExists) when `destination` exists and when the `overwrite` option is not `true`. + * @throws [`NoPermissions`](#FileSystemError.NoPermissions) when permissions aren't sufficient. + */ + copy?(source: Uri, destination: Uri, options: { overwrite: boolean }): void | Promise; } - /** - * An event that is fired when files are going to be deleted. - * - * To make modifications to the workspace before the files are deleted, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). - */ - export interface FileWillDeleteEvent { - - /** - * The files that are going to be deleted. - */ - readonly files: ReadonlyArray; + /** + * The file system interface exposes the editor's built-in and contributed + * [file system providers](#FileSystemProvider). It allows extensions to work + * with files from the local disk as well as files from remote places, like the + * remote extension host or ftp-servers. + * + * *Note* that an instance of this interface is available as [`workspace.fs`](#workspace.fs). + */ - /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). - * - * *Note:* This function can only be called during event dispatch and not - * in an asynchronous manner: - * - * ```ts - * workspace.onWillCreateFiles(event => { - * // async, will *throw* an error - * setTimeout(() => event.waitUntil(promise)); - * - * // sync, OK - * event.waitUntil(promise); - * }) - * ``` - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - /** - * Allows to pause the event until the provided thenable resolves. - * - * *Note:* This function can only be called during event dispatch. - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - } + /** + * The file system interface exposes the editor's built-in and contributed + * [file system providers](#FileSystemProvider). It allows extensions to work + * with files from the local disk as well as files from remote places, like the + * remote extension host or ftp-servers. + * + * *Note* that an instance of this interface is available as [`workspace.fs`](#workspace.fs). + */ + export interface FileSystem { - /** - * An event that is fired after files are deleted. - */ - export interface FileDeleteEvent { + /** + * Retrieve metadata about a file. + * + * @param uri The uri of the file to retrieve metadata about. + * @return The file metadata about the file. + */ + stat(uri: Uri): Thenable; - /** - * The files that got deleted. - */ - readonly files: ReadonlyArray; - } + /** + * Retrieve all entries of a [directory](#FileType.Directory). + * + * @param uri The uri of the folder. + * @return An array of name/type-tuples or a thenable that resolves to such. + */ + readDirectory(uri: Uri): Thenable<[string, FileType][]>; - /** - * An event that is fired when files are going to be renamed. - * - * To make modifications to the workspace before the files are renamed, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). - */ - export interface FileWillRenameEvent { + /** + * Create a new directory (Note, that new files are created via `write`-calls). + * + * *Note* that missing directories are created automatically, e.g this call has + * `mkdirp` semantics. + * + * @param uri The uri of the new folder. + */ + createDirectory(uri: Uri): Thenable; - /** - * The files that are going to be renamed. - */ - readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + /** + * Read the entire contents of a file. + * + * @param uri The uri of the file. + * @return An array of bytes or a thenable that resolves to such. + */ + readFile(uri: Uri): Thenable; - /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). - * - * *Note:* This function can only be called during event dispatch and not - * in an asynchronous manner: - * - * ```ts - * workspace.onWillCreateFiles(event => { - * // async, will *throw* an error - * setTimeout(() => event.waitUntil(promise)); - * - * // sync, OK - * event.waitUntil(promise); - * }) - * ``` - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; + /** + * Write data to a file, replacing its entire contents. + * + * @param uri The uri of the file. + * @param content The new content of the file. + */ + writeFile(uri: Uri, content: Uint8Array): Thenable; - /** - * Allows to pause the event until the provided thenable resolves. - * - * *Note:* This function can only be called during event dispatch. - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - } + /** + * Delete a file. + * + * @param uri The resource that is to be deleted. + * @param options Defines if trash can should be used and if deletion of folders is recursive + */ + delete(uri: Uri, options?: { recursive?: boolean, useTrash?: boolean }): Thenable; - /** - * An event that is fired after files are renamed. - */ - export interface FileRenameEvent { + /** + * Rename a file or folder. + * + * @param oldUri The existing file. + * @param newUri The new location. + * @param options Defines if existing files should be overwritten. + */ + rename(source: Uri, target: Uri, options?: { overwrite?: boolean }): Thenable; - /** - * The files that got renamed. - */ - readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + /** + * Copy files or folders. + * + * @param source The existing file. + * @param destination The destination location. + * @param options Defines if existing files should be overwritten. + */ + copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): Thenable; } /** @@ -5183,18 +5216,18 @@ declare module '@theia/plugin' { */ export function applyEdit(edit: WorkspaceEdit): PromiseLike; - /** - * Register a filesystem provider for a given scheme, e.g. `ftp`. - * - * There can only be one provider per scheme and an error is being thrown when a scheme - * has been claimed by another provider or when it is reserved. - * - * @param scheme The uri-[scheme](#Uri.scheme) the provider registers for. - * @param provider The filesystem provider. - * @param options Immutable metadata about the provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. - */ - export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { isCaseSensitive?: boolean, isReadonly?: boolean }): Disposable; + /** + * Register a filesystem provider for a given scheme, e.g. `ftp`. + * + * There can only be one provider per scheme and an error is being thrown when a scheme + * has been claimed by another provider or when it is reserved. + * + * @param scheme The uri-[scheme](#Uri.scheme) the provider registers for. + * @param provider The filesystem provider. + * @param options Immutable metadata about the provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { readonly isCaseSensitive?: boolean, readonly isReadonly?: boolean }): Disposable; /** * Returns the [workspace folder](#WorkspaceFolder) that contains a given uri. @@ -7244,27 +7277,36 @@ declare module '@theia/plugin' { } /** - * A document link is a range in a text document that links to an internal or external resource, like another - * text document or a web site. - */ + * A document link is a range in a text document that links to an internal or external resource, like another + * text document or a web site. + */ export class DocumentLink { - /** - * The range this link applies to. - */ + /** + * The range this link applies to. + */ range: Range; - /** - * The uri this link points to. - */ + /** + * The uri this link points to. + */ target?: Uri; - /** - * Creates a new document link. - * - * @param range The range the document link applies to. Must not be empty. - * @param target The uri the document link points to. - */ + /** + * The tooltip text when you hover over this link. + * + * If a tooltip is provided, is will be displayed in a string that includes instructions on how to + * trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary depending on OS, + * user settings, and localization. + */ + tooltip?: string; + + /** + * Creates a new document link. + * + * @param range The range the document link applies to. Must not be empty. + * @param target The uri the document link points to. + */ constructor(range: Range, target?: Uri); } diff --git a/packages/preferences/src/browser/folder-preference-provider.ts b/packages/preferences/src/browser/folder-preference-provider.ts index cd460439a39df..8e7da67ebe56c 100644 --- a/packages/preferences/src/browser/folder-preference-provider.ts +++ b/packages/preferences/src/browser/folder-preference-provider.ts @@ -17,7 +17,7 @@ import { inject, injectable } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { PreferenceScope } from '@theia/core/lib/browser'; -import { FileStat } from '@theia/filesystem/lib/common'; +import { FileStat } from '@theia/filesystem/lib/common/files'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { SectionPreferenceProvider } from './section-preference-provider'; @@ -42,7 +42,7 @@ export class FolderPreferenceProvider extends SectionPreferenceProvider { get folderUri(): URI { if (!this._folderUri) { - this._folderUri = new URI(this.folder.uri); + this._folderUri = this.folder.resource; } return this._folderUri; } diff --git a/packages/preferences/src/browser/folders-preferences-provider.ts b/packages/preferences/src/browser/folders-preferences-provider.ts index 596972562eb25..bee0fded0bb97 100644 --- a/packages/preferences/src/browser/folders-preferences-provider.ts +++ b/packages/preferences/src/browser/folders-preferences-provider.ts @@ -22,7 +22,7 @@ import { PreferenceProvider, PreferenceResolveResult } from '@theia/core/lib/bro import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; import { FolderPreferenceProvider, FolderPreferenceProviderFactory } from './folder-preference-provider'; -import { FileStat } from '@theia/filesystem/lib/common'; +import { FileStat } from '@theia/filesystem/lib/common/files'; @injectable() export class FoldersPreferencesProvider extends PreferenceProvider { @@ -58,7 +58,7 @@ export class FoldersPreferencesProvider extends PreferenceProvider { for (const folder of roots) { for (const configPath of this.configurations.getPaths()) { for (const configName of [...this.configurations.getSectionNames(), this.configurations.getConfigName()]) { - const sectionUri = this.configurations.createUri(new URI(folder.uri), configPath, configName); + const sectionUri = this.configurations.createUri(folder.resource, configPath, configName); const sectionKey = sectionUri.toString(); toDelete.delete(sectionKey); if (!this.providers.has(sectionKey)) { @@ -98,7 +98,7 @@ export class FoldersPreferencesProvider extends PreferenceProvider { } getDomain(): string[] { - return this.workspaceService.tryGetRoots().map(root => root.uri); + return this.workspaceService.tryGetRoots().map(root => root.resource.toString()); } resolve(preferenceName: string, resourceUri?: string): PreferenceResolveResult { diff --git a/packages/preferences/src/browser/preferences-contribution.ts b/packages/preferences/src/browser/preferences-contribution.ts index 8e5b0a822545f..cf45d3d2e2ca6 100644 --- a/packages/preferences/src/browser/preferences-contribution.ts +++ b/packages/preferences/src/browser/preferences-contribution.ts @@ -30,7 +30,6 @@ import { import { isFirefox } from '@theia/core/lib/browser'; import { isOSX } from '@theia/core/lib/common/os'; import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { EditorManager, EditorWidget } from '@theia/editor/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { PreferencesWidget } from './views/preference-widget'; @@ -39,12 +38,13 @@ import { WorkspacePreferenceProvider } from './workspace-preference-provider'; import { USER_PREFERENCE_URI } from './user-preference-provider'; import { Preference, PreferencesCommands, PreferenceMenus } from './util/preference-types'; import { ClipboardService } from '@theia/core/lib/browser/clipboard-service'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; @injectable() export class PreferencesContribution extends AbstractViewContribution { @inject(PreferencesEventService) protected readonly preferencesEventService: PreferencesEventService; - @inject(FileSystem) protected readonly filesystem: FileSystem; + @inject(FileService) protected readonly fileService: FileService; @inject(PreferenceProvider) @named(PreferenceScope.Workspace) protected readonly workspacePreferenceProvider: WorkspacePreferenceProvider; @inject(EditorManager) protected readonly editorManager: EditorManager; @inject(PreferenceService) protected readonly preferenceValueRetrievalService: PreferenceService; @@ -181,13 +181,12 @@ export class PreferencesContribution extends AbstractViewContribution { - const folderSettingsURI = `${folderURI}/.theia/settings.json`; - if (folderSettingsURI && !await this.filesystem.exists(folderSettingsURI)) { - await this.filesystem.createFile(folderSettingsURI); + const folderSettingsURI = new URI(folderURI).resolve('.theia/settings.json'); + if (!await this.fileService.exists(folderSettingsURI)) { + await this.fileService.create(folderSettingsURI); } - return new URI(folderSettingsURI); + return folderSettingsURI; } /** diff --git a/packages/preferences/src/browser/user-preference-provider.ts b/packages/preferences/src/browser/user-preference-provider.ts index 5acc9cb760cd6..449e6e400c316 100644 --- a/packages/preferences/src/browser/user-preference-provider.ts +++ b/packages/preferences/src/browser/user-preference-provider.ts @@ -16,7 +16,7 @@ import { injectable } from 'inversify'; import URI from '@theia/core/lib/common/uri'; -import { UserStorageUri } from '@theia/userstorage/lib/browser'; +import { UserStorageUri } from '@theia/userstorage/lib/browser/user-storage-uri'; import { PreferenceScope } from '@theia/core/lib/browser'; import { SectionPreferenceProvider } from './section-preference-provider'; diff --git a/packages/preferences/src/browser/util/preference-scope-command-manager.ts b/packages/preferences/src/browser/util/preference-scope-command-manager.ts index 98b654704453d..680e8f2a919c5 100644 --- a/packages/preferences/src/browser/util/preference-scope-command-manager.ts +++ b/packages/preferences/src/browser/util/preference-scope-command-manager.ts @@ -16,8 +16,7 @@ import { injectable, inject } from 'inversify'; import { PreferenceScope, LabelProvider } from '@theia/core/lib/browser'; -import { FileStat } from '@theia/filesystem/lib/common'; -import URI from '@theia/core/lib/common/uri'; +import { FileStat } from '@theia/filesystem/lib/common/files'; import { CommandRegistry, MenuModelRegistry, Command } from '@theia/core/lib/common'; import { Preference } from './preference-types'; @@ -42,11 +41,11 @@ export class PreferenceScopeCommandManager { this.foldersAsCommands.length = 0; folderWorkspaces.forEach(folderWorkspace => { - const folderLabel = this.labelProvider.getName(new URI(folderWorkspace.uri)); + const folderLabel = this.labelProvider.getName(folderWorkspace.resource); - const iconClass = currentFolderURI === folderWorkspace.uri ? 'fa fa-check' : ''; + const iconClass = currentFolderURI === folderWorkspace.resource.toString() ? 'fa fa-check' : ''; const newFolderAsCommand = { - id: `preferenceScopeCommand:${folderWorkspace.uri}`, + id: `preferenceScopeCommand:${folderWorkspace.resource.toString()}`, label: folderLabel, iconClass: iconClass }; @@ -57,7 +56,7 @@ export class PreferenceScopeCommandManager { isVisible: (callback, check) => check === 'from-tabbar', isEnabled: (callback, check) => check === 'from-tabbar', execute: (callback: (scopeDetails: Preference.SelectedScopeDetails) => void) => { - callback({ scope: PreferenceScope.Folder.toString(), uri: folderWorkspace.uri, activeScopeIsFolder: 'true' }); + callback({ scope: PreferenceScope.Folder.toString(), uri: folderWorkspace.resource.toString(), activeScopeIsFolder: 'true' }); } }); diff --git a/packages/preferences/src/browser/views/preference-scope-tabbar-widget.tsx b/packages/preferences/src/browser/views/preference-scope-tabbar-widget.tsx index 8fa54e33c58e0..9044bacc46929 100644 --- a/packages/preferences/src/browser/views/preference-scope-tabbar-widget.tsx +++ b/packages/preferences/src/browser/views/preference-scope-tabbar-widget.tsx @@ -19,7 +19,7 @@ import { TabBar, Widget, Title } from '@phosphor/widgets'; import { PreferenceScope, Message, ContextMenuRenderer, LabelProvider } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import URI from '@theia/core/lib/common/uri'; -import { FileStat } from '@theia/filesystem/lib/common'; +import { FileStat } from '@theia/filesystem/lib/common/files'; import { PreferencesEventService } from '../util/preference-event-service'; import { PreferenceScopeCommandManager, FOLDER_SCOPE_MENU_PATH } from '../util/preference-scope-command-manager'; import { Preference } from '../util/preference-types'; @@ -143,10 +143,10 @@ export class PreferencesScopeTabBar extends TabBar { } protected getWorkspaceDataset(): Preference.SelectedScopeDetails { - const { uri, isDirectory } = this.workspaceService.workspace!; + const { resource, isDirectory } = this.workspaceService.workspace!; const scope = WORKSPACE_TAB_INDEX; const activeScopeIsFolder = isDirectory.toString(); - return { uri, activeScopeIsFolder, scope }; + return { uri: resource.toString(), activeScopeIsFolder, scope }; } protected addOrUpdateFolderTab(): void { @@ -187,9 +187,9 @@ export class PreferencesScopeTabBar extends TabBar { this.folderTitle.dataset = { ...this.currentSelection, folderTitle: 'true' }; this.folderTitle.className = multipleFolderRootsAreAvailable ? SELECTED_FOLDER_DROPDOWN_CLASSNAME : SINGLE_FOLDER_TAB_CLASSNAME; } else { - const singleFolderRoot = this.currentWorkspaceRoots[0].uri; - const singleFolderLabel = this.labelProvider.getName(new URI(singleFolderRoot)); - const defaultURI = multipleFolderRootsAreAvailable ? '' : singleFolderRoot; + const singleFolderRoot = this.currentWorkspaceRoots[0].resource; + const singleFolderLabel = this.labelProvider.getName(singleFolderRoot); + const defaultURI = multipleFolderRootsAreAvailable ? '' : singleFolderRoot.toString(); this.folderTitle.label = multipleFolderRootsAreAvailable ? FOLDER_TAB_LABEL : singleFolderLabel; this.folderTitle.className = multipleFolderRootsAreAvailable ? UNSELECTED_FOLDER_DROPDOWN_CLASSNAME : SINGLE_FOLDER_TAB_CLASSNAME; this.folderTitle.dataset = { folderTitle: 'true', scope: FOLDER_TAB_INDEX, uri: defaultURI }; @@ -227,7 +227,7 @@ export class PreferencesScopeTabBar extends TabBar { const folderWasRemoved = newRoots.length < this.currentWorkspaceRoots.length; this.currentWorkspaceRoots = newRoots; if (folderWasRemoved) { - const removedFolderWasSelectedScope = !this.currentWorkspaceRoots.some(root => root.uri === this.currentSelection.uri); + const removedFolderWasSelectedScope = !this.currentWorkspaceRoots.some(root => root.resource.toString() === this.currentSelection.uri); if (removedFolderWasSelectedScope) { this.setNewScopeSelection(Preference.DEFAULT_SCOPE); } diff --git a/packages/preferences/src/browser/workspace-file-preference-provider.ts b/packages/preferences/src/browser/workspace-file-preference-provider.ts index 37b79d9a15eed..fa4e0fe181836 100644 --- a/packages/preferences/src/browser/workspace-file-preference-provider.ts +++ b/packages/preferences/src/browser/workspace-file-preference-provider.ts @@ -60,6 +60,6 @@ export class WorkspaceFilePreferenceProvider extends AbstractResourcePreferenceP getDomain(): string[] { // workspace file is treated as part of the workspace - return this.workspaceService.tryGetRoots().map(r => r.uri).concat([this.options.workspaceUri.toString()]); + return this.workspaceService.tryGetRoots().map(r => r.resource.toString()).concat([this.options.workspaceUri.toString()]); } } diff --git a/packages/preferences/src/browser/workspace-preference-provider.ts b/packages/preferences/src/browser/workspace-preference-provider.ts index 49d4071f1edf5..41cbfd9d7271c 100644 --- a/packages/preferences/src/browser/workspace-preference-provider.ts +++ b/packages/preferences/src/browser/workspace-preference-provider.ts @@ -80,7 +80,7 @@ export class WorkspacePreferenceProvider extends PreferenceProvider { return this.folderPreferenceProvider; } return this.workspaceFileProviderFactory({ - workspaceUri: new URI(workspace.uri) + workspaceUri: workspace.resource }); } @@ -109,7 +109,7 @@ export class WorkspacePreferenceProvider extends PreferenceProvider { protected ensureResourceUri(): string | undefined { if (this.workspaceService.workspace && !this.workspaceService.isMultiRootWorkspaceOpened) { - return this.workspaceService.workspace.uri; + return this.workspaceService.workspace.resource.toString(); } return undefined; } diff --git a/packages/process/src/node/multi-ring-buffer.ts b/packages/process/src/node/multi-ring-buffer.ts index a33b840b2cc28..7a5d5c9970c14 100644 --- a/packages/process/src/node/multi-ring-buffer.ts +++ b/packages/process/src/node/multi-ring-buffer.ts @@ -44,7 +44,7 @@ export class MultiRingBufferReadableStream extends stream.Readable implements Di this.deq(size); } - _destroy(err: Error | undefined, callback: (err?: Error) => void): void { + _destroy(err: Error | null, callback: (err: Error | null) => void): void { this.ringBuffer.closeStream(this); this.ringBuffer.closeReader(this.reader); this.disposed = true; diff --git a/packages/scm-extra/src/browser/history/scm-history-widget.tsx b/packages/scm-extra/src/browser/history/scm-history-widget.tsx index 8623e40103efb..7496a415923c6 100644 --- a/packages/scm-extra/src/browser/history/scm-history-widget.tsx +++ b/packages/scm-extra/src/browser/history/scm-history-widget.tsx @@ -25,13 +25,13 @@ import { ScmService } from '@theia/scm/lib/browser/scm-service'; import { ScmHistoryProvider } from '.'; import { SCM_HISTORY_ID, SCM_HISTORY_MAX_COUNT, SCM_HISTORY_LABEL } from './scm-history-contribution'; import { ScmHistoryCommit, ScmFileChange } from '../scm-file-change-node'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { ScmAvatarService } from '@theia/scm/lib/browser/scm-avatar-service'; import { ScmItemComponent } from '../scm-navigable-list-widget'; import { ScmFileChangeNode } from '../scm-file-change-node'; import { ScmNavigableListWidget } from '../scm-navigable-list-widget'; import * as React from 'react'; import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export const ScmHistorySupport = Symbol('scm-history-support'); export interface ScmHistorySupport { @@ -92,7 +92,7 @@ export class ScmHistoryWidget extends ScmNavigableListWidget @inject(ScmService) protected readonly scmService: ScmService, @inject(OpenerService) protected readonly openerService: OpenerService, @inject(ApplicationShell) protected readonly shell: ApplicationShell, - @inject(FileSystem) protected readonly fileSystem: FileSystem, + @inject(FileService) protected readonly fileService: FileService, @inject(ScmAvatarService) protected readonly avatarService: ScmAvatarService, @inject(WidgetManager) protected readonly widgetManager: WidgetManager, ) { @@ -187,8 +187,12 @@ export class ScmHistoryWidget extends ScmNavigableListWidget async setContent(options?: HistoryWidgetOptions): Promise { this.resetState(options); if (options && options.uri) { - const fileStat = await this.fileSystem.getFileStat(options.uri); - this.singleFileMode = !!fileStat && !fileStat.isDirectory; + try { + const fileStat = await this.fileService.resolve(new URI(options.uri)); + this.singleFileMode = !fileStat.isDirectory; + } catch { + this.singleFileMode = true; + } } await this.addCommits(options); this.onDataReady(); @@ -328,7 +332,7 @@ export class ScmHistoryWidget extends ScmNavigableListWidget const relPath = relPathEncoded ? `${decodeURIComponent(relPathEncoded)}` : ''; const repo = this.scmService.findRepository(new URI(this.options.uri)); - const repoName = repo ? `${new URI(repo.provider.rootUri).displayName}` : ''; + const repoName = repo ? `${this.labelProvider.getName(new URI(repo.provider.rootUri))}` : ''; const relPathAndRepo = [relPath, repoName].filter(Boolean).join(' in '); path = ` for ${relPathAndRepo}`; @@ -442,24 +446,24 @@ export class ScmHistoryWidget extends ScmNavigableListWidget
-
-
- {commit.commitDetails.summary} -
-
- {commit.commitDetails.authorDateRelative + ' by ' + commit.commitDetails.authorName} -
-
-
this.openDetailWidget(commit)}>
- { - !this.singleFileMode ?
-
-
{commit.commitDetails.fileChanges.length.toString()}
-
+
+
+ {commit.commitDetails.summary} +
+
+ {commit.commitDetails.authorDateRelative + ' by ' + commit.commitDetails.authorName}
- : '' - } +
this.openDetailWidget(commit)}>
+ { + !this.singleFileMode ?
+
+
{commit.commitDetails.fileChanges.length.toString()}
+
+
+
+ : '' + }
; } diff --git a/packages/scm/src/browser/scm-contribution.ts b/packages/scm/src/browser/scm-contribution.ts index ff94f8582839b..8095d16743c68 100644 --- a/packages/scm/src/browser/scm-contribution.ts +++ b/packages/scm/src/browser/scm-contribution.ts @@ -25,7 +25,8 @@ import { StatusBarEntry, KeybindingRegistry, ViewContainerTitleOptions, - ViewContainer} from '@theia/core/lib/browser'; + ViewContainer +} from '@theia/core/lib/browser'; import { TabBarToolbarContribution, TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { CommandRegistry, Command, Disposable, DisposableCollection, CommandService } from '@theia/core/lib/common'; import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service'; diff --git a/packages/scm/src/browser/scm-quick-open-service.ts b/packages/scm/src/browser/scm-quick-open-service.ts index c204460157d02..7f1faf9ec91ea 100644 --- a/packages/scm/src/browser/scm-quick-open-service.ts +++ b/packages/scm/src/browser/scm-quick-open-service.ts @@ -19,16 +19,16 @@ import { QuickOpenItem, QuickOpenMode, QuickOpenModel } from '@theia/core/lib/br import { QuickOpenService, QuickOpenOptions } from '@theia/core/lib/browser/quick-open/quick-open-service'; import { MessageService } from '@theia/core/lib/common/message-service'; import URI from '@theia/core/lib/common/uri'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { ScmService } from './scm-service'; import { ScmRepository } from './scm-repository'; +import { LabelProvider } from '@theia/core/lib/browser/label-provider'; @injectable() export class ScmQuickOpenService { @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService; @inject(MessageService) protected readonly messageService: MessageService; - @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @inject(ScmService) protected readonly scmService: ScmService; async changeRepository(): Promise { @@ -39,9 +39,8 @@ export class ScmQuickOpenService { const execute = () => { this.scmService.selectedRepository = repository; }; - const toLabel = () => uri.path.base; - const fsPath = await this.fileSystem.getFsPath(uri.toString()); - const toDescription = () => fsPath; + const toLabel = () => this.labelProvider.getName(uri); + const toDescription = () => this.labelProvider.getLongName(uri); return new ScmQuickOpenItem(repository, execute, toLabel, toDescription); })); this.open(items, 'Select repository to work with:'); diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts index 2973e56d9154e..04d2577ce05bc 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts @@ -23,11 +23,11 @@ import { NavigatorContextMenu } from '@theia/navigator/lib/browser/navigator-con import { UriCommandHandler, UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import URI from '@theia/core/lib/common/uri'; import { WorkspaceService } from '@theia/workspace/lib/browser'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { Range } from 'vscode-languageserver-types'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export namespace SearchInWorkspaceCommands { const SEARCH_CATEGORY = 'Search'; @@ -77,7 +77,7 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut @inject(SelectionService) protected readonly selectionService: SelectionService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; - @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(FileService) protected readonly fileService: FileService; @inject(EditorManager) protected readonly editorManager: EditorManager; @inject(SearchInWorkspaceContextKeyService) @@ -120,20 +120,16 @@ export class SearchInWorkspaceFrontendContribution extends AbstractViewContribut commands.registerCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER, this.newMultiUriAwareCommandHandler({ execute: async uris => { const resources: string[] = []; - await Promise.all(uris.map(uri => - this.fileSystem.getFileStat(uri.toString()) - )).then(stats => { - for (const stat of stats) { - if (stat) { - const uri = new URI(stat.uri); - let uriStr = this.labelProvider.getLongName(uri); - if (stat && !stat.isDirectory) { - uriStr = this.labelProvider.getLongName(uri.parent); - } - resources.push(uriStr); + for (const { stat } of await this.fileService.resolveAll(uris.map(resource => ({ resource })))) { + if (stat) { + const uri = stat.resource; + let uriStr = this.labelProvider.getLongName(uri); + if (stat && !stat.isDirectory) { + uriStr = this.labelProvider.getLongName(uri.parent); } + resources.push(uriStr); } - }); + } const widget = await this.openView({ activate: true }); widget.findInFolder(resources); } diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-service.ts b/packages/search-in-workspace/src/browser/search-in-workspace-service.ts index 0505bd4f3ad33..20819180f2585 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-service.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-service.ts @@ -114,7 +114,7 @@ export class SearchInWorkspaceService implements SearchInWorkspaceClient { } const roots = await this.workspaceService.roots; - return this.doSearch(what, roots.map(r => r.uri), callbacks, opts); + return this.doSearch(what, roots.map(r => r.resource.toString()), callbacks, opts); } protected async doSearch(what: string, rootsUris: string[], callbacks: SearchInWorkspaceCallbacks, opts?: SearchInWorkspaceOptions): Promise { diff --git a/packages/task/src/browser/quick-open-task.ts b/packages/task/src/browser/quick-open-task.ts index 99b577be001da..690b456483631 100644 --- a/packages/task/src/browser/quick-open-task.ts +++ b/packages/task/src/browser/quick-open-task.ts @@ -22,7 +22,6 @@ import URI from '@theia/core/lib/common/uri'; import { QuickOpenHandler, QuickOpenService, QuickOpenOptions, QuickOpenBaseAction, LabelProvider } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { QuickOpenModel, QuickOpenItem, QuickOpenActionProvider, QuickOpenMode, QuickOpenGroupItem, QuickOpenGroupItemOptions, QuickOpenAction } from '@theia/core/lib/common/quick-open-model'; @@ -99,9 +98,6 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { @inject(TaskSourceResolver) protected readonly taskSourceResolver: TaskSourceResolver; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; - @inject(TaskConfigurationManager) protected readonly taskConfigurationManager: TaskConfigurationManager; @@ -270,7 +266,7 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { isFirstGroup = false; } - const rootUris = (await this.workspaceService.roots).map(rootStat => rootStat.uri); + const rootUris = (await this.workspaceService.roots).map(rootStat => rootStat.resource.toString()); for (const rootFolder of rootUris) { const folderName = new URI(rootFolder).displayName; if (groupedTasks.has(rootFolder)) { diff --git a/packages/task/src/browser/task-configuration-manager.ts b/packages/task/src/browser/task-configuration-manager.ts index 0a3856f8bcc43..b3b038c7902c9 100644 --- a/packages/task/src/browser/task-configuration-manager.ts +++ b/packages/task/src/browser/task-configuration-manager.ts @@ -26,9 +26,9 @@ import { TaskConfigurationModel } from './task-configuration-model'; import { TaskTemplateSelector } from './task-templates'; import { TaskCustomization, TaskConfiguration, TaskConfigurationScope, TaskScope } from '../common/task-protocol'; import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution'; -import { FileSystem, FileSystemError } from '@theia/filesystem/lib/common'; import { FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; export interface TasksChange { scope: TaskConfigurationScope; @@ -50,8 +50,8 @@ export class TaskConfigurationManager { @inject(QuickPickService) protected readonly quickPick: QuickPickService; - @inject(FileSystem) - protected readonly filesystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; @inject(PreferenceProvider) @named(PreferenceScope.Folder) protected readonly folderPreferences: PreferenceProvider; @@ -93,7 +93,7 @@ export class TaskConfigurationManager { const roots = await this.workspaceService.roots; const toDelete = new Set(this.models.keys()); for (const rootStat of roots) { - const key = rootStat.uri; + const key = rootStat.resource.toString(); toDelete.delete(key); if (!this.models.has(key)) { const model = new TaskConfigurationModel(key, this.folderPreferences); @@ -194,18 +194,7 @@ export class TaskConfigurationManager { } else { // fallback uri = new URI(model.getWorkspaceFolder()).resolve(`${this.preferenceConfigurations.getPaths()[0]}/tasks.json`); } - - const fileStat = await this.filesystem.getFileStat(uri.toString()); - if (!fileStat) { - throw new Error(`file not found: ${uri.toString()}`); - } - try { - this.filesystem.setContent(fileStat, content); - } catch (e) { - if (!FileSystemError.FileExists.is(e)) { - throw e; - } - } + await this.fileService.write(uri, content); return uri; } } diff --git a/packages/task/src/browser/task-service.ts b/packages/task/src/browser/task-service.ts index b86ba6e452e51..10f175a1b4762 100644 --- a/packages/task/src/browser/task-service.ts +++ b/packages/task/src/browser/task-service.ts @@ -1126,7 +1126,7 @@ export class TaskService implements TaskConfigurationClient { } protected getContext(): string | undefined { - return this.workspaceService.workspace && this.workspaceService.workspace.uri; + return this.workspaceService.workspace?.resource.toString(); } /** Kill task for a given id if task is found */ diff --git a/packages/task/src/browser/task-terminal-widget-manager.ts b/packages/task/src/browser/task-terminal-widget-manager.ts index cb35f082350a0..a316a25124aec 100644 --- a/packages/task/src/browser/task-terminal-widget-manager.ts +++ b/packages/task/src/browser/task-terminal-widget-manager.ts @@ -102,7 +102,7 @@ export class TaskTerminalWidgetManager { const terminal = TaskTerminalWidget.is(widget) && widget; if (terminal) { const didConnectListener = terminal.onDidOpen(async () => { - const context = this.workspaceService.workspace && this.workspaceService.workspace.uri; + const context = this.workspaceService?.workspace?.resource.toString(); const tasksInfo = await this.taskServer.getTasks(context); const taskInfo = tasksInfo.find(info => info.terminalId === widget.terminalId); if (taskInfo) { diff --git a/packages/terminal/src/browser/terminal-frontend-contribution.ts b/packages/terminal/src/browser/terminal-frontend-contribution.ts index ff064c131e947..88e7e1bda79ae 100644 --- a/packages/terminal/src/browser/terminal-frontend-contribution.ts +++ b/packages/terminal/src/browser/terminal-frontend-contribution.ts @@ -39,7 +39,6 @@ import { TerminalKeybindingContexts } from './terminal-keybinding-contexts'; import { TerminalService } from './base/terminal-service'; import { TerminalWidgetOptions, TerminalWidget } from './base/terminal-widget'; import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { ShellTerminalServerProxy } from '../common/shell-terminal-protocol'; import URI from '@theia/core/lib/common/uri'; import { MAIN_MENU_BAR } from '@theia/core'; @@ -48,6 +47,8 @@ import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { terminalAnsiColorMap } from './terminal-theme-service'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat } from '@theia/filesystem/lib/common/files'; export namespace TerminalMenus { export const TERMINAL = [...MAIN_MENU_BAR, '7_terminal']; @@ -135,13 +136,11 @@ export namespace TerminalCommands { @injectable() export class TerminalFrontendContribution implements TerminalService, CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution, ColorContribution { - constructor( - @inject(ApplicationShell) protected readonly shell: ApplicationShell, - @inject(ShellTerminalServerProxy) protected readonly shellTerminalServer: ShellTerminalServerProxy, - @inject(WidgetManager) protected readonly widgetManager: WidgetManager, - @inject(FileSystem) protected readonly fileSystem: FileSystem, - @inject(SelectionService) protected readonly selectionService: SelectionService - ) { } + @inject(ApplicationShell) protected readonly shell: ApplicationShell; + @inject(ShellTerminalServerProxy) protected readonly shellTerminalServer: ShellTerminalServerProxy; + @inject(WidgetManager) protected readonly widgetManager: WidgetManager; + @inject(FileService) protected readonly fileService: FileService; + @inject(SelectionService) protected readonly selectionService: SelectionService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @@ -356,8 +355,10 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon async openInTerminal(uri: URI): Promise { // Determine folder path of URI - const stat = await this.fileSystem.getFileStat(uri.toString()); - if (!stat) { + let stat: FileStat; + try { + stat = await this.fileService.resolve(uri); + } catch { return; } @@ -566,7 +567,7 @@ export class TerminalFrontendContribution implements TerminalService, CommandCon protected async selectTerminalCwd(): Promise { const roots = this.workspaceService.tryGetRoots(); return this.quickPick.show(roots.map( - ({ uri }) => ({ label: this.labelProvider.getName(new URI(uri)), description: this.labelProvider.getLongName(new URI(uri)), value: uri }) + ({ resource }) => ({ label: this.labelProvider.getName(resource), description: this.labelProvider.getLongName(resource), value: resource.toString() }) ), { placeholder: 'Select current working directory for new terminal' }); } diff --git a/packages/terminal/src/browser/terminal-linkmatcher-files.ts b/packages/terminal/src/browser/terminal-linkmatcher-files.ts index 090b45c5dd36d..be6472be056e6 100644 --- a/packages/terminal/src/browser/terminal-linkmatcher-files.ts +++ b/packages/terminal/src/browser/terminal-linkmatcher-files.ts @@ -22,15 +22,15 @@ import { Position } from '@theia/editor/lib/browser'; import { AbstractCmdClickTerminalContribution } from './terminal-linkmatcher'; import { TerminalWidgetImpl } from './terminal-widget-impl'; import { Path } from '@theia/core'; -import { FileSystem } from '@theia/filesystem/lib/common'; import URI from '@theia/core/lib/common/uri'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; @injectable() export class TerminalLinkmatcherFiles extends AbstractCmdClickTerminalContribution { @inject(ApplicationServer) protected appServer: ApplicationServer; @inject(OpenerService) protected openerService: OpenerService; - @inject(FileSystem) protected fileSystem: FileSystem; + @inject(FileService) protected fileService: FileService; protected backendOs: Promise; @@ -51,9 +51,10 @@ export class TerminalLinkmatcherFiles extends AbstractCmdClickTerminalContributi const toOpen = await this.toURI(match, await terminalWidget.cwd); if (toOpen) { // TODO: would be better to ask the opener service, but it returns positively even for unknown files. - const f = await this.fileSystem.getFileStat(toOpen.toString()); - // eslint-disable-next-line no-null/no-null - return f !== undefined && f !== null && !f.isDirectory; + try { + const stat = await this.fileService.resolve(toOpen); + return !stat.isDirectory; + } catch { } } } catch (err) { console.trace('Error validating ' + match); diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index b4b4999fc7ddc..4850b5436e228 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -394,7 +394,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget let rootURI = this.options.cwd; if (!rootURI) { const root = (await this.workspaceService.roots)[0]; - rootURI = root && root.uri; + rootURI = root?.resource?.toString(); } const { cols, rows } = this.term; diff --git a/packages/userstorage/src/browser/index.ts b/packages/userstorage/src/browser/index.ts index 42d1f8c9f5172..0d7d83e01ff2c 100644 --- a/packages/userstorage/src/browser/index.ts +++ b/packages/userstorage/src/browser/index.ts @@ -14,8 +14,5 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export * from './user-storage-service'; -export * from './user-storage-resource'; export * from './user-storage-uri'; -export * from './user-storage-service-filesystem'; export * from './user-storage-frontend-module'; diff --git a/packages/userstorage/src/browser/user-storage-contribution.ts b/packages/userstorage/src/browser/user-storage-contribution.ts new file mode 100644 index 0000000000000..b628ea7ee1b4f --- /dev/null +++ b/packages/userstorage/src/browser/user-storage-contribution.ts @@ -0,0 +1,62 @@ +/******************************************************************************** + * Copyright (C) 2020 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { FileSystemProvider } from '@theia/filesystem/lib/common/files'; +import { FileService, FileServiceContribution } from '@theia/filesystem/lib/browser/file-service'; +import { DelegatingFileSystemProvider } from '@theia/filesystem/lib/common/delegating-file-system-provider'; +import { UserStorageUri } from './user-storage-uri'; + +@injectable() +export class UserStorageContribution implements FileServiceContribution { + + @inject(EnvVariablesServer) + protected readonly environments: EnvVariablesServer; + + registerFileSystemProviders(service: FileService): void { + service.onWillActivateFileSystemProvider(event => { + if (event.scheme === UserStorageUri.SCHEME) { + event.waitUntil((async () => { + const provider = await this.createProvider(service); + service.registerProvider(UserStorageUri.SCHEME, provider); + })()); + } + }); + } + + protected async createProvider(service: FileService): Promise { + const delegate = await service.activateProvider('file'); + const configDirUri = new URI(await this.environments.getConfigDirUri()); + return new DelegatingFileSystemProvider(delegate, { + uriConverter: { + to: resource => configDirUri.resolve(resource.path).normalizePath(), + from: resource => { + const relativePath = configDirUri.relative(resource); + if (relativePath) { + return resource.withScheme(UserStorageUri.SCHEME).withPath('/' + relativePath); + } + return undefined; + } + } + }, new DisposableCollection( + delegate.watch(configDirUri, { excludes: [], recursive: true }) + )); + } + +} diff --git a/packages/userstorage/src/browser/user-storage-frontend-module.ts b/packages/userstorage/src/browser/user-storage-frontend-module.ts index 02979d8226eee..a8c8a898a5383 100644 --- a/packages/userstorage/src/browser/user-storage-frontend-module.ts +++ b/packages/userstorage/src/browser/user-storage-frontend-module.ts @@ -14,17 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { ContainerModule, interfaces, } from 'inversify'; -import { ResourceResolver } from '@theia/core/lib/common'; -import { UserStorageResolver } from './user-storage-resource'; -import { UserStorageServiceFilesystemImpl } from './user-storage-service-filesystem'; -import { UserStorageService } from './user-storage-service'; - -export function bindUserStorage(bind: interfaces.Bind): void { - bind(ResourceResolver).to(UserStorageResolver).inSingletonScope(); - bind(UserStorageService).to(UserStorageServiceFilesystemImpl).inSingletonScope(); -} +import { ContainerModule, } from 'inversify'; +import { FileServiceContribution } from '@theia/filesystem/lib/browser/file-service'; +import { UserStorageContribution } from './user-storage-contribution'; export default new ContainerModule(bind => { - bindUserStorage(bind); + bind(UserStorageContribution).toSelf().inSingletonScope(); + bind(FileServiceContribution).toService(UserStorageContribution); }); diff --git a/packages/userstorage/src/browser/user-storage-resource.ts b/packages/userstorage/src/browser/user-storage-resource.ts deleted file mode 100644 index b1d88cc543b5b..0000000000000 --- a/packages/userstorage/src/browser/user-storage-resource.ts +++ /dev/null @@ -1,74 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2017 Ericsson and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { injectable, inject } from 'inversify'; -import URI from '@theia/core/lib/common/uri'; -import { Resource, ResourceResolver, Emitter, Event, MaybePromise, DisposableCollection } from '@theia/core/lib/common'; -import { UserStorageService } from './user-storage-service'; -import { UserStorageUri } from './user-storage-uri'; - -export class UserStorageResource implements Resource { - - protected readonly onDidChangeContentsEmitter = new Emitter(); - protected readonly toDispose = new DisposableCollection(); - constructor( - public uri: URI, - protected readonly service: UserStorageService - ) { - this.toDispose.push(this.service.onUserStorageChanged(e => { - for (const changedUri of e.uris) { - if (changedUri.toString() === this.uri.toString()) { - this.onDidChangeContentsEmitter.fire(undefined); - } - } - })); - - this.toDispose.push(this.onDidChangeContentsEmitter); - } - - dispose(): void { - this.toDispose.dispose(); - } - - readContents(options?: { encoding?: string }): Promise { - return this.service.readContents(this.uri); - } - - saveContents(content: string): Promise { - return this.service.saveContents(this.uri, content); - } - - get onDidChangeContents(): Event { - return this.onDidChangeContentsEmitter.event; - } -} - -@injectable() -export class UserStorageResolver implements ResourceResolver { - - constructor( - @inject(UserStorageService) protected readonly service: UserStorageService - - ) { } - - resolve(uri: URI): MaybePromise { - if (uri.scheme !== UserStorageUri.SCHEME || !uri.path.isAbsolute) { - throw new Error('The given uri is not a user storage uri: ' + uri); - } - return new UserStorageResource(uri, this.service); - } - -} diff --git a/packages/userstorage/src/browser/user-storage-service-filesystem.spec.ts b/packages/userstorage/src/browser/user-storage-service-filesystem.spec.ts deleted file mode 100644 index c49ef86e74254..0000000000000 --- a/packages/userstorage/src/browser/user-storage-service-filesystem.spec.ts +++ /dev/null @@ -1,235 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2017 Ericsson and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { Container } from 'inversify'; -import * as chai from 'chai'; -import * as temp from 'temp'; -import { UserStorageServiceFilesystemImpl } from './user-storage-service-filesystem'; -import { UserStorageService } from './user-storage-service'; -import { UserStorageResource } from './user-storage-resource'; -import { Emitter, } from '@theia/core/lib/common'; -import { ILogger } from '@theia/core/lib/common/logger'; -import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; -import { FileSystem, FileStat, FileShouldOverwrite } from '@theia/filesystem/lib/common/'; -import { FileSystemPreferences, createFileSystemPreferences } from '@theia/filesystem/lib/browser/filesystem-preferences'; -import { FileSystemWatcher, FileChange, FileChangeType } from '@theia/filesystem/lib/browser/filesystem-watcher'; -import { PreferenceService } from '@theia/core/lib/browser/preferences'; -import { MockPreferenceService } from '@theia/core/lib/browser/preferences/test/mock-preference-service'; -import { FileSystemWatcherServer } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; -import { MockFilesystem, MockFilesystemWatcherServer } from '@theia/filesystem/lib/common/test'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; -import { MockEnvVariablesServerImpl } from '@theia/core/lib/browser/test/mock-env-variables-server'; -import { UserStorageUri } from './user-storage-uri'; -import URI from '@theia/core/lib/common/uri'; -import { FileUri } from '@theia/core/lib/node'; - -import * as sinon from 'sinon'; - -const expect = chai.expect; -let testContainer: Container; - -let userStorageService: UserStorageServiceFilesystemImpl; - -const track = temp.track(); -const userStorageFolder = FileUri.create(track.mkdirSync()); -const envVariableServer = new MockEnvVariablesServerImpl(userStorageFolder); - -const mockOnFileChangedEmitter = new Emitter(); -let files: { [key: string]: string; } = {}; - -before(async () => { - testContainer = new Container(); - - /* Preference bindings*/ - testContainer.bind(MockPreferenceService).toSelf().inSingletonScope(); - testContainer.bind(PreferenceService).to(MockPreferenceService).inSingletonScope(); - testContainer.bind(FileSystemPreferences).toDynamicValue(ctx => { - const preferences = ctx.container.get(PreferenceService); - sinon.stub(preferences, 'get').returns({ - 'files.watcherExclude': { - '**/.git/objects/**': true, - '**/.git/subtree-cache/**': true, - '**/node_modules/**': true - } - }); - return createFileSystemPreferences(preferences); - }).inSingletonScope(); - - /* FS mocks and bindings */ - testContainer.bind(FileSystemWatcherServer).to(MockFilesystemWatcherServer).inSingletonScope(); - testContainer.bind(FileSystemWatcher).toSelf().inSingletonScope().onActivation((_, watcher) => { - sinon.stub(watcher, 'onFilesChanged').get(() => - mockOnFileChangedEmitter.event - ); - return watcher; - }); - testContainer.bind(FileShouldOverwrite).toFunction( - async (originalStat: FileStat, currentStat: FileStat): Promise => true); - - /* Mock logger binding*/ - testContainer.bind(ILogger).to(MockLogger); - /* Stub getCurrentUserHome to return test home directory */ - testContainer.bind(FileSystem).toDynamicValue(ctx => { - const fs = new MockFilesystem(); - - sinon.stub(fs, 'resolveContent').callsFake((uri): Promise<{ stat: FileStat, content: string }> => { - const content = files[uri]; - return Promise.resolve( - { stat: { uri: uri, lastModification: 0, isDirectory: false }, content: content } - ); - }); - - sinon.stub(fs, 'setContent').callsFake((filestat, content: string) => { - files[filestat.uri] = content; - return Promise.resolve(content); - }); - - sinon.stub(fs, 'getFileStat').callsFake(uri => - Promise.resolve({ uri, lastModification: 0, isDirectory: false }) - ); - - return fs; - }).inSingletonScope(); - testContainer.bind(EnvVariablesServer).toConstantValue(envVariableServer); - testContainer.bind(UserStorageService).to(UserStorageServiceFilesystemImpl); -}); - -after(() => { - track.cleanupSync(); -}); - -describe('User Storage Service (Filesystem implementation)', () => { - let testFile: string; - before(() => { - testFile = 'test.json'; - userStorageService = testContainer.get(UserStorageService); - }); - - after(() => { - userStorageService.dispose(); - }); - - beforeEach(() => { - files = {}; - }); - - it('Should return a user storage uri from a filesystem uri', () => { - - const test = UserStorageServiceFilesystemImpl.toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile))!; - expect(test.scheme).eq(UserStorageUri.SCHEME); - expect(test.toString()).eq(UserStorageUri.SCHEME + ':/' + testFile); - - const testFragment = UserStorageServiceFilesystemImpl. - toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile).withFragment('test'))!; - expect(testFragment.fragment).eq('test'); - - const testQuery = UserStorageServiceFilesystemImpl. - toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile).withQuery('test=1'))!; - expect(testQuery.query).eq('test=1'); - - const testQueryAndFragment = UserStorageServiceFilesystemImpl. - toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile).withQuery('test=1').withFragment('test'))!; - expect(testQueryAndFragment.fragment).eq('test'); - expect(testQueryAndFragment.query).eq('test=1'); - }); - - it('Should return a filesystem uri from a user storage uri', () => { - const test = UserStorageServiceFilesystemImpl.toFilesystemURI(userStorageFolder, new URI(UserStorageUri.SCHEME + ':' + testFile)); - - expect(test.scheme).eq('file'); - expect(test.path.toString()).eq(userStorageFolder.resolve(testFile).path.toString()); - }); - - it('Should register a client and notifies it of the fs changes by converting them to user storage changes', done => { - userStorageService.onUserStorageChanged(event => { - const userStorageUri = event.uris[0]; - expect(userStorageUri.scheme).eq(UserStorageUri.SCHEME); - expect(userStorageUri.path.toString()).eq('/' + testFile); - done(); - }); - - mockOnFileChangedEmitter.fire([ - { - type: FileChangeType.UPDATED, - uri: userStorageFolder.resolve(testFile) - } - ]); - - }).timeout(2000); - - it('Should save the contents correctly using a user storage uri to a filesystem uri', async () => { - - const userStorageUri = UserStorageServiceFilesystemImpl. - toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile))!; - - await userStorageService.saveContents(userStorageUri, 'test content'); - - const newContent = await userStorageService.readContents(userStorageUri); - - expect(newContent).eq('test content'); - // Confirm that the URI was transformed to a filesystem uri by accessing it via the fs index - const fsUri = UserStorageServiceFilesystemImpl.toFilesystemURI(userStorageFolder, userStorageUri); - expect(files[fsUri.toString()]).eq('test content'); - - }).timeout(2000); - -}); - -describe('User Storage Resource (Filesystem implementation)', () => { - let userStorageResource: UserStorageResource; - let testFile: string; - - before(() => { - testFile = 'test.json'; - userStorageService = testContainer.get(UserStorageService); - const userStorageUriTest = UserStorageServiceFilesystemImpl. - toUserStorageUri(userStorageFolder, userStorageFolder.resolve(testFile))!; - userStorageResource = new UserStorageResource(userStorageUriTest, userStorageService); - }); - - after(() => { - userStorageService.dispose(); - }); - - beforeEach(() => { - files = {}; - }); - - it('Should return notify client when resource changed underneath', done => { - userStorageResource.onDidChangeContents(() => { - done(); - }); - - mockOnFileChangedEmitter.fire([ - { - type: FileChangeType.UPDATED, - uri: userStorageFolder.resolve(testFile) - } - ]); - }).timeout(2000); - - it('Should save and read correctly to fs', async () => { - const testContent = 'test content'; - await userStorageResource.saveContents(testContent); - const testFsUri = UserStorageServiceFilesystemImpl.toFilesystemURI(userStorageFolder, userStorageResource.uri); - - expect(files[testFsUri.toString()]).eq(testContent); - - const readTestContent = await userStorageResource.readContents(); - expect(readTestContent).eq(testContent); - - }).timeout(2000); -}); diff --git a/packages/userstorage/src/browser/user-storage-service-filesystem.ts b/packages/userstorage/src/browser/user-storage-service-filesystem.ts deleted file mode 100644 index 2137cff52b1e4..0000000000000 --- a/packages/userstorage/src/browser/user-storage-service-filesystem.ts +++ /dev/null @@ -1,127 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2017 Ericsson and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { DisposableCollection, ILogger, Emitter, Event } from '@theia/core/lib/common'; -import { UserStorageChangeEvent, UserStorageService } from './user-storage-service'; -import { injectable, inject } from 'inversify'; -import { FileSystemWatcher, FileChangeEvent } from '@theia/filesystem/lib/browser/filesystem-watcher'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; -import { FileSystem } from '@theia/filesystem/lib/common'; -import URI from '@theia/core/lib/common/uri'; -import { UserStorageUri } from './user-storage-uri'; - -@injectable() -export class UserStorageServiceFilesystemImpl implements UserStorageService { - - protected readonly toDispose = new DisposableCollection(); - protected readonly onUserStorageChangedEmitter = new Emitter(); - protected readonly userStorageFolder: Promise; - - constructor( - @inject(FileSystem) protected readonly fileSystem: FileSystem, - @inject(FileSystemWatcher) protected readonly watcher: FileSystemWatcher, - @inject(ILogger) protected readonly logger: ILogger, - @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer - - ) { - this.userStorageFolder = this.envServer.getConfigDirUri().then(configDirUri => { - const userDataFolderUri = new URI(configDirUri); - watcher.watchFileChanges(userDataFolderUri).then(disposable => - this.toDispose.push(disposable) - ); - this.toDispose.push(this.watcher.onFilesChanged(changes => this.onDidFilesChanged(changes))); - return userDataFolderUri; - }); - - this.toDispose.push(this.onUserStorageChangedEmitter); - - } - - dispose(): void { - this.toDispose.dispose(); - } - - protected onDidFilesChanged(event: FileChangeEvent): void { - const uris: URI[] = []; - this.userStorageFolder.then(folder => { - if (folder) { - for (const change of event) { - const userStorageUri = UserStorageServiceFilesystemImpl.toUserStorageUri(folder, change.uri); - if (userStorageUri) { - uris.push(userStorageUri); - } - } - if (uris.length > 0) { - this.onUserStorageChangedEmitter.fire({ uris }); - } - } - }); - } - - async readContents(uri: URI): Promise { - const folderUri = await this.userStorageFolder; - if (folderUri) { - const filesystemUri = UserStorageServiceFilesystemImpl.toFilesystemURI(folderUri, uri); - const exists = await this.fileSystem.exists(filesystemUri.toString()); - - if (exists) { - return this.fileSystem.resolveContent(filesystemUri.toString()).then(({ stat, content }) => content); - } - } - return ''; - } - - async saveContents(uri: URI, content: string): Promise { - const folderUri = await this.userStorageFolder; - if (!folderUri) { - return; - } - const filesystemUri = UserStorageServiceFilesystemImpl.toFilesystemURI(folderUri, uri); - - const fileStat = await this.fileSystem.getFileStat(filesystemUri.toString()); - if (fileStat) { - this.fileSystem.setContent(fileStat, content).then(() => Promise.resolve()); - } else { - this.fileSystem.createFile(filesystemUri.toString(), { content }); - } - } - - get onUserStorageChanged(): Event { - return this.onUserStorageChangedEmitter.event; - } - - /** - * Creates a new user storage URI from the filesystem URI. - * @param userStorageFolderUri User storage folder URI - * @param fsPath The filesystem URI - */ - public static toUserStorageUri(userStorageFolderUri: URI, rawUri: URI): URI | undefined { - const relativePath = userStorageFolderUri.relative(rawUri); - if (relativePath) { - return rawUri.withScheme(UserStorageUri.SCHEME).withPath('/' + relativePath); - } - return undefined; - } - - /** - * Returns the associated filesystem URI relative to the user storage folder passed as argument. - * @param userStorageFolderUri User storage folder URI - * @param userStorageUri User storage URI to be converted in filesystem URI - */ - public static toFilesystemURI(userStorageFolderUri: URI, userStorageUri: URI): URI { - return userStorageFolderUri.resolve(userStorageUri.path).normalizePath(); - } -} diff --git a/packages/userstorage/src/browser/user-storage-service.ts b/packages/userstorage/src/browser/user-storage-service.ts deleted file mode 100644 index 0e3528d8cd096..0000000000000 --- a/packages/userstorage/src/browser/user-storage-service.ts +++ /dev/null @@ -1,31 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2017 Ericsson and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import { Event, Disposable } from '@theia/core/lib/common'; -import URI from '@theia/core/lib/common/uri'; - -export const UserStorageService = Symbol('UserStorageService'); - -export interface UserStorageService extends Disposable { - readContents(uri: URI): Promise; - - saveContents(uri: URI, content: string): Promise; - - onUserStorageChanged: Event; -} - -export interface UserStorageChangeEvent { - uris: URI[]; -} diff --git a/packages/filesystem/src/common/test/index.ts b/packages/userstorage/src/package.spec.ts similarity index 62% rename from packages/filesystem/src/common/test/index.ts rename to packages/userstorage/src/package.spec.ts index d193effd48562..2dbe8d2fd7e5d 100644 --- a/packages/filesystem/src/common/test/index.ts +++ b/packages/userstorage/src/package.spec.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2017 Ericsson and others. + * Copyright (C) 2020 TypeFox and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -14,5 +14,16 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export * from './mock-filesystem-watcher-server'; -export * from './mock-filesystem'; +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('userstorage package', () => { + + it('support code coverage statistics', () => true); + +}); diff --git a/packages/workspace/src/browser/diff-service.ts b/packages/workspace/src/browser/diff-service.ts index c9046ff1b42b8..92b25e7e089be 100644 --- a/packages/workspace/src/browser/diff-service.ts +++ b/packages/workspace/src/browser/diff-service.ts @@ -17,28 +17,23 @@ import { inject, injectable } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; -import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; import { open, OpenerService, OpenerOptions } from '@theia/core/lib/browser'; import { MessageService } from '@theia/core/lib/common/message-service'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; @injectable() export class DiffService { - @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(FileService) protected readonly fileService: FileService; @inject(OpenerService) protected readonly openerService: OpenerService; @inject(MessageService) protected readonly messageService: MessageService; public async openDiffEditor(left: URI, right: URI, label?: string, options?: OpenerOptions): Promise { if (left.scheme === 'file' && right.scheme === 'file') { - const [leftExists, rightExists] = await Promise.all([ - this.fileSystem.exists(left.toString()), - this.fileSystem.exists(right.toString()) - ]); - if (leftExists && rightExists) { - const [leftStat, rightStat] = await Promise.all([ - this.fileSystem.getFileStat(left.toString()), - this.fileSystem.getFileStat(right.toString()), - ]); + const [resolvedLeft, resolvedRight] = await this.fileService.resolveAll([{ resource: left }, { resource: right }]); + if (resolvedLeft.success && resolvedRight.success) { + const leftStat = resolvedLeft.stat; + const rightStat = resolvedRight.stat; if (leftStat && rightStat) { if (!leftStat.isDirectory && !rightStat.isDirectory) { const uri = DiffUris.encode(left, right, label); diff --git a/packages/workspace/src/browser/quick-open-workspace.ts b/packages/workspace/src/browser/quick-open-workspace.ts index 485b6bcd4a054..6f3c2186d851f 100644 --- a/packages/workspace/src/browser/quick-open-workspace.ts +++ b/packages/workspace/src/browser/quick-open-workspace.ts @@ -18,11 +18,12 @@ import { injectable, inject } from 'inversify'; import { QuickOpenService, QuickOpenModel, QuickOpenItem, QuickOpenGroupItem, QuickOpenMode, LabelProvider } from '@theia/core/lib/browser'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { WorkspaceService } from './workspace-service'; -import { getTemporaryWorkspaceFileUri } from '../common'; import { WorkspacePreferences } from './workspace-preferences'; import URI from '@theia/core/lib/common/uri'; -import { FileSystem, FileSystemUtils } from '@theia/filesystem/lib/common'; import * as moment from 'moment'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat } from '@theia/filesystem/lib/common/files'; +import { FileSystemUtils } from '@theia/filesystem/lib/common'; @injectable() export class QuickOpenWorkspace implements QuickOpenModel { @@ -32,7 +33,7 @@ export class QuickOpenWorkspace implements QuickOpenModel { @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; - @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(FileService) protected readonly fileService: FileService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @inject(WorkspacePreferences) protected preferences: WorkspacePreferences; @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer; @@ -40,10 +41,10 @@ export class QuickOpenWorkspace implements QuickOpenModel { async open(workspaces: string[]): Promise { this.items = []; const [homeDirUri, tempWorkspaceFile] = await Promise.all([ - this.fileSystem.getCurrentUserHome(), - getTemporaryWorkspaceFileUri(this.envServer) + this.envServer.getHomeDirUri(), + this.workspaceService.getUntitledWorkspace() ]); - const home = homeDirUri ? await this.fileSystem.getFsPath(homeDirUri.uri) : undefined; + const home = new URI(homeDirUri).path.toString(); await this.preferences.ready; if (!workspaces.length) { this.items.push(new QuickOpenGroupItem({ @@ -53,7 +54,10 @@ export class QuickOpenWorkspace implements QuickOpenModel { } for (const workspace of workspaces) { const uri = new URI(workspace); - const stat = await this.fileSystem.getFileStat(workspace); + let stat: FileStat | undefined; + try { + stat = await this.fileService.resolve(uri); + } catch { } if (!stat || !this.preferences['workspace.supportMultiRootWorkspace'] && !stat.isDirectory) { continue; // skip the workspace files if multi root is not supported @@ -65,8 +69,8 @@ export class QuickOpenWorkspace implements QuickOpenModel { const iconClass = icon === '' ? undefined : icon + ' file-icon'; this.items.push(new QuickOpenGroupItem({ label: uri.path.base, - description: (home) ? FileSystemUtils.tildifyPath(uri.path.toString(), home) : uri.path.toString(), - groupLabel: `last modified ${moment(stat.lastModification).fromNow()}`, + description: FileSystemUtils.tildifyPath(uri.path.toString(), home), + groupLabel: `last modified ${moment(stat.mtime).fromNow()}`, iconClass, run: (mode: QuickOpenMode): boolean => { if (mode !== QuickOpenMode.OPEN) { @@ -74,7 +78,7 @@ export class QuickOpenWorkspace implements QuickOpenModel { } const current = this.workspaceService.workspace; const uriToOpen = new URI(workspace); - if ((current && current.uri !== workspace) || !current) { + if ((current && current.resource.toString() !== workspace) || !current) { this.workspaceService.open(uriToOpen); } return true; diff --git a/packages/workspace/src/browser/workspace-commands.ts b/packages/workspace/src/browser/workspace-commands.ts index d211d0d5b7c11..c659697c384be 100644 --- a/packages/workspace/src/browser/workspace-commands.ts +++ b/packages/workspace/src/browser/workspace-commands.ts @@ -20,7 +20,6 @@ import { SelectionService } from '@theia/core/lib/common/selection-service'; import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; import { MenuContribution, MenuModelRegistry } from '@theia/core/lib/common/menu'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common/filesystem'; import { FileDialogService } from '@theia/filesystem/lib/browser'; import { SingleTextInputDialog, ConfirmDialog } from '@theia/core/lib/browser/dialogs'; import { OpenerService, OpenHandler, open, FrontendApplication, LabelProvider } from '@theia/core/lib/browser'; @@ -36,6 +35,8 @@ import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/fil import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution'; import { WorkspaceInputDialog } from './workspace-input-dialog'; import { Emitter, Event } from '@theia/core/lib/common'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat } from '@theia/filesystem/lib/common/files'; const validFilename: (arg: string) => boolean = require('valid-filename'); @@ -179,7 +180,7 @@ export interface DidCreateNewResourceEvent { export class WorkspaceCommandContribution implements CommandContribution { @inject(LabelProvider) protected readonly labelProvider: LabelProvider; - @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(FileService) protected readonly fileService: FileService; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(SelectionService) protected readonly selectionService: SelectionService; @inject(OpenerService) protected readonly openerService: OpenerService; @@ -224,7 +225,7 @@ export class WorkspaceCommandContribution implements CommandContribution { registry.registerCommand(WorkspaceCommands.NEW_FILE, this.newWorkspaceRootUriAwareCommandHandler({ execute: uri => this.getDirectory(uri).then(parent => { if (parent) { - const parentUri = new URI(parent.uri); + const parentUri = parent.resource; const { fileName, fileExtension } = this.getDefaultFileConfig(); const vacantChildUri = FileSystemUtils.generateUniqueResourceURI(parentUri, parent, fileName, fileExtension); @@ -238,7 +239,7 @@ export class WorkspaceCommandContribution implements CommandContribution { dialog.open().then(async name => { if (name) { const fileUri = parentUri.resolve(name); - await this.fileSystem.createFile(fileUri.toString()); + await this.fileService.create(fileUri); this.fireCreateNewFile({ parent: parentUri, uri: fileUri }); open(this.openerService, fileUri); } @@ -249,7 +250,7 @@ export class WorkspaceCommandContribution implements CommandContribution { registry.registerCommand(WorkspaceCommands.NEW_FOLDER, this.newWorkspaceRootUriAwareCommandHandler({ execute: uri => this.getDirectory(uri).then(parent => { if (parent) { - const parentUri = new URI(parent.uri); + const parentUri = parent.resource; const vacantChildUri = FileSystemUtils.generateUniqueResourceURI(parentUri, parent, 'Untitled'); const dialog = new WorkspaceInputDialog({ title: 'New Folder', @@ -260,7 +261,7 @@ export class WorkspaceCommandContribution implements CommandContribution { dialog.open().then(async name => { if (name) { const folderUri = parentUri.resolve(name); - await this.fileSystem.createFolder(folderUri.toString()); + await this.fileService.createFolder(folderUri); this.fireCreateNewFile({ parent: parentUri, uri: folderUri }); } }); @@ -275,10 +276,7 @@ export class WorkspaceCommandContribution implements CommandContribution { const parent = await this.getParent(uri); if (parent) { const initialValue = uri.path.base; - const stat = await this.fileSystem.getFileStat(uri.toString()); - if (stat === undefined) { - throw new Error(`Unexpected error occurred when renaming. File does not exist. URI: ${uri.toString(true)}.`); - } + const stat = await this.fileService.resolve(uri); const fileType = stat.isDirectory ? 'Directory' : 'File'; const titleStr = `Rename ${fileType}`; const dialog = new SingleTextInputDialog({ @@ -299,7 +297,7 @@ export class WorkspaceCommandContribution implements CommandContribution { if (fileName) { const oldUri = uri; const newUri = uri.parent.resolve(fileName); - this.fileSystem.move(oldUri.toString(), newUri.toString()); + this.fileService.move(oldUri, newUri); } } }); @@ -380,8 +378,8 @@ export class WorkspaceCommandContribution implements CommandContribution { if (name.split(/[\\/]/).some(file => !file || !validFilename(file) || /^\s+$/.test(file))) { return `The name "${this.trimFileName(name)}" is not a valid file or folder name.`; } - const childUri = new URI(parent.uri).resolve(name).toString(); - const exists = await this.fileSystem.exists(childUri); + const childUri = parent.resource.resolve(name); + const exists = await this.fileService.exists(childUri); if (exists) { return `A file or folder "${this.trimFileName(name)}" already exists at this location.`; } @@ -396,23 +394,32 @@ export class WorkspaceCommandContribution implements CommandContribution { } protected async getDirectory(candidate: URI): Promise { - const stat = await this.fileSystem.getFileStat(candidate.toString()); + let stat: FileStat | undefined; + try { + stat = await this.fileService.resolve(candidate); + } catch { } if (stat && stat.isDirectory) { return stat; } return this.getParent(candidate); } - protected getParent(candidate: URI): Promise { - return this.fileSystem.getFileStat(candidate.parent.toString()); + protected async getParent(candidate: URI): Promise { + try { + return await this.fileService.resolve(candidate.parent); + } catch { + return undefined; + } } protected async addFolderToWorkspace(uri: URI | undefined): Promise { if (uri) { - const stat = await this.fileSystem.getFileStat(uri.toString()); - if (stat && stat.isDirectory) { - await this.workspaceService.addRoot(uri); - } + try { + const stat = await this.fileService.resolve(uri); + if (stat.isDirectory) { + await this.workspaceService.addRoot(uri); + } + } catch { } } } @@ -421,7 +428,7 @@ export class WorkspaceCommandContribution implements CommandContribution { } protected isWorkspaceRoot(uri: URI): boolean { - const rootUris = new Set(this.workspaceService.tryGetRoots().map(root => root.uri)); + const rootUris = new Set(this.workspaceService.tryGetRoots().map(root => root.resource.toString())); return rootUris.has(uri.toString()); } @@ -437,7 +444,7 @@ export class WorkspaceCommandContribution implements CommandContribution { * @param uris the list of folder uris to remove. */ protected async removeFolderFromWorkspace(uris: URI[]): Promise { - const roots = new Set(this.workspaceService.tryGetRoots().map(root => root.uri)); + const roots = new Set(this.workspaceService.tryGetRoots().map(root => root.resource.toString())); const toRemove = uris.filter(uri => roots.has(uri.toString())); if (toRemove.length > 0) { const messageContainer = document.createElement('div'); @@ -454,7 +461,7 @@ export class WorkspaceCommandContribution implements CommandContribution { listItem.title = this.labelProvider.getLongName(uri); const listContent = document.createElement('span'); listContent.classList.add('theia-dialog-node-segment'); - listContent.appendChild(document.createTextNode(uri.displayName)); + listContent.appendChild(document.createTextNode(this.labelProvider.getName(uri))); listItem.appendChild(listContent); list.appendChild(listItem); }); @@ -512,7 +519,7 @@ export class WorkspaceRootUriAwareCommandHandler extends UriAwareCommandHandler< } // Return the first root if available. if (!!this.workspaceService.tryGetRoots().length) { - return new URI(this.workspaceService.tryGetRoots()[0].uri); + return this.workspaceService.tryGetRoots()[0].resource; } } diff --git a/packages/workspace/src/browser/workspace-delete-handler.ts b/packages/workspace/src/browser/workspace-delete-handler.ts index 429133d90dc66..7e23ecc05c7eb 100644 --- a/packages/workspace/src/browser/workspace-delete-handler.ts +++ b/packages/workspace/src/browser/workspace-delete-handler.ts @@ -17,16 +17,18 @@ import { injectable, inject } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { ConfirmDialog, ApplicationShell, SaveableWidget, NavigatableWidget } from '@theia/core/lib/browser'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { UriCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import { WorkspaceService } from './workspace-service'; import { WorkspaceUtils } from './workspace-utils'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileSystemPreferences } from '@theia/filesystem/lib/browser/filesystem-preferences'; +import { FileDeleteOptions, FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files'; @injectable() export class WorkspaceDeleteHandler implements UriCommandHandler { - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; @inject(ApplicationShell) protected readonly shell: ApplicationShell; @@ -37,6 +39,9 @@ export class WorkspaceDeleteHandler implements UriCommandHandler { @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(FileSystemPreferences) + protected readonly fsPreferences: FileSystemPreferences; + /** * Determine if the command is visible. * @@ -64,8 +69,12 @@ export class WorkspaceDeleteHandler implements UriCommandHandler { */ async execute(uris: URI[]): Promise { const distinctUris = URI.getDistinctParents(uris); - if (await this.confirm(distinctUris)) { - await Promise.all(distinctUris.map(uri => this.delete(uri))); + const resolved: FileDeleteOptions = { + recursive: true, + useTrash: this.fsPreferences['files.enableTrash'] && distinctUris[0] && this.fileService.hasCapability(distinctUris[0], FileSystemProviderCapabilities.Trash) + }; + if (await this.confirm(distinctUris, resolved)) { + await Promise.all(distinctUris.map(uri => this.delete(uri, resolved))); } } @@ -74,9 +83,15 @@ export class WorkspaceDeleteHandler implements UriCommandHandler { * * @param uris URIs of selected resources. */ - protected confirm(uris: URI[]): Promise { + protected confirm(uris: URI[], options: FileDeleteOptions): Promise { + let title = `File${uris.length === 1 ? '' : 's'}`; + if (options.useTrash) { + title = 'Move ' + title + ' to Trash'; + } else { + title = 'Delete ' + title; + } return new ConfirmDialog({ - title: `Delete File${uris.length === 1 ? '' : 's'}`, + title, msg: this.getConfirmMessage(uris) }).open(); } @@ -133,11 +148,11 @@ export class WorkspaceDeleteHandler implements UriCommandHandler { * * @param uri URI of selected resource. */ - protected async delete(uri: URI): Promise { + protected async delete(uri: URI, options: FileDeleteOptions): Promise { try { await Promise.all([ this.closeWithoutSaving(uri), - this.fileSystem.delete(uri.toString()) + this.fileService.delete(uri, options) ]); } catch (e) { console.error(e); diff --git a/packages/workspace/src/browser/workspace-duplicate-handler.ts b/packages/workspace/src/browser/workspace-duplicate-handler.ts index c7c2c7f987156..dca34070e50fc 100644 --- a/packages/workspace/src/browser/workspace-duplicate-handler.ts +++ b/packages/workspace/src/browser/workspace-duplicate-handler.ts @@ -18,15 +18,15 @@ import URI from '@theia/core/lib/common/uri'; import { injectable, inject } from 'inversify'; import { WorkspaceUtils } from './workspace-utils'; import { WorkspaceService } from './workspace-service'; -import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; import { UriCommandHandler } from '@theia/core/lib/common/uri-command-handler'; import { FileSystemUtils } from '@theia/filesystem/lib/common/filesystem-utils'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; @injectable() export class WorkspaceDuplicateHandler implements UriCommandHandler { - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; @inject(WorkspaceUtils) protected readonly workspaceUtils: WorkspaceUtils; @@ -61,17 +61,15 @@ export class WorkspaceDuplicateHandler implements UriCommandHandler { */ async execute(uris: URI[]): Promise { await Promise.all(uris.map(async uri => { - const parent = await this.fileSystem.getFileStat(uri.parent.toString()); - if (parent) { - const parentUri = new URI(parent.uri); + try { + const parent = await this.fileService.resolve(uri.parent); + const parentUri = parent.resource; const name = uri.path.name + '_copy'; const ext = uri.path.ext; const target = FileSystemUtils.generateUniqueResourceURI(parentUri, parent, name, ext); - try { - this.fileSystem.copy(uri.toString(), target.toString()); - } catch (e) { - console.error(e); - } + await this.fileService.copy(uri, target); + } catch (e) { + console.error(e); } })); } diff --git a/packages/workspace/src/browser/workspace-frontend-contribution.ts b/packages/workspace/src/browser/workspace-frontend-contribution.ts index a7fb1c60b18c5..f80edc33d3a91 100644 --- a/packages/workspace/src/browser/workspace-frontend-contribution.ts +++ b/packages/workspace/src/browser/workspace-frontend-contribution.ts @@ -14,15 +14,14 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject, postConstruct } from 'inversify'; +import { injectable, inject } from 'inversify'; import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, SelectionService } from '@theia/core/lib/common'; import { isOSX, environment, OS } from '@theia/core'; import { open, OpenerService, CommonMenus, StorageService, LabelProvider, - ConfirmDialog, KeybindingRegistry, KeybindingContribution, CommonCommands + ConfirmDialog, KeybindingRegistry, KeybindingContribution, CommonCommands, FrontendApplicationContribution } from '@theia/core/lib/browser'; import { FileDialogService, OpenFileDialogProps, FileDialogTreeFilters } from '@theia/filesystem/lib/browser'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { WorkspaceService } from './workspace-service'; import { THEIA_EXT, VSCODE_EXT } from '../common'; @@ -31,6 +30,11 @@ import { QuickOpenWorkspace } from './quick-open-workspace'; import { WorkspacePreferences } from './workspace-preferences'; import URI from '@theia/core/lib/common/uri'; import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { EncodingRegistry } from '@theia/core/lib/browser/encoding-registry'; +import { UTF8 } from '@theia/core/lib/common/encodings'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; export enum WorkspaceStates { /** @@ -49,9 +53,9 @@ export enum WorkspaceStates { export type WorkspaceState = keyof typeof WorkspaceStates; @injectable() -export class WorkspaceFrontendContribution implements CommandContribution, KeybindingContribution, MenuContribution { +export class WorkspaceFrontendContribution implements CommandContribution, KeybindingContribution, MenuContribution, FrontendApplicationContribution { - @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(FileService) protected readonly fileService: FileService; @inject(OpenerService) protected readonly openerService: OpenerService; @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(StorageService) protected readonly workspaceStorage: StorageService; @@ -65,12 +69,17 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; - @postConstruct() - protected init(): void { - this.initWorkspaceContextKeys(); - } + @inject(EncodingRegistry) + protected readonly encodingRegistry: EncodingRegistry; + + @inject(PreferenceConfigurations) + protected readonly preferenceConfigurations: PreferenceConfigurations; + + configure(): void { + this.encodingRegistry.registerOverride({ encoding: UTF8, extension: THEIA_EXT }); + this.encodingRegistry.registerOverride({ encoding: UTF8, extension: VSCODE_EXT }); + this.updateEncodingOverrides(); - protected initWorkspaceContextKeys(): void { const workspaceFolderCountKey = this.contextKeyService.createKey('workspaceFolderCount', 0); const updateWorkspaceFolderCountKey = () => workspaceFolderCountKey.set(this.workspaceService.tryGetRoots().length); updateWorkspaceFolderCountKey(); @@ -81,12 +90,24 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi this.updateStyles(); this.workspaceService.onWorkspaceChanged(() => { + this.updateEncodingOverrides(); updateWorkspaceFolderCountKey(); updateWorkspaceStateKey(); this.updateStyles(); }); } + protected readonly toDisposeOnUpdateEncodingOverrides = new DisposableCollection(); + protected updateEncodingOverrides(): void { + this.toDisposeOnUpdateEncodingOverrides.dispose(); + for (const root of this.workspaceService.tryGetRoots()) { + for (const configPath of this.preferenceConfigurations.getPaths()) { + const parent = root.resource.resolve(configPath); + this.toDisposeOnUpdateEncodingOverrides.push(this.encodingRegistry.registerOverride({ encoding: UTF8, parent })); + } + } + } + protected updateStyles(): void { document.body.classList.remove('theia-no-open-workspace'); // Display the 'no workspace opened' theme color when no folders are opened (single-root). @@ -218,16 +239,14 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi canSelectFolders: true, canSelectFiles: true }, rootStat); - if (destinationUri && this.getCurrentWorkspaceUri().toString() !== destinationUri.toString()) { - const destination = await this.fileSystem.getFileStat(destinationUri.toString()); - if (destination) { - if (destination.isDirectory) { - this.workspaceService.open(destinationUri); - } else { - await open(this.openerService, destinationUri); - } - return destinationUri; + if (destinationUri && this.getCurrentWorkspaceUri()?.toString() !== destinationUri.toString()) { + const destination = await this.fileService.resolve(destinationUri); + if (destination.isDirectory) { + this.workspaceService.open(destinationUri); + } else { + await open(this.openerService, destinationUri); } + return destinationUri; } return undefined; } @@ -249,8 +268,8 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi const [rootStat] = await this.workspaceService.roots; const destinationFileUri = await this.fileDialogService.showOpenDialog(props, rootStat); if (destinationFileUri) { - const destinationFile = await this.fileSystem.getFileStat(destinationFileUri.toString()); - if (destinationFile && !destinationFile.isDirectory) { + const destinationFile = await this.fileService.resolve(destinationFileUri); + if (!destinationFile.isDirectory) { await open(this.openerService, destinationFileUri); return destinationFileUri; } @@ -275,9 +294,9 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi const [rootStat] = await this.workspaceService.roots; const destinationFolderUri = await this.fileDialogService.showOpenDialog(props, rootStat); if (destinationFolderUri && - this.getCurrentWorkspaceUri().toString() !== destinationFolderUri.toString()) { - const destinationFolder = await this.fileSystem.getFileStat(destinationFolderUri.toString()); - if (destinationFolder && destinationFolder.isDirectory) { + this.getCurrentWorkspaceUri()?.toString() !== destinationFolderUri.toString()) { + const destinationFolder = await this.fileService.resolve(destinationFolderUri); + if (destinationFolder.isDirectory) { this.workspaceService.open(destinationFolderUri); return destinationFolderUri; } @@ -315,8 +334,8 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi const [rootStat] = await this.workspaceService.roots; const workspaceFolderOrWorkspaceFileUri = await this.fileDialogService.showOpenDialog(props, rootStat); if (workspaceFolderOrWorkspaceFileUri && - this.getCurrentWorkspaceUri().toString() !== workspaceFolderOrWorkspaceFileUri.toString()) { - const destinationFolder = await this.fileSystem.getFileStat(workspaceFolderOrWorkspaceFileUri.toString()); + this.getCurrentWorkspaceUri()?.toString() !== workspaceFolderOrWorkspaceFileUri.toString()) { + const destinationFolder = await this.fileService.exists(workspaceFolderOrWorkspaceFileUri); if (destinationFolder) { this.workspaceService.open(workspaceFolderOrWorkspaceFileUri); return workspaceFolderOrWorkspaceFileUri; @@ -361,7 +380,7 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi if (!displayName.endsWith(`.${THEIA_EXT}`) && !displayName.endsWith(`.${VSCODE_EXT}`)) { selected = selected.parent.resolve(`${displayName}.${THEIA_EXT}`); } - exist = await this.fileSystem.exists(selected.toString()); + exist = await this.fileService.exists(selected); if (exist) { overwrite = await this.confirmOverwrite(selected); } @@ -382,7 +401,7 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi let exist: boolean = false; let overwrite: boolean = false; let selected: URI | undefined; - const stat = await this.fileSystem.getFileStat(uri.toString()); + const stat = await this.fileService.resolve(uri); do { selected = await this.fileDialogService.showSaveDialog( { @@ -391,7 +410,7 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi inputValue: uri.path.base }, stat); if (selected) { - exist = await this.fileSystem.exists(selected.toString()); + exist = await this.fileService.exists(selected); if (exist) { overwrite = await this.confirmOverwrite(selected); } @@ -400,7 +419,7 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi if (selected) { try { await this.commandRegistry.executeCommand(CommonCommands.SAVE.id); - await this.fileSystem.copy(uri.toString(), selected.toString(), { overwrite }); + await this.fileService.copy(uri, selected, { overwrite }); } catch (e) { console.warn(e); } @@ -436,8 +455,8 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi * * @returns the current workspace URI. */ - private getCurrentWorkspaceUri(): URI { - return new URI(this.workspaceService.workspace && this.workspaceService.workspace.uri); + private getCurrentWorkspaceUri(): URI | undefined { + return this.workspaceService.workspace?.resource; } } diff --git a/packages/workspace/src/browser/workspace-service.spec.ts b/packages/workspace/src/browser/workspace-service.spec.ts deleted file mode 100644 index e6e56ea2fda04..0000000000000 --- a/packages/workspace/src/browser/workspace-service.spec.ts +++ /dev/null @@ -1,935 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Ericsson and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; -let disableJSDOM = enableJSDOM(); - -import { Container } from 'inversify'; -import { WorkspaceService } from './workspace-service'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; -import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; -import { FileSystemNode } from '@theia/filesystem/lib/node/node-filesystem'; -import { FileSystemWatcher, FileChangeEvent, FileChangeType } from '@theia/filesystem/lib/browser/filesystem-watcher'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; -import { DefaultWindowService } from '@theia/core/lib/browser/window/default-window-service'; -import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; -import { MockEnvVariablesServerImpl } from '@theia/core/lib/browser/test/mock-env-variables-server'; -import { WorkspaceServer, THEIA_EXT, VSCODE_EXT } from '../common'; -import { DefaultWorkspaceServer } from '../node/default-workspace-server'; -import { Emitter, Disposable, DisposableCollection, ILogger, Logger } from '@theia/core'; -import { PreferenceServiceImpl, PreferenceSchemaProvider } from '@theia/core/lib/browser'; -import { WorkspacePreferences } from './workspace-preferences'; -import { createMockPreferenceProxy } from '@theia/core/lib/browser/preferences/test'; -import { MessageService } from '@theia/core/lib/common'; -import * as jsoncparser from 'jsonc-parser'; -import * as sinon from 'sinon'; -import * as chai from 'chai'; -import * as assert from 'assert'; -import * as temp from 'temp'; -import { FileUri } from '@theia/core/lib/node'; -import URI from '@theia/core/lib/common/uri'; -const expect = chai.expect; - -disableJSDOM(); -const track = temp.track(); - -const folderA = Object.freeze({ - uri: 'file:///home/folderA', - lastModification: 0, - isDirectory: true -}); -const folderB = Object.freeze({ - uri: 'file:///home/folderB', - lastModification: 0, - isDirectory: true -}); -const getFormattedJson = (data: string): string => { - const edits = jsoncparser.format(data, undefined, { tabSize: 3, insertSpaces: true, eol: '' }); - return jsoncparser.applyEdits(data, edits); -}; - -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable no-unused-expressions */ -describe('WorkspaceService', () => { - const toRestore: Array = []; - const toDispose: Disposable[] = []; - let wsService: WorkspaceService; - let updateTitleStub: sinon.SinonStub; - // stub of window.location.reload - let windowLocationReloadStub: sinon.SinonStub; - let onFilesChangedStub: sinon.SinonStub; - - let mockFileChangeEmitter: Emitter; - let mockPreferenceValues: { [p: string]: any }; - let mockFilesystem: FileSystem; - let mockFileSystemWatcher: FileSystemWatcher; - let mockWorkspaceServer: WorkspaceServer; - let mockWindowService: WindowService; - let mockILogger: ILogger; - let mockPref: WorkspacePreferences; - let mockPreferenceServiceImpl: PreferenceServiceImpl; - let mockPreferenceSchemaProvider: PreferenceSchemaProvider; - - before(() => { - disableJSDOM = enableJSDOM(); - FrontendApplicationConfigProvider.set({ - 'applicationName': 'test', - }); - }); - - after(() => { - disableJSDOM(); - track.cleanupSync(); - }); - - beforeEach(() => { - mockPreferenceValues = {}; - mockFilesystem = sinon.createStubInstance(FileSystemNode); - mockFileSystemWatcher = sinon.createStubInstance(FileSystemWatcher); - mockWorkspaceServer = sinon.createStubInstance(DefaultWorkspaceServer); - mockWindowService = sinon.createStubInstance(DefaultWindowService); - mockILogger = sinon.createStubInstance(Logger); - mockPref = createMockPreferenceProxy(mockPreferenceValues); - mockPreferenceServiceImpl = sinon.createStubInstance(PreferenceServiceImpl); - mockPreferenceSchemaProvider = sinon.createStubInstance(PreferenceSchemaProvider); - - const testContainer = new Container(); - testContainer.bind(WorkspaceService).toSelf().inSingletonScope(); - testContainer.bind(FileSystem).toConstantValue(mockFilesystem); - testContainer.bind(FileSystemWatcher).toConstantValue(mockFileSystemWatcher); - testContainer.bind(WorkspaceServer).toConstantValue(mockWorkspaceServer); - testContainer.bind(WindowService).toConstantValue(mockWindowService); - testContainer.bind(ILogger).toConstantValue(mockILogger); - testContainer.bind(WorkspacePreferences).toConstantValue(mockPref); - testContainer.bind(EnvVariablesServer).toConstantValue(new MockEnvVariablesServerImpl(FileUri.create(track.mkdirSync()))); - testContainer.bind(PreferenceServiceImpl).toConstantValue(mockPreferenceServiceImpl); - testContainer.bind(PreferenceSchemaProvider).toConstantValue(mockPreferenceSchemaProvider); - testContainer.bind(MessageService).toConstantValue({} as MessageService); - - // stub the updateTitle() & reloadWindow() function because `document` and `window` are unavailable - updateTitleStub = sinon.stub(WorkspaceService.prototype, 'updateTitle').callsFake(() => { }); - windowLocationReloadStub = sinon.stub(window.location, 'reload'); - mockFileChangeEmitter = new Emitter(); - onFilesChangedStub = sinon.stub(mockFileSystemWatcher, 'onFilesChanged').value(mockFileChangeEmitter.event); - toDispose.push(mockFileChangeEmitter); - toRestore.push(...[updateTitleStub, windowLocationReloadStub, onFilesChangedStub]); - - wsService = testContainer.get(WorkspaceService); - }); - - afterEach(() => { - wsService['toDisposeOnWorkspace'].dispose(); - toRestore.forEach(res => { - res.restore(); - }); - toRestore.length = 0; - toDispose.forEach(dis => dis.dispose()); - toDispose.length = 0; - }); - - describe('constructor and init', () => { - it('should reset the exposed roots and title if the most recently used workspace is unavailable', async () => { - (mockWorkspaceServer.getMostRecentlyUsedWorkspace).resolves(undefined); - - await wsService['init'](); - expect(wsService.workspace).to.to.be.undefined; - expect((await wsService.roots).length).to.eq(0); - expect(wsService.tryGetRoots().length).to.eq(0); - expect(updateTitleStub.called).to.be.true; - expect(window.location.hash).to.be.empty; - }); - - it('should reset the exposed roots and title if server returns an invalid or nonexistent file / folder', async () => { - const invalidStat = { - uri: 'file:///home/invalid', - lastModification: 0, - isDirectory: true - }; - (mockWorkspaceServer.getMostRecentlyUsedWorkspace).resolves(invalidStat.uri); - (mockFilesystem.getFileStat).resolves(undefined); - (mockFileSystemWatcher.watchFileChanges).resolves(new DisposableCollection()); - - await wsService['init'](); - expect(wsService.workspace).to.to.be.undefined; - expect((await wsService.roots).length).to.eq(0); - expect(wsService.tryGetRoots().length).to.eq(0); - expect(updateTitleStub.called).to.be.true; - expect(window.location.hash).to.be.empty; - }); - - ['/home/oneFolder', '/home/oneFolder/'].forEach(uriStr => { - it('should set the exposed roots and workspace to the folder returned by server as the most recently used workspace, and start watching that folder', async () => { - const stat = { - uri: 'file://' + uriStr, - lastModification: 0, - isDirectory: true - }; - (mockWorkspaceServer.getMostRecentlyUsedWorkspace).resolves(stat.uri); - (mockFilesystem.getFileStat).resolves(stat); - (mockFileSystemWatcher.watchFileChanges).resolves(new DisposableCollection()); - - await wsService['init'](); - expect(wsService.workspace).to.eq(stat); - expect((await wsService.roots).length).to.eq(1); - expect(wsService.tryGetRoots().length).to.eq(1); - expect(wsService.tryGetRoots()[0]).to.eq(stat); - expect((mockFileSystemWatcher.watchFileChanges).calledWith(new URI(stat.uri))).to.be.true; - expect(window.location.hash).eq('#' + uriStr); - }); - }); - - it( - 'should set the exposed roots and workspace to the folders listed in the workspace file returned by the server, ' + - 'and start watching the workspace file and all the folders', - async () => { - const workspaceFilePath = '/home/workspaceFile.theia-workspace'; - const workspaceFileUri = 'file://' + workspaceFilePath; - const workspaceFileStat = { - uri: workspaceFileUri, - lastModification: 0, - isDirectory: false - }; - const rootA = 'file:///folderA'; - const rootB = 'file:///folderB'; - (mockWorkspaceServer.getMostRecentlyUsedWorkspace).resolves(workspaceFileStat.uri); - const stubGetFileStat = (mockFilesystem.getFileStat); - stubGetFileStat.withArgs(workspaceFileUri).resolves(workspaceFileStat); - (mockFilesystem.exists).resolves(true); - (mockFilesystem.resolveContent).resolves({ - stat: workspaceFileStat, - content: `{"folders":[{"path":"${rootA}"},{"path":"${rootB}"}],"settings":{}}` - }); - stubGetFileStat.withArgs(rootA).resolves({ - uri: rootA, lastModification: 0, isDirectory: true - }); // rootA exists - stubGetFileStat.withArgs(rootB).throws(new Error()); // no access to rootB - (mockFileSystemWatcher.watchFileChanges).resolves(new DisposableCollection()); - - await wsService['init'](); - expect(wsService.workspace).to.eq(workspaceFileStat); - expect((await wsService.roots).length).to.eq(2); - expect(wsService.tryGetRoots().length).to.eq(2); - expect(wsService.tryGetRoots()[0].uri).to.eq(rootA); - expect(wsService.tryGetRoots()[1].uri).to.eq(rootB); - expect(window.location.hash).to.eq('#' + workspaceFilePath); - - expect((>wsService['rootWatchers']).size).to.eq(2); - expect((>wsService['rootWatchers']).has(rootA)).to.be.true; - expect((>wsService['rootWatchers']).has(rootB)).to.be.true; - }); - - it( - 'should resolve a relative workspace root path to a normalized root path', - async () => { - const workspaceFilePath = '/home/workspaceFile.theia-workspace'; - const workspaceFileUri = 'file://' + workspaceFilePath; - const workspaceFileStat = { - uri: workspaceFileUri, - lastModification: 0, - isDirectory: false - }; - const rootRelative = '../workspace'; - const rootActual = 'file:///workspace'; - (mockWorkspaceServer.getMostRecentlyUsedWorkspace).resolves(workspaceFileStat.uri); - const stubGetFileStat = (mockFilesystem.getFileStat); - stubGetFileStat.withArgs(workspaceFileUri).resolves(workspaceFileStat); - (mockFilesystem.exists).resolves(true); - (mockFilesystem.resolveContent).resolves({ - stat: workspaceFileStat, - content: `{"folders":[{"path":"${rootRelative}"}],"settings":{}}` - }); - stubGetFileStat.withArgs(rootActual).resolves({ - uri: rootActual, lastModification: 0, isDirectory: true - }); - (mockFileSystemWatcher.watchFileChanges).resolves(new DisposableCollection()); - - await wsService['init'](); - expect(wsService.workspace).to.eq(workspaceFileStat); - expect((await wsService.roots).length).to.eq(1); - expect(wsService.tryGetRoots().length).to.eq(1); - expect(wsService.tryGetRoots()[0].uri).to.eq(rootActual); - }); - - it('should set the exposed roots an empty array if the workspace file stores invalid workspace data', async () => { - const workspaceFileUri = 'file:///home/workspaceFile.theia-workspace'; - const workspaceFileStat = { - uri: workspaceFileUri, - lastModification: 0, - isDirectory: false - }; - (mockWorkspaceServer.getMostRecentlyUsedWorkspace).resolves(workspaceFileStat.uri); - (mockFilesystem.getFileStat).withArgs(workspaceFileUri).resolves(workspaceFileStat); - (mockFilesystem.exists).resolves(true); - (mockFilesystem.resolveContent).resolves({ - stat: workspaceFileStat, - content: 'invalid workspace data' - }); - (mockFileSystemWatcher.watchFileChanges).resolves(new DisposableCollection()); - - await wsService['init'](); - expect(wsService.workspace && wsService.workspace.uri).to.eq(workspaceFileStat.uri); - expect((await wsService.roots).length).to.eq(0); - expect(wsService.tryGetRoots().length).to.eq(0); - expect((mockILogger.error).called).to.be.true; - }); - - it('should use the workspace path in the URL fragment, if available', async function (): Promise { - const workspacePath = '/home/somewhere'; - window.location.hash = '#' + workspacePath; - const stat = { - uri: 'file://' + workspacePath, - lastModification: 0, - isDirectory: true - }; - (mockFilesystem.getFileStat).resolves(stat); - (mockFileSystemWatcher.watchFileChanges).resolves(new DisposableCollection()); - - await wsService['init'](); - expect(wsService.workspace).to.eq(stat); - expect((await wsService.roots).length).to.eq(1); - expect(wsService.tryGetRoots().length).to.eq(1); - expect(wsService.tryGetRoots()[0]).to.eq(stat); - expect((mockFileSystemWatcher.watchFileChanges).calledWith(new URI(stat.uri))).to.be.true; - expect(window.location.hash).to.eq('#' + workspacePath); - }); - }); - - describe('onStop() function', () => { - it('should send server an empty string if there is no workspace', () => { - wsService['_workspace'] = undefined; - wsService.onStop(); - expect((mockWorkspaceServer.setMostRecentlyUsedWorkspace).calledWith('')).to.be.true; - }); - - it('should send server the uri of current workspace if there is workspace opened', () => { - const uri = 'file:///home/testUri.theia-workspace'; - wsService['_workspace'] = { - uri, - lastModification: 0, - isDirectory: false - }; - wsService.onStop(); - expect((mockWorkspaceServer.setMostRecentlyUsedWorkspace).calledWith(uri)).to.be.true; - }); - }); - - describe('recentWorkspaces() function', () => { - it('should get the recent workspaces from the server', () => { - wsService.recentWorkspaces(); - expect((mockWorkspaceServer.getRecentWorkspaces).called).to.be.true; - }); - }); - - describe('open() function', () => { - it('should call doOpen() with exactly the same arguments', () => { - const uri = new URI('file:///home/testUri'); - toRestore.push(sinon.stub(WorkspaceService.prototype, 'doOpen').callsFake(() => { })); - wsService.open(uri, {}); - expect((wsService['doOpen']).calledWith(uri, {})).to.be.true; - }); - - it('should throw an error if the uri passed in is invalid or nonexistent', done => { - (mockFilesystem.getFileStat).resolves(undefined); - wsService['doOpen'](new URI('file:///home/testUri')) - .then(() => { - done(new Error('WorkspaceService.doOpen() should throw an error but did not')); - }).catch(e => { - expect(window.location.hash).to.be.empty; - done(); - }); - }); - - it('should reload the current window with new uri if preferences["workspace.preserveWindow"] = true and there is an opened current workspace', async () => { - mockPreferenceValues['workspace.preserveWindow'] = true; - const newPath = '/home/newWorkspaceUri'; - const newUriStr = 'file://' + newPath; - const newUri = new URI(newUriStr); - const stat = { - uri: newUriStr, - lastModification: 0, - isDirectory: true - }; - (mockFilesystem.getFileStat).resolves(stat); - toRestore.push(sinon.stub(wsService, 'roots').resolves([stat])); - (wsService['_workspace'] as any) = stat; - - await wsService['doOpen'](newUri, {}); - expect(windowLocationReloadStub.called).to.be.true; - expect((mockWorkspaceServer.setMostRecentlyUsedWorkspace).calledWith(newUriStr)).to.be.true; - expect(wsService.workspace).to.eq(stat); - expect(window.location.hash).to.eq('#' + newPath); - }); - - it('should keep the old Theia window & open a new window if preferences["workspace.preserveWindow"] = false and there is an opened current workspace', async () => { - mockPreferenceValues['workspace.preserveWindow'] = false; - const oldWorkspacePath = '/home/oldWorkspaceUri'; - const oldWorkspaceUriStr = 'file:///home/oldWorkspaceUri'; - const oldStat = { - uri: oldWorkspaceUriStr, - lastModification: 0, - isDirectory: true - }; - toRestore.push(sinon.stub(wsService, 'roots').resolves([oldStat])); - (wsService['_workspace'] as any) = oldStat; - window.location.hash = '#' + oldWorkspacePath; - const newWorkspaceUriStr = 'file:///home/newWorkspaceUri'; - const uri = new URI(newWorkspaceUriStr); - const newStat = { - uri: newWorkspaceUriStr, - lastModification: 0, - isDirectory: true - }; - (mockFilesystem.getFileStat).resolves(newStat); - const stubOpenNewWindow = sinon.stub(wsService, 'openNewWindow').callsFake(() => { }); - toRestore.push(stubOpenNewWindow); - - await wsService['doOpen'](uri, {}); - expect(windowLocationReloadStub.called).to.be.false; - expect((mockWorkspaceServer.setMostRecentlyUsedWorkspace).calledWith(newWorkspaceUriStr)).to.be.true; - expect(stubOpenNewWindow.called).to.be.true; - expect(wsService.workspace).to.eq(oldStat); - expect(window.location.hash).to.eq('#' + oldWorkspacePath); - }); - - it('should reload the current window with new uri if preferences["workspace.preserveWindow"] = false and browser blocks the new window being opened', async () => { - mockPreferenceValues['workspace.preserveWindow'] = false; - const oldWorkspacePath = '/home/oldWorkspaceUri'; - const oldWorkspaceUriStr = 'file://' + oldWorkspacePath; - const oldStat = { - uri: oldWorkspaceUriStr, - lastModification: 0, - isDirectory: true - }; - toRestore.push(sinon.stub(wsService, 'roots').resolves([oldStat])); - (wsService['_workspace'] as any) = oldStat; - window.location.hash = '#' + oldWorkspacePath; - const newWorkspacePath = '/home/newWorkspaceUri'; - const newWorkspaceUriStr = 'file://' + newWorkspacePath; - const uri = new URI(newWorkspaceUriStr); - const newStat = { - uri: newWorkspaceUriStr, - lastModification: 0, - isDirectory: true - }; - (mockFilesystem.getFileStat).resolves(newStat); - (mockILogger.error).resolves(undefined); - const stubOpenNewWindow = sinon.stub(wsService, 'openNewWindow').throws(); - toRestore.push(stubOpenNewWindow); - - await wsService['doOpen'](uri, {}); - expect(windowLocationReloadStub.called).to.be.true; - expect((mockWorkspaceServer.setMostRecentlyUsedWorkspace).calledWith(newWorkspaceUriStr)).to.be.true; - expect(stubOpenNewWindow.called).to.be.true; - expect(wsService.workspace).to.eq(newStat); - expect(window.location.hash).to.eq('#' + newWorkspacePath); - }); - }); - - describe('close() function', () => { - it('should reset the exposed roots and workspace, and set the most recently used workspace empty through the server', async () => { - const stat = { - uri: 'file:///home/folder', - lastModification: 0, - isDirectory: true - }; - wsService['_workspace'] = stat; - wsService['_roots'] = [stat]; - window.location.hash = '#something'; - - await wsService.close(); - expect(wsService.workspace).to.be.undefined; - expect((await wsService.roots).length).to.eq(0); - expect((mockWorkspaceServer.setMostRecentlyUsedWorkspace).calledWith('')).to.be.true; - expect(window.location.hash).to.be.empty; - }); - }); - - describe('addRoot() function', () => { - it('should throw an error if there is no opened workspace', done => { - wsService['_workspace'] = undefined; - - wsService.addRoot(new URI()) - .then(() => { - done(new Error('WorkspaceService.addRoot() should throw an error but did not.')); - }).catch(e => { - done(); - }); - }); - - it('should throw an error if the added uri is invalid or nonexistent', done => { - (mockFilesystem.getFileStat).resolves(undefined); - toRestore.push(sinon.stub(wsService, 'opened').value(true)); - wsService.addRoot(new URI()) - .then(() => { - done(new Error('WorkspaceService.addRoot() should throw an error but did not.')); - }).catch(e => { - done(); - }); - }); - - it('should throw an error if the added uri points to a file instead of a folder', done => { - (mockFilesystem.getFileStat).resolves({ - uri: 'file:///home/file.theia-workspace', - lastModification: 0, - isDirectory: false - }); - toRestore.push(sinon.stub(wsService, 'opened').value(true)); - wsService.addRoot(new URI()) - .then(() => { - done(new Error('WorkspaceService.addRoot() should throw an error but did not.')); - }).catch(e => { - done(); - }); - }); - - it('should do nothing if the added uri is already part of the current workspace', async () => { - const stat = { - uri: 'file:///home/folder', - lastModification: 0, - isDirectory: true - }; - wsService['_workspace'] = stat; - wsService['_roots'] = [stat]; - (mockFilesystem.getFileStat).resolves(stat); - - await wsService.addRoot(new URI(stat.uri)); - expect(wsService.workspace && wsService.workspace.uri).to.eq(stat.uri); - expect(wsService.tryGetRoots().length).to.eq(1); - }); - - it('should write new data into the workspace file when the workspace data is stored in a file', async () => { - const workspaceFileStat = { - uri: 'file:///home/file.theia-workspace', - lastModification: 0, - isDirectory: false - }; - wsService['_workspace'] = workspaceFileStat; - wsService['_roots'] = [folderA]; - (mockFilesystem.getFileStat).resolves(folderB); - (mockFilesystem.resolveContent).resolves({ - stat: workspaceFileStat, content: JSON.stringify({ folders: [{ path: 'folderA' }] }) - }); - (mockFilesystem.exists).resolves(true); - const spyWriteFile = sinon.spy(wsService, 'writeWorkspaceFile'); - - await wsService.addRoot(new URI(folderB.uri)); - expect(spyWriteFile.calledWith(workspaceFileStat, { folders: [{ path: folderA.uri }, { path: folderB.uri }] })).to.be.true; - }); - }); - - describe('save() function', () => { - it('should leave the current workspace unchanged if the passed in uri points to the current workspace', async () => { - const file = { - uri: 'file:///home/file.theia-workspace', - lastModification: 0, - isDirectory: false - }; - const oldWorkspaceData = { folders: [{ path: 'folderA' }, { path: 'folderB' }], settings: {} }; - (mockFilesystem.exists).resolves(true); - (mockFilesystem.getFileStat).resolves(file); - wsService['_workspace'] = file; - wsService['_roots'] = [folderA, folderB]; - const stubSetContent = (mockFilesystem.setContent).resolves(file); - (mockFilesystem.resolveContent).resolves({ stat: file, content: JSON.stringify(oldWorkspaceData) }); - - expect(wsService.workspace && wsService.workspace.uri).to.eq(file.uri); - await wsService.save(new URI(file.uri)); - expect((mockWorkspaceServer.setMostRecentlyUsedWorkspace).calledWith(file.uri)).to.be.true; - expect(stubSetContent.calledWith(file, getFormattedJson(JSON.stringify(oldWorkspaceData)))).to.be.true; - expect(wsService.workspace && wsService.workspace.uri).to.eq(file.uri); - }); - - it('should create a new workspace file, save the workspace data into that new file, and update the title of theia', async () => { - const oldFile = { - uri: 'file:///home/oldfile.theia-workspace', - lastModification: 0, - isDirectory: false - }; - const newFile = { - uri: 'file:///home/newfile.theia-workspace', - lastModification: 0, - isDirectory: false - }; - const oldWorkspaceData = { folders: [{ path: 'folderA' }, { path: 'folderB' }], settings: {} }; - wsService['_roots'] = [folderA, folderB]; - const stubExist = mockFilesystem.exists; - stubExist.withArgs(oldFile.uri).resolves(true); - stubExist.withArgs(newFile.uri).resolves(false); - (mockFilesystem.getFileStat).resolves(newFile); - (mockFileSystemWatcher.watchFileChanges).resolves(new DisposableCollection()); - wsService['_workspace'] = oldFile; - const stubSetContent = (mockFilesystem.setContent).resolves(newFile); - (mockFilesystem.resolveContent).resolves({ stat: oldFile, content: JSON.stringify(oldWorkspaceData) }); - - expect(wsService.workspace && wsService.workspace.uri).to.eq(oldFile.uri); - await wsService.save(new URI(newFile.uri)); - expect((mockWorkspaceServer.setMostRecentlyUsedWorkspace).calledWith(newFile.uri)).to.be.true; - expect(stubSetContent.calledWith(newFile, getFormattedJson(JSON.stringify(oldWorkspaceData)))).to.be.true; - expect(wsService.workspace && wsService.workspace.uri).to.eq(newFile.uri); - expect(updateTitleStub.called).to.be.true; - }); - - it('should use relative paths or translate relative paths to absolute path when necessary before saving, and emit "savedLocationChanged" event', done => { - const oldFile = { - uri: 'file:///home/oldFolder/oldFile.theia-workspace', - lastModification: 0, - isDirectory: false - }; - const newFile = { - uri: 'file:///home/newFolder/newFile.theia-workspace', - lastModification: 0, - isDirectory: false - }; - const folder1 = { - uri: 'file:///home/thirdFolder/folder1.theia-workspace', - lastModification: 0, - isDirectory: true - }; - const folder2 = { - uri: 'file:///home/newFolder/folder2.theia-workspace', - lastModification: 0, - isDirectory: true - }; - const oldWorkspaceData = { folders: [{ path: folder1.uri }, { path: folder2.uri }], settings: {} }; - (mockFilesystem.resolveContent).resolves({ stat: oldFile, content: JSON.stringify(oldWorkspaceData) }); - const stubExist = mockFilesystem.exists; - stubExist.withArgs(oldFile.uri).resolves(true); - stubExist.withArgs(newFile.uri).resolves(false); - (mockFilesystem.getFileStat).resolves(newFile); - wsService['_workspace'] = oldFile; - wsService['_roots'] = [folder1, folder2]; - const stubSetContent = (mockFilesystem.setContent).resolves(newFile); - (mockFileSystemWatcher.watchFileChanges).resolves(new DisposableCollection()); - - expect(wsService.workspace && wsService.workspace.uri).to.eq(oldFile.uri); - wsService.onWorkspaceLocationChanged(() => { - done(); - }); - wsService.save(new URI(newFile.uri)).then(() => { - expect((mockWorkspaceServer.setMostRecentlyUsedWorkspace).calledWith(newFile.uri)).to.be.true; - expect(stubSetContent.calledWith(newFile, getFormattedJson(JSON.stringify({ folders: [{ path: folder1.uri }, { path: 'folder2' }], settings: {} })))).to.be.true; - expect(wsService.workspace && wsService.workspace.uri).to.eq(newFile.uri); - expect(updateTitleStub.called).to.be.true; - }); - }).timeout(2000); - }); - - describe('saved status', () => { - it('should be true if there is an opened workspace, and the opened workspace is not a folder, otherwise false', () => { - const file = { - uri: 'file:///home/file.theia-workspace', - lastModification: 0, - isDirectory: false - }; - - expect(wsService.saved).to.be.false; - wsService['_workspace'] = file; - expect(wsService.saved).to.be.true; - wsService['_workspace'] = folderA; - expect(wsService.saved).to.be.false; - }); - }); - - describe('isMultiRootWorkspaceEnabled status', () => { - it('should be true if there is an opened workspace and preference["workspace.supportMultiRootWorkspace"] = true, otherwise false', () => { - mockPreferenceValues['workspace.supportMultiRootWorkspace'] = true; - expect(wsService.isMultiRootWorkspaceEnabled).to.be.false; - - const file = { - uri: 'file:///home/file.theia-workspace', - lastModification: 0, - isDirectory: false - }; - wsService['_workspace'] = file; - mockPreferenceValues['workspace.supportMultiRootWorkspace'] = true; - expect(wsService.isMultiRootWorkspaceEnabled).to.be.true; - - mockPreferenceValues['workspace.supportMultiRootWorkspace'] = false; - expect(wsService.isMultiRootWorkspaceEnabled).to.be.false; - }); - }); - - describe('isMultiRootWorkspaceOpened status', () => { - it('should be true if there is an opened workspace and the workspace is not a directory, otherwise false', () => { - expect(wsService.isMultiRootWorkspaceOpened).to.be.false; - - const file = { - uri: 'file:///home/file.theia-workspace', - lastModification: 0, - isDirectory: false - }; - wsService['_workspace'] = file; - expect(wsService.isMultiRootWorkspaceOpened).to.be.true; - - const dir = { - uri: 'file:///home/dir', - lastModification: 0, - isDirectory: true - }; - wsService['_workspace'] = dir; - expect(wsService.isMultiRootWorkspaceOpened).to.be.false; - }); - }); - - describe('containsSome() function', () => { - it('should resolve false if the current workspace is not open', async () => { - sinon.stub(wsService, 'roots').resolves([]); - sinon.stub(wsService, 'opened').value(false); - wsService['_roots'] = []; - - expect(await wsService.containsSome([])).to.be.false; - }); - - it('should resolve false if the passed in paths is an empty array', async () => { - sinon.stub(wsService, 'roots').resolves([]); - sinon.stub(wsService, 'opened').value(true); - wsService['_roots'] = [folderA, folderB]; - - expect(await wsService.containsSome([])).to.be.false; - }); - - it('should resolve false if on or more passed in paths are found in the workspace, otherwise false', async () => { - sinon.stub(wsService, 'roots').value([folderA, folderB]); - sinon.stub(wsService, 'opened').value(true); - wsService['_roots'] = [folderA, folderB]; - - (mockFilesystem.exists).withArgs('file:///home/folderB/subfolder').resolves(true); - const val = await wsService.containsSome(['A', 'subfolder', 'C']); - expect(val).to.be.true; - expect(await wsService.containsSome(['C', 'A', 'B'])).to.be.false; - }); - }); - - describe('removeRoots() function', () => { - it('should throw an error if the current workspace is not open', done => { - sinon.stub(wsService, 'opened').value(false); - - wsService.removeRoots([]).then(() => { - done(new Error('WorkspaceService.removeRoots() should throw an error while did not.')); - }).catch(e => { - done(); - }); - }); - - it('should not update the workspace file if the workspace is undefined', async () => { - wsService['_workspace'] = undefined; - sinon.stub(wsService, 'opened').value(true); - const stubWriteWorkspaceFile = sinon.stub(wsService, 'writeWorkspaceFile'); - - await wsService.removeRoots([]); - expect(stubWriteWorkspaceFile.called).to.be.false; - }); - - it('should update the workspace file with remaining folders', async () => { - const file = { - uri: 'file:///home/oneFile.theia-workspace', - lastModification: 0, - isDirectory: false - }; - wsService['_workspace'] = file; - sinon.stub(wsService, 'opened').value(true); - wsService['_roots'] = [folderA, folderB]; - const stubSetContent = mockFilesystem.setContent; - stubSetContent.resolves(file); - (mockFilesystem.resolveContent).resolves({ stat: file, content: JSON.stringify({ folders: [{ path: 'folderA' }, { path: 'folderB' }] }) }); - (mockFilesystem.exists).resolves(true); - - await wsService.removeRoots([new URI()]); - expect(stubSetContent.calledWith(file, getFormattedJson(JSON.stringify({ folders: [{ path: 'folderA' }, { path: 'folderB' }] })))).to.be.true; - - await wsService.removeRoots([new URI(folderB.uri)]); - expect(stubSetContent.calledWith(file, getFormattedJson(JSON.stringify({ folders: [{ path: 'folderA' }] })))).to.be.true; - }); - }); - - describe('spliceRoots', () => { - const workspace = { uri: 'file:///workspace.theia-workspace', isDirectory: false }; - const fooDir = { uri: 'file:///foo', isDirectory: true }; - const workspaceService: WorkspaceService = new WorkspaceService(); - workspaceService['getUntitledWorkspace'] = async () => new URI('file:///untitled.theia-workspace'); - workspaceService['save'] = async () => { }; - workspaceService['getWorkspaceDataFromFile'] = async () => ({ folders: [] }); - workspaceService['writeWorkspaceFile'] = async (_, data) => { - workspaceService['_roots'] = data.folders.map(({ path }) => { uri: path }); - return undefined; - }; - const assertRemoved = (removed: URI[], ...roots: string[]) => - assert.deepEqual(removed.map(uri => uri.toString()), roots); - const assertRoots = (...roots: string[]) => - assert.deepEqual(workspaceService['_roots'].map(root => root.uri), roots); - - beforeEach(() => { - workspaceService['_workspace'] = workspace; - workspaceService['_roots'] = [fooDir]; - }); - - it('skip', async () => { - assertRemoved(await workspaceService.spliceRoots(0, 0)); - assertRoots('file:///foo'); - }); - - it('add', async () => { - assertRemoved(await workspaceService.spliceRoots(1, 0, new URI('file:///bar'))); - assertRoots('file:///foo', 'file:///bar'); - }); - - it('add dups', async () => { - assertRemoved(await workspaceService.spliceRoots(1, 0, new URI('file:///bar'), new URI('file:///baz'), new URI('file:///bar'))); - assertRoots('file:///foo', 'file:///bar', 'file:///baz'); - }); - - it('remove', async () => { - assertRemoved(await workspaceService.spliceRoots(0, 1), 'file:///foo'); - assertRoots(); - }); - - it('update', async () => { - assertRemoved(await workspaceService.spliceRoots(0, 1, new URI('file:///bar')), 'file:///foo'); - assertRoots('file:///bar'); - }); - - it('add untitled', async () => { - workspaceService['_workspace'] = fooDir; - assertRemoved(await workspaceService.spliceRoots(1, 0, new URI('file:///bar'))); - assertRoots('file:///foo', 'file:///bar'); - }); - }); - - describe('getWorkspaceRootUri() function', () => { - it('should return undefined if no uri is passed into the function', () => { - expect(wsService.getWorkspaceRootUri(undefined)).to.be.undefined; - }); - - it('should return the root folder uri that the file belongs to', () => { - wsService['_roots'] = [folderA, folderB]; - const root = wsService.getWorkspaceRootUri(new URI(folderB.uri + '/testfile')); - expect(root!.toString()).to.equal(folderB.uri); - }); - - it('should return the closest root folder uri that the file belongs to', () => { - const home = Object.freeze({ - uri: 'file:///home', - lastModification: 0, - isDirectory: true - }); - wsService['_roots'] = [folderA, folderB, home]; - const root = wsService.getWorkspaceRootUri(new URI(folderB.uri + '/testfile')); - expect(root!.toString()).to.equal(folderB.uri); - }); - }); - - it('should emit roots in the current workspace when initialized', done => { - const rootA = 'file:///folderA'; - const rootB = 'file:///folderB'; - const statA = { - uri: rootA, - lastModification: 0, - isDirectory: true - }; - const statB = { - uri: rootB, - lastModification: 0, - isDirectory: true - }; - const dis = wsService.onWorkspaceChanged(roots => { - expect(roots.length).to.eq(2); - expect(roots[0].uri).to.eq(rootA); - expect(roots[1].uri).to.eq(rootB); - dis.dispose(); - done(); - }); - toDispose.push(dis); - wsService['onWorkspaceChangeEmitter'].fire([statA, statB]); - }).timeout(2000); - - it('should emit updated roots when workspace file is changed', done => { - const workspaceFileUri = 'file:///home/workspaceFile.theia-workspace'; - const workspaceFileStat = { - uri: workspaceFileUri, - lastModification: 0, - isDirectory: false - }; - wsService['_workspace'] = workspaceFileStat; - const folderC = { - uri: 'file:///home/folderC', - lastModification: 0, - isDirectory: true - }; - - (mockWorkspaceServer.getMostRecentlyUsedWorkspace).resolves(workspaceFileUri); - const stubGetFileStat = (mockFilesystem.getFileStat); - stubGetFileStat.withArgs(workspaceFileUri).resolves(workspaceFileStat); - (mockFilesystem.exists).resolves(true); - const oldWorkspaceFileContent = { - stat: workspaceFileStat, - content: '{"folders":[{"path":"folderA"},{"path":"folderB"}],"settings":{}}' - }; - const newWorkspaceFileContent = { - stat: workspaceFileStat, - content: '{"folders":[{"path":"folderB"},{"path":"folderC"}],"settings":{}}' - }; - (mockFilesystem.resolveContent).onCall(0).resolves(oldWorkspaceFileContent); - (mockFilesystem.resolveContent).onCall(1).resolves(newWorkspaceFileContent); - (mockFilesystem.resolveContent).onCall(2).resolves(newWorkspaceFileContent); - stubGetFileStat.withArgs(folderA.uri).resolves(folderA); - stubGetFileStat.withArgs(folderB.uri).resolves(folderB); - stubGetFileStat.withArgs(folderC.uri).resolves(folderC); - (mockFileSystemWatcher.watchFileChanges).resolves(new DisposableCollection()); - - wsService['init']().then(() => { - const dis = wsService.onWorkspaceChanged(roots => { - expect(roots.length).to.eq(2); - expect(roots[0].uri).to.eq(folderB.uri); - expect(roots[1].uri).to.eq(folderC.uri); - dis.dispose(); - done(); - }); - toDispose.push(dis); - mockFileChangeEmitter.fire([{ uri: new URI(workspaceFileUri), type: FileChangeType.UPDATED }]); - }); - }).timeout(2000); - - describe('isWorkspaceFile()', () => { - it(`should return true for '${THEIA_EXT}' files`, async () => { - const uri = `/home/foo/bar.${THEIA_EXT}`; - const stat = { - uri: uri, - lastModification: 0, - isDirectory: false - }; - expect(await wsService['isWorkspaceFile'](stat)).equal(true); - }); - it(`should return true for '${VSCODE_EXT}' files`, async () => { - const uri = `/home/foo/bar.${VSCODE_EXT}`; - const stat = { - uri: uri, - lastModification: 0, - isDirectory: false - }; - expect(await wsService['isWorkspaceFile'](stat)).equal(true); - }); - it(`should return false if not a '${THEIA_EXT}' or '${VSCODE_EXT} file`, async () => { - const uri = '/home/foo/foo.bar'; - const stat = { - uri: uri, - lastModification: 0, - isDirectory: false - }; - expect(await wsService['isWorkspaceFile'](stat)).equal(false); - }); - }); - -}); diff --git a/packages/workspace/src/browser/workspace-service.ts b/packages/workspace/src/browser/workspace-service.ts index a184dffa9b5d8..8b792a774dcd3 100644 --- a/packages/workspace/src/browser/workspace-service.ts +++ b/packages/workspace/src/browser/workspace-service.ts @@ -16,12 +16,10 @@ import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; -import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; -import { FileSystemWatcher, FileChangeEvent } from '@theia/filesystem/lib/browser/filesystem-watcher'; import { WorkspaceServer, THEIA_EXT, VSCODE_EXT, getTemporaryWorkspaceFileUri } from '../common'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { - FrontendApplicationContribution, PreferenceServiceImpl, PreferenceScope, PreferenceSchemaProvider + FrontendApplicationContribution, PreferenceServiceImpl, PreferenceScope, PreferenceSchemaProvider, LabelProvider } from '@theia/core/lib/browser'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; @@ -30,6 +28,9 @@ import { WorkspacePreferences } from './workspace-preferences'; import * as jsoncparser from 'jsonc-parser'; import * as Ajv from 'ajv'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +import { FileStat, BaseStat } from '@theia/filesystem/lib/common/files'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileSystemPreferences } from '@theia/filesystem/lib/browser'; /** * The workspace service. @@ -42,11 +43,8 @@ export class WorkspaceService implements FrontendApplicationContribution { private _roots: FileStat[] = []; private deferredRoots = new Deferred(); - @inject(FileSystem) - protected readonly fileSystem: FileSystem; - - @inject(FileSystemWatcher) - protected readonly watcher: FileSystemWatcher; + @inject(FileService) + protected readonly fileService: FileService; @inject(WorkspaceServer) protected readonly server: WorkspaceServer; @@ -72,6 +70,12 @@ export class WorkspaceService implements FrontendApplicationContribution { @inject(MessageService) protected readonly messageService: MessageService; + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + @inject(FileSystemPreferences) + protected readonly fsPreferences: FileSystemPreferences; + protected applicationName: string; @postConstruct() @@ -81,17 +85,21 @@ export class WorkspaceService implements FrontendApplicationContribution { const wsStat = await this.toFileStat(wsUriString); await this.setWorkspace(wsStat); - this.watcher.onFilesChanged(event => { - if (this._workspace && FileChangeEvent.isAffected(event, new URI(this._workspace.uri))) { + this.fileService.onDidFilesChange(event => { + if (this._workspace && this._workspace.isFile && event.contains(this._workspace.resource)) { this.updateWorkspace(); } }); this.preferences.onPreferenceChanged(event => { - const multiRootPrefName = 'workspace.supportMultiRootWorkspace'; - if (event.preferenceName === multiRootPrefName) { + if (event.preferenceName === 'workspace.supportMultiRootWorkspace') { this.updateWorkspace(); } }); + this.fsPreferences.onPreferenceChanged(e => { + if (e.preferenceName === 'files.watcherExclude') { + this.refreshRootWatchers(); + } + }); } /** @@ -114,8 +122,11 @@ export class WorkspaceService implements FrontendApplicationContribution { // Remove the leading # and decode the URI. const wpPath = decodeURI(window.location.hash.substring(1)); const workspaceUri = new URI().withPath(wpPath).withScheme('file'); - const workspaceStat = await this.fileSystem.getFileStat(workspaceUri.toString()); - if (workspaceStat && !workspaceStat.isDirectory && !await this.isWorkspaceFile(workspaceStat)) { + let workspaceStat: FileStat | undefined; + try { + workspaceStat = await this.fileService.resolve(workspaceUri); + } catch { } + if (workspaceStat && !workspaceStat.isDirectory && !this.isWorkspaceFile(workspaceStat)) { this.messageService.error(`Not a valid workspace file: ${workspaceUri}`); return undefined; } @@ -164,14 +175,20 @@ export class WorkspaceService implements FrontendApplicationContribution { protected readonly toDisposeOnWorkspace = new DisposableCollection(); protected async setWorkspace(workspaceStat: FileStat | undefined): Promise { - if (FileStat.equals(this._workspace, workspaceStat)) { + if (this._workspace && workspaceStat && + this._workspace.resource === workspaceStat.resource && + this._workspace.mtime === workspaceStat.mtime && + this._workspace.etag === workspaceStat.etag && + this._workspace.size === workspaceStat.size) { return; } this.toDisposeOnWorkspace.dispose(); this._workspace = workspaceStat; if (this._workspace) { - const uri = new URI(this._workspace.uri); - this.toDisposeOnWorkspace.push(await this.watcher.watchFileChanges(uri)); + const uri = this._workspace.resource; + if (this._workspace.isFile) { + this.toDisposeOnWorkspace.push(this.fileService.watch(uri)); + } this.setURLFragment(uri.path.toString()); } else { this.setURLFragment(''); @@ -181,9 +198,6 @@ export class WorkspaceService implements FrontendApplicationContribution { } protected async updateWorkspace(): Promise { - if (this._workspace) { - this.toFileStat(this._workspace.uri).then(stat => this._workspace = stat); - } await this.updateRoots(); this.watchRoots(); } @@ -195,7 +209,7 @@ export class WorkspaceService implements FrontendApplicationContribution { rootsChanged = true; } else { for (const newRoot of newRoots) { - if (!this._roots.some(r => r.uri === newRoot.uri)) { + if (!this._roots.some(r => r.resource.toString() === newRoot.resource.toString())) { rootsChanged = true; break; } @@ -224,11 +238,7 @@ export class WorkspaceService implements FrontendApplicationContribution { if (valid) { roots.push(valid); } else { - roots.push({ - uri: path, - lastModification: Date.now(), - isDirectory: true - }); + roots.push(FileStat.dir(path)); } } } @@ -237,21 +247,21 @@ export class WorkspaceService implements FrontendApplicationContribution { } protected async getWorkspaceDataFromFile(): Promise { - if (this._workspace && await this.fileSystem.exists(this._workspace.uri)) { + if (this._workspace && await this.fileService.exists(this._workspace.resource)) { if (this._workspace.isDirectory) { return { - folders: [{ path: this._workspace.uri }] + folders: [{ path: this._workspace.resource.toString() }] }; - } else if (await this.isWorkspaceFile(this._workspace)) { - const { stat, content } = await this.fileSystem.resolveContent(this._workspace.uri); - const strippedContent = jsoncparser.stripComments(content); + } else if (this.isWorkspaceFile(this._workspace)) { + const stat = await this.fileService.read(this._workspace.resource); + const strippedContent = jsoncparser.stripComments(stat.value); const data = jsoncparser.parse(strippedContent); if (data && WorkspaceData.is(data)) { return WorkspaceData.transformToAbsolute(data, stat); } - this.logger.error(`Unable to retrieve workspace data from the file: '${this._workspace.uri}'. Please check if the file is corrupted.`); + this.logger.error(`Unable to retrieve workspace data from the file: '${this.labelProvider.getLongName(this._workspace)}'. Please check if the file is corrupted.`); } else { - this.logger.warn(`Not a valid workspace file: ${this._workspace.uri}`); + this.logger.warn(`Not a valid workspace file: ${this.labelProvider.getLongName(this._workspace)}`); } } } @@ -264,8 +274,7 @@ export class WorkspaceService implements FrontendApplicationContribution { protected updateTitle(): void { let title: string | undefined; if (this._workspace) { - const uri = new URI(this._workspace.uri); - const displayName = uri.displayName; + const displayName = this._workspace.name; if (!this._workspace.isDirectory && (displayName.endsWith(`.${THEIA_EXT}`) || displayName.endsWith(`.${VSCODE_EXT}`))) { title = displayName.slice(0, displayName.lastIndexOf('.')); @@ -280,7 +289,7 @@ export class WorkspaceService implements FrontendApplicationContribution { * on unload, we set our workspace root as the last recently used on the backend. */ onStop(): void { - this.server.setMostRecentlyUsedWorkspace(this._workspace ? this._workspace.uri : ''); + this.server.setMostRecentlyUsedWorkspace(this._workspace ? this._workspace.resource.toString() : ''); } async recentWorkspaces(): Promise { @@ -322,7 +331,7 @@ export class WorkspaceService implements FrontendApplicationContribution { const rootUri = uri.toString(); const stat = await this.toFileStat(rootUri); if (stat) { - if (!stat.isDirectory && !await this.isWorkspaceFile(stat)) { + if (!stat.isDirectory && !this.isWorkspaceFile(stat)) { const message = `Not a valid workspace file: ${uri}`; this.messageService.error(message); throw new Error(message); @@ -363,7 +372,7 @@ export class WorkspaceService implements FrontendApplicationContribution { const workspaceData = await this.getWorkspaceDataFromFile(); this._workspace = await this.writeWorkspaceFile(this._workspace, WorkspaceData.buildWorkspaceData( - this._roots.filter(root => uris.findIndex(u => u.toString() === root.uri) < 0), + this._roots.filter(root => uris.findIndex(u => u.toString() === root.resource.toString()) < 0), workspaceData!.settings ) ); @@ -375,7 +384,7 @@ export class WorkspaceService implements FrontendApplicationContribution { throw new Error('There is not active workspace'); } const dedup = new Set(); - const roots = this._roots.map(root => (dedup.add(root.uri), root.uri)); + const roots = this._roots.map(root => (dedup.add(root.resource.toString()), root.resource.toString())); const toAdd: string[] = []; for (const root of rootsToAdd) { const uri = root.toString(); @@ -390,9 +399,7 @@ export class WorkspaceService implements FrontendApplicationContribution { } if (this._workspace.isDirectory) { const untitledWorkspace = await this.getUntitledWorkspace(); - if (untitledWorkspace) { - await this.save(untitledWorkspace); - } + await this.save(untitledWorkspace); } const currentData = await this.getWorkspaceDataFromFile(); const newData = WorkspaceData.buildWorkspaceData(roots, currentData && currentData.settings); @@ -400,7 +407,7 @@ export class WorkspaceService implements FrontendApplicationContribution { return toRemove.map(root => new URI(root)); } - protected async getUntitledWorkspace(): Promise { + async getUntitledWorkspace(): Promise { return getTemporaryWorkspaceFileUri(this.envVariableServer); } @@ -409,8 +416,8 @@ export class WorkspaceService implements FrontendApplicationContribution { const data = JSON.stringify(WorkspaceData.transformToRelative(workspaceData, workspaceFile)); const edits = jsoncparser.format(data, undefined, { tabSize: 3, insertSpaces: true, eol: '' }); const result = jsoncparser.applyEdits(data, edits); - const stat = await this.fileSystem.setContent(workspaceFile, result); - return stat; + await this.fileService.write(workspaceFile.resource, result); + return this.fileService.resolve(workspaceFile.resource); } } @@ -448,19 +455,15 @@ export class WorkspaceService implements FrontendApplicationContribution { if (uriStr.endsWith('/')) { uriStr = uriStr.slice(0, -1); } - const normalizedUriStr = new URI(uriStr).normalizePath().toString(); - const fileStat = await this.fileSystem.getFileStat(normalizedUriStr); - if (!fileStat) { - return undefined; - } - return fileStat; + const normalizedUri = new URI(uriStr).normalizePath(); + return await this.fileService.resolve(normalizedUri); } catch (error) { return undefined; } } protected openWindow(uri: FileStat, options?: WorkspaceInput): void { - const workspacePath = new URI(uri.uri).path.toString(); + const workspacePath = uri.resource.path.toString(); if (this.shouldPreserveWindow(options)) { this.reloadWindow(); @@ -478,7 +481,7 @@ export class WorkspaceService implements FrontendApplicationContribution { protected reloadWindow(): void { // Set the new workspace path as the URL fragment. if (this._workspace !== undefined) { - this.setURLFragment(new URI(this._workspace.uri).path.toString()); + this.setURLFragment(this._workspace.resource.path.toString()); } else { this.setURLFragment(''); } @@ -504,10 +507,10 @@ export class WorkspaceService implements FrontendApplicationContribution { await this.roots; if (this.opened) { for (const root of this._roots) { - const uri = new URI(root.uri); + const uri = root.resource; for (const path of paths) { - const fileUri = uri.resolve(path).toString(); - const exists = await this.fileSystem.exists(fileUri); + const fileUri = uri.resolve(path); + const exists = await this.fileService.exists(fileUri); if (exists) { return exists; } @@ -526,9 +529,9 @@ export class WorkspaceService implements FrontendApplicationContribution { * @param uri URI or FileStat of the workspace file */ async save(uri: URI | FileStat): Promise { - const uriStr = uri instanceof URI ? uri.toString() : uri.uri; - if (!await this.fileSystem.exists(uriStr)) { - await this.fileSystem.createFile(uriStr); + const resource = uri instanceof URI ? uri : uri.resource; + if (!await this.fileService.exists(resource)) { + await this.fileService.create(resource); } const workspaceData: WorkspaceData = { folders: [], settings: {} }; if (!this.saved) { @@ -542,10 +545,10 @@ export class WorkspaceService implements FrontendApplicationContribution { } } } - let stat = await this.toFileStat(uriStr); + let stat = await this.toFileStat(resource); Object.assign(workspaceData, await this.getWorkspaceDataFromFile()); stat = await this.writeWorkspaceFile(stat, WorkspaceData.buildWorkspaceData(this._roots, workspaceData ? workspaceData.settings : undefined)); - await this.server.setMostRecentlyUsedWorkspace(uriStr); + await this.server.setMostRecentlyUsedWorkspace(resource.toString()); await this.setWorkspace(stat); this.onWorkspaceLocationChangedEmitter.fire(stat); } @@ -553,7 +556,7 @@ export class WorkspaceService implements FrontendApplicationContribution { protected readonly rootWatchers = new Map(); protected async watchRoots(): Promise { - const rootUris = new Set(this._roots.map(r => r.uri)); + const rootUris = new Set(this._roots.map(r => r.resource.toString())); for (const [uri, watcher] of this.rootWatchers.entries()) { if (!rootUris.has(uri)) { watcher.dispose(); @@ -564,16 +567,32 @@ export class WorkspaceService implements FrontendApplicationContribution { } } + protected async refreshRootWatchers(): Promise { + for (const watcher of this.rootWatchers.values()) { + watcher.dispose(); + } + await this.watchRoots(); + } + protected async watchRoot(root: FileStat): Promise { - const uriStr = root.uri; + const uriStr = root.resource.toString(); if (this.rootWatchers.has(uriStr)) { return; } - const watcher = this.watcher.watchFileChanges(new URI(uriStr)); - this.rootWatchers.set(uriStr, Disposable.create(() => { - watcher.then(disposable => disposable.dispose()); - this.rootWatchers.delete(uriStr); - })); + const excludes = this.getExcludes(uriStr); + const watcher = this.fileService.watch(new URI(uriStr), { + recursive: true, + excludes + }); + this.rootWatchers.set(uriStr, new DisposableCollection( + watcher, + Disposable.create(() => this.rootWatchers.delete(uriStr)) + )); + } + + protected getExcludes(uri: string): string[] { + const patterns = this.fsPreferences.get('files.watcherExclude', undefined, uri); + return Object.keys(patterns).filter(pattern => patterns[pattern]); } /** @@ -586,13 +605,13 @@ export class WorkspaceService implements FrontendApplicationContribution { if (!uri) { const root = this.tryGetRoots()[0]; if (root) { - return new URI(root.uri); + return root.resource; } return undefined; } const rootUris: URI[] = []; for (const root of this.tryGetRoots()) { - const rootUri = new URI(root.uri); + const rootUri = root.resource; if (rootUri && rootUri.isEqualOrParent(uri)) { rootUris.push(rootUri); } @@ -604,7 +623,7 @@ export class WorkspaceService implements FrontendApplicationContribution { if (!uris.length) { return false; } - const rootUris = new Set(this.tryGetRoots().map(root => root.uri)); + const rootUris = new Set(this.tryGetRoots().map(root => root.resource.toString())); return uris.every(uri => rootUris.has(uri.toString())); } @@ -613,8 +632,8 @@ export class WorkspaceService implements FrontendApplicationContribution { * * Example: We should not try to read the contents of an .exe file. */ - protected async isWorkspaceFile(fileStat: FileStat): Promise { - return fileStat.uri.endsWith(`.${THEIA_EXT}`) || fileStat.uri.endsWith(`.${VSCODE_EXT}`); + protected isWorkspaceFile(fileStat: FileStat): boolean { + return fileStat.resource.path.ext === `.${THEIA_EXT}` || fileStat.resource.path.ext === `.${VSCODE_EXT}`; } } @@ -669,7 +688,7 @@ export namespace WorkspaceData { let roots: string[] = []; if (folders.length > 0) { if (typeof folders[0] !== 'string') { - roots = (folders).map(folder => folder.uri); + roots = (folders).map(folder => folder.resource.toString()); } else { roots = folders; } @@ -685,7 +704,7 @@ export namespace WorkspaceData { export function transformToRelative(data: WorkspaceData, workspaceFile?: FileStat): WorkspaceData { const folderUris: string[] = []; - const workspaceFileUri = new URI(workspaceFile ? workspaceFile.uri : '').withScheme('file'); + const workspaceFileUri = new URI(workspaceFile ? workspaceFile.resource.toString() : '').withScheme('file'); for (const { path } of data.folders) { const folderUri = new URI(path).withScheme('file'); const rel = workspaceFileUri.parent.relative(folderUri); @@ -698,7 +717,7 @@ export namespace WorkspaceData { return buildWorkspaceData(folderUris, data.settings); } - export function transformToAbsolute(data: WorkspaceData, workspaceFile?: FileStat): WorkspaceData { + export function transformToAbsolute(data: WorkspaceData, workspaceFile?: BaseStat): WorkspaceData { if (workspaceFile) { const folders: string[] = []; for (const folder of data.folders) { @@ -706,7 +725,7 @@ export namespace WorkspaceData { if (path.startsWith('file:///')) { folders.push(path); } else { - folders.push(new URI(workspaceFile.uri).withScheme('file').parent.resolve(path).toString()); + folders.push(workspaceFile.resource.withScheme('file').parent.resolve(path).toString()); } } diff --git a/packages/workspace/src/browser/workspace-storage-service.ts b/packages/workspace/src/browser/workspace-storage-service.ts index 68eb18eb80d45..4eb540877d9d3 100644 --- a/packages/workspace/src/browser/workspace-storage-service.ts +++ b/packages/workspace/src/browser/workspace-storage-service.ts @@ -18,7 +18,7 @@ import { inject, injectable, postConstruct } from 'inversify'; import { LocalStorageService } from '@theia/core/lib/browser/storage-service'; import { StorageService } from '@theia/core/lib/browser/storage-service'; import { WorkspaceService } from './workspace-service'; -import { FileStat } from '@theia/filesystem/lib/common'; +import { FileStat } from '@theia/filesystem/lib/common/files'; /* * Prefixes any stored data with the current workspace path. @@ -59,7 +59,7 @@ export class WorkspaceStorageService implements StorageService { } protected getPrefix(workspaceStat: FileStat | undefined): string { - return workspaceStat ? workspaceStat.uri : '_global_'; + return workspaceStat ? workspaceStat.resource.toString() : '_global_'; } private updatePrefix(): void { diff --git a/packages/workspace/src/browser/workspace-uri-contribution.spec.ts b/packages/workspace/src/browser/workspace-uri-contribution.spec.ts index ab1b6fb64cebf..1d9a450c8b022 100644 --- a/packages/workspace/src/browser/workspace-uri-contribution.spec.ts +++ b/packages/workspace/src/browser/workspace-uri-contribution.spec.ts @@ -23,13 +23,13 @@ import { Container } from 'inversify'; import { Signal } from '@phosphor/signaling'; import { Event } from '@theia/core/lib/common/event'; import { ApplicationShell, WidgetManager } from '@theia/core/lib/browser'; -import { FileStat, FileSystem } from '@theia/filesystem/lib/common/filesystem'; -import { MockFilesystem } from '@theia/filesystem/lib/common/test'; import { DefaultUriLabelProviderContribution } from '@theia/core/lib/browser/label-provider'; import { WorkspaceUriLabelProviderContribution } from './workspace-uri-contribution'; import URI from '@theia/core/lib/common/uri'; import { WorkspaceVariableContribution } from './workspace-variable-contribution'; import { WorkspaceService } from './workspace-service'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileStat } from '@theia/filesystem/lib/common/files'; after(() => disableJSDOM()); @@ -37,11 +37,7 @@ let container: Container; let labelProvider: WorkspaceUriLabelProviderContribution; let roots: FileStat[]; beforeEach(() => { - roots = [{ - uri: 'file:///workspace', - lastModification: 0, - isDirectory: true - }]; + roots = [FileStat.dir('file:///workspace')]; container = new Container(); container.bind(ApplicationShell).toConstantValue({ @@ -58,7 +54,7 @@ beforeEach(() => { container.bind(WorkspaceService).toConstantValue(workspaceService); container.bind(WorkspaceVariableContribution).toSelf().inSingletonScope(); container.bind(WorkspaceUriLabelProviderContribution).toSelf().inSingletonScope(); - container.bind(FileSystem).to(MockFilesystem).inSingletonScope(); + container.bind(FileService).toConstantValue({} as FileService); labelProvider = container.get(WorkspaceUriLabelProviderContribution); }); @@ -84,51 +80,31 @@ describe('WorkspaceUriLabelProviderContribution class', () => { it('should return 10 if the passed in argument is a FileStat or URI with the "file" scheme', () => { expect(labelProvider.canHandle(new URI('file:///home/settings.json'))).eq(10); - expect(labelProvider.canHandle({ - uri: 'file:///home/settings.json', - lastModification: 0, - isDirectory: false - })).eq(10); + expect(labelProvider.canHandle(FileStat.file('file:///home/settings.json'))).eq(10); }); }); describe('getIcon()', () => { it('should return folder icon from the FileStat of a folder', async () => { - expect(labelProvider.getIcon({ - uri: 'file:///home/', - lastModification: 0, - isDirectory: true - })).eq(labelProvider.defaultFolderIcon); + expect(labelProvider.getIcon(FileStat.dir('file:///home/'))).eq(labelProvider.defaultFolderIcon); }); it('should return file icon from a non-folder FileStat', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any stubs.push(sinon.stub(DefaultUriLabelProviderContribution.prototype, 'getFileIcon').returns(undefined)); - expect(labelProvider.getIcon({ - uri: 'file:///home/test', - lastModification: 0, - isDirectory: false - })).eq(labelProvider.defaultFileIcon); + expect(labelProvider.getIcon(FileStat.file('file:///home/test'))).eq(labelProvider.defaultFileIcon); }); it('should return folder icon from a folder URI', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any stubs.push(sinon.stub(DefaultUriLabelProviderContribution.prototype, 'getFileIcon').returns(undefined)); - expect(labelProvider.getIcon({ - uri: 'file:///home/test', - lastModification: 0, - isDirectory: true - })).eq(labelProvider.defaultFolderIcon); + expect(labelProvider.getIcon(FileStat.dir('file:///home/test'))).eq(labelProvider.defaultFolderIcon); }); it('should return file icon from a file URI', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any stubs.push(sinon.stub(DefaultUriLabelProviderContribution.prototype, 'getFileIcon').returns(undefined)); - expect(labelProvider.getIcon({ - uri: 'file:///home/test', - lastModification: 0, - isDirectory: false - })).eq(labelProvider.defaultFileIcon); + expect(labelProvider.getIcon(FileStat.file('file:///home/test'))).eq(labelProvider.defaultFileIcon); }); it('should return what getFileIcon() returns from a URI or non-folder FileStat, if getFileIcon() does not return null or undefined', async () => { @@ -136,11 +112,7 @@ describe('WorkspaceUriLabelProviderContribution class', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any stubs.push(sinon.stub(DefaultUriLabelProviderContribution.prototype, 'getFileIcon').returns(ret)); expect(labelProvider.getIcon(new URI('file:///home/test'))).eq(ret); - expect(labelProvider.getIcon({ - uri: 'file:///home/test', - lastModification: 0, - isDirectory: false - })).eq(ret); + expect(labelProvider.getIcon(FileStat.file('file:///home/test'))).eq(ret); }); }); @@ -152,11 +124,7 @@ describe('WorkspaceUriLabelProviderContribution class', () => { }); it('should return the display name of a file from its FileStat', () => { - const file: FileStat = { - uri: 'file:///workspace-2/jacques.doc', - lastModification: 0, - isDirectory: false - }; + const file: FileStat = FileStat.file('file:///workspace-2/jacques.doc'); const name = labelProvider.getName(file); expect(name).eq('jacques.doc'); }); @@ -170,11 +138,7 @@ describe('WorkspaceUriLabelProviderContribution class', () => { }); it('should return the path of a file relative to the workspace from the file\'s FileStat if the file is in the workspace', () => { - const file: FileStat = { - uri: 'file:///workspace/some/very-long/path.js', - lastModification: 0, - isDirectory: false - }; + const file: FileStat = FileStat.file('file:///workspace/some/very-long/path.js'); const longName = labelProvider.getLongName(file); expect(longName).eq('some/very-long/path.js'); }); @@ -186,11 +150,7 @@ describe('WorkspaceUriLabelProviderContribution class', () => { }); it('should return the absolute path of a file from the file\'s FileStat if the file is not in the workspace', () => { - const file: FileStat = { - uri: 'file:///tmp/prout.txt', - lastModification: 0, - isDirectory: false - }; + const file: FileStat = FileStat.file('file:///tmp/prout.txt'); const longName = labelProvider.getLongName(file); expect(longName).eq('/tmp/prout.txt'); }); diff --git a/packages/workspace/src/browser/workspace-uri-contribution.ts b/packages/workspace/src/browser/workspace-uri-contribution.ts index 795aa96725ec5..56ec868fbbcaa 100644 --- a/packages/workspace/src/browser/workspace-uri-contribution.ts +++ b/packages/workspace/src/browser/workspace-uri-contribution.ts @@ -17,7 +17,7 @@ import { DefaultUriLabelProviderContribution, URIIconReference } from '@theia/core/lib/browser/label-provider'; import URI from '@theia/core/lib/common/uri'; import { injectable, inject, postConstruct } from 'inversify'; -import { FileStat } from '@theia/filesystem/lib/common'; +import { FileStat } from '@theia/filesystem/lib/common/files'; import { WorkspaceVariableContribution } from './workspace-variable-contribution'; @injectable() @@ -57,14 +57,14 @@ export class WorkspaceUriLabelProviderContribution extends DefaultUriLabelProvid protected asURIIconReference(element: URI | URIIconReference | FileStat): URI | URIIconReference { if (FileStat.is(element)) { - return URIIconReference.create(element.isDirectory ? 'folder' : 'file', new URI(element.uri)); + return URIIconReference.create(element.isDirectory ? 'folder' : 'file', element.resource); } return element; } protected getUri(element: URI | URIIconReference | FileStat): URI | undefined { if (FileStat.is(element)) { - return new URI(element.uri); + return element.resource; } return super.getUri(element); } diff --git a/packages/workspace/src/browser/workspace-utils.ts b/packages/workspace/src/browser/workspace-utils.ts index 9cbd45905a24d..61204a6a73bd4 100644 --- a/packages/workspace/src/browser/workspace-utils.ts +++ b/packages/workspace/src/browser/workspace-utils.ts @@ -15,6 +15,8 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +// TODO get rid of util files, replace with methods in a responsible class + import URI from '@theia/core/lib/common/uri'; import { inject, injectable } from 'inversify'; import { WorkspaceService } from './workspace-service'; @@ -36,7 +38,7 @@ export class WorkspaceUtils { */ containsRootDirectory(uris: URI[]): boolean { // obtain all roots URIs for a given workspace - const rootUris = this.workspaceService.tryGetRoots().map(root => new URI(root.uri)); + const rootUris = this.workspaceService.tryGetRoots().map(root => root.resource); // return true if at least a single URI is a root directory return rootUris.some(rootUri => uris.some(uri => uri.isEqualOrParent(rootUri))); } diff --git a/packages/workspace/src/browser/workspace-variable-contribution.ts b/packages/workspace/src/browser/workspace-variable-contribution.ts index a0646284ad2cf..a9055818029cf 100644 --- a/packages/workspace/src/browser/workspace-variable-contribution.ts +++ b/packages/workspace/src/browser/workspace-variable-contribution.ts @@ -17,10 +17,10 @@ import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { Path } from '@theia/core/lib/common/path'; -import { FileSystem } from '@theia/filesystem/lib/common'; import { ApplicationShell, NavigatableWidget, WidgetManager } from '@theia/core/lib/browser'; import { VariableContribution, VariableRegistry, Variable } from '@theia/variable-resolver/lib/browser'; import { WorkspaceService } from './workspace-service'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; @injectable() export class WorkspaceVariableContribution implements VariableContribution { @@ -29,8 +29,8 @@ export class WorkspaceVariableContribution implements VariableContribution { protected readonly workspaceService: WorkspaceService; @inject(ApplicationShell) protected readonly shell: ApplicationShell; - @inject(FileSystem) - protected readonly fileSystem: FileSystem; + @inject(FileService) + protected readonly fileService: FileService; @inject(WidgetManager) protected readonly widgetManager: WidgetManager; @@ -100,7 +100,7 @@ export class WorkspaceVariableContribution implements VariableContribution { description: 'The path of the currently opened file', resolve: () => { const uri = this.getResourceUri(); - return uri && this.fileSystem.getFsPath(uri.toString()); + return uri && this.fileService.fsPath(uri); } }); variables.registerVariable({ @@ -142,8 +142,8 @@ export class WorkspaceVariableContribution implements VariableContribution { name: variable.name, description: variable.description, resolve: (context, workspaceRootName) => { - const workspaceRoot = workspaceRootName && this.workspaceService.tryGetRoots().find(r => new URI(r.uri).path.name === workspaceRootName); - return variable.resolve(workspaceRoot ? new URI(workspaceRoot.uri) : context); + const workspaceRoot = workspaceRootName && this.workspaceService.tryGetRoots().find(r => r.resource.path.name === workspaceRootName); + return variable.resolve(workspaceRoot ? workspaceRoot.resource : context); } }); variables.registerVariable(scoped({ @@ -151,7 +151,7 @@ export class WorkspaceVariableContribution implements VariableContribution { description: 'The path of the workspace root folder', resolve: (context?: URI) => { const uri = this.getWorkspaceRootUri(context); - return uri && this.fileSystem.getFsPath(uri.toString()); + return uri && this.fileService.fsPath(uri); } })); variables.registerVariable(scoped({ @@ -159,7 +159,7 @@ export class WorkspaceVariableContribution implements VariableContribution { description: 'The path of the workspace root folder', resolve: (context?: URI) => { const uri = this.getWorkspaceRootUri(context); - return uri && this.fileSystem.getFsPath(uri.toString()); + return uri && this.fileService.fsPath(uri); } })); variables.registerVariable(scoped({ @@ -183,7 +183,7 @@ export class WorkspaceVariableContribution implements VariableContribution { description: "The task runner's current working directory on startup", resolve: (context?: URI) => { const uri = this.getWorkspaceRootUri(context); - return (uri && this.fileSystem.getFsPath(uri.toString())) || ''; + return (uri && this.fileService.fsPath(uri)) || ''; } })); variables.registerVariable(scoped({ diff --git a/packages/workspace/src/common/utils.ts b/packages/workspace/src/common/utils.ts index 6288e0e80542c..bdc0b6682aa5b 100644 --- a/packages/workspace/src/common/utils.ts +++ b/packages/workspace/src/common/utils.ts @@ -14,12 +14,17 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +// TODO get rid of util files, replace with methods in a responsible class + import URI from '@theia/core/lib/common/uri'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; export const THEIA_EXT = 'theia-workspace'; export const VSCODE_EXT = 'code-workspace'; +/** + * @deprecated since 1.4.0 - because of https://github.com/eclipse-theia/theia/wiki/Coding-Guidelines#di-function-export, use `WorksapceService.getUntitledWorkspace` instead + */ export async function getTemporaryWorkspaceFileUri(envVariableServer: EnvVariablesServer): Promise { const configDirUri = await envVariableServer.getConfigDirUri(); return new URI(configDirUri).resolve(`Untitled.${THEIA_EXT}`); diff --git a/yarn.lock b/yarn.lock index 0d64a0e052ad0..4b8fb3e1462e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1150,6 +1150,11 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== +"@stroncium/procfs@^1.0.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@stroncium/procfs/-/procfs-1.2.1.tgz#6b9be6fd20fb0a4c20e99a8695e083c699bb2b45" + integrity sha512-X1Iui3FUNZP18EUvysTHxt+Avu2nlVzyf90YM8OYgP6SGzTzzX/0JgObfO1AQQDzuZtNNz29bVh8h5R97JrjxA== + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -1403,7 +1408,7 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== -"@types/minimatch@*", "@types/minimatch@3.0.3": +"@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== @@ -1528,6 +1533,13 @@ resolved "https://registry.yarnpkg.com/@types/route-parser/-/route-parser-0.1.3.tgz#f8af16886ebe0b525879628c04f81433ac617af0" integrity sha512-1AQYpsMbxangSnApsyIHzck5TP8cfas8fzmemljLi2APssJvlZiHkTar/ZtcZwOtK/Ory/xwLg2X8dwhkbnM+g== +"@types/safer-buffer@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/safer-buffer/-/safer-buffer-2.1.0.tgz#8c498815fe00af8f8b23d63eb3fd6fae6ae2ab7a" + integrity sha512-04WlrCdOLy1Ejpwc3A7qyZzsH6uqeWoH+XO80V8S8NRubGg+E4FMMM3VAS6jZZ8w+dXki1/5FI5upmMDQlaQsQ== + dependencies: + "@types/node" "*" + "@types/sanitize-html@^1.13.31": version "1.20.2" resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-1.20.2.tgz#59777f79f015321334e3a9f28882f58c0a0d42b8" @@ -1598,11 +1610,6 @@ dependencies: "@types/node" "*" -"@types/touch@0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@types/touch/-/touch-0.0.1.tgz#10289d42e80530f3997f3413eab1ac6ef9027d0c" - integrity sha1-ECidQugFMPOZfzQT6rGsbvkCfQw= - "@types/tough-cookie@*": version "2.3.6" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5" @@ -1669,55 +1676,56 @@ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-11.1.3.tgz#33c8ebf05f78f1edeb249c1cde1a42ae57f5664e" integrity sha512-moBUF6X8JsK5MbLZGP3vCfG/TVHZHsaePj3EimlLKp8+ESUjGjpXalxyn90a2L9fTM2ZGtW4swb6Am1DvVRNGA== -"@typescript-eslint/eslint-plugin-tslint@^2.16.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-2.16.0.tgz#8e8573cc2560ddae5a8a30fc2eb6af10f1ff9658" - integrity sha512-F+In2z6VCWiI0J4P4OzUQY9UzT5y/0Xg6bHM6twoK5XmPdbR7zT9JzIZKLcXyr80XT5LbgHjnW6oPGp1O3W36g== +"@typescript-eslint/eslint-plugin-tslint@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin-tslint/-/eslint-plugin-tslint-3.1.0.tgz#bb40bb135240c776df7e9780e46efaff53981128" + integrity sha512-zeQkzrl85jf0F/mCtcgdWEB/GH9mEhLLij8gyva5QGVM7EfVvghzOLQ08tOr3ZhmzlB4Bj38ZU2NmZS6PJZrsQ== dependencies: - "@typescript-eslint/experimental-utils" "2.16.0" + "@typescript-eslint/experimental-utils" "3.1.0" lodash "^4.17.15" -"@typescript-eslint/eslint-plugin@^2.16.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.16.0.tgz#bf339b7db824c7cc3fd1ebedbc88dd17016471af" - integrity sha512-TKWbeFAKRPrvKiR9GNxErQ8sELKqg1ZvXi6uho07mcKShBnCnqNpDQWP01FEvWKf0bxM2g7uQEI5MNjSNqvUpQ== +"@typescript-eslint/eslint-plugin@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz#4ac00ecca3bbea740c577f1843bc54fa69c3def2" + integrity sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg== dependencies: - "@typescript-eslint/experimental-utils" "2.16.0" - eslint-utils "^1.4.3" + "@typescript-eslint/experimental-utils" "3.1.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" + semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.16.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.16.0.tgz#bba65685728c532e0ddc811a0376e8d38e671f77" - integrity sha512-bXTmAztXpqxliDKZgvWkl+5dHeRN+jqXVZ16peKKFzSXVzT6mz8kgBpHiVzEKO2NZ8OCU7dG61K9sRS/SkUUFQ== +"@typescript-eslint/experimental-utils@3.1.0", "@typescript-eslint/experimental-utils@^2.19.2 || ^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz#2d5dba7c2ac2a3da3bfa3f461ff64de38587a872" + integrity sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.16.0" + "@typescript-eslint/typescript-estree" "3.1.0" eslint-scope "^5.0.0" + eslint-utils "^2.0.0" -"@typescript-eslint/parser@^2.16.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.16.0.tgz#d0c0135a8fdb915f670802ddd7c1ba457c1b4f9d" - integrity sha512-+w8dMaYETM9v6il1yYYkApMSiwgnqXWJbXrA94LAWN603vXHACsZTirJduyeBOJjA9wT6xuXe5zZ1iCUzoxCfw== +"@typescript-eslint/parser@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.1.0.tgz#9c02ba5d88ad2355672f39e6cd4176f172dd47f8" + integrity sha512-NcDSJK8qTA2tPfyGiPes9HtVKLbksmuYjlgGAUs7Ld2K0swdWibnCq9IJx9kJN8JJdgUJSorFiGaPHBgH81F/Q== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.16.0" - "@typescript-eslint/typescript-estree" "2.16.0" + "@typescript-eslint/experimental-utils" "3.1.0" + "@typescript-eslint/typescript-estree" "3.1.0" eslint-visitor-keys "^1.1.0" -"@typescript-eslint/typescript-estree@2.16.0": - version "2.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.16.0.tgz#b444943a76c716ed32abd08cbe96172d2ca0ab75" - integrity sha512-hyrCYjFHISos68Bk5KjUAXw0pP/455qq9nxqB1KkT67Pxjcfw+r6Yhcmqnp8etFL45UexCHUMrADHH7dI/m2WQ== +"@typescript-eslint/typescript-estree@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz#eaff52d31e615e05b894f8b9d2c3d8af152a5dd2" + integrity sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" glob "^7.1.6" is-glob "^4.0.1" lodash "^4.17.15" - semver "^6.3.0" + semver "^7.3.2" tsutils "^3.17.1" "@webassemblyjs/ast@1.8.5": @@ -2297,7 +2305,7 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== -async@^1.5.0, async@^1.5.2: +async@^1.5.0: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= @@ -3030,13 +3038,6 @@ back@~0.1.5: resolved "https://registry.yarnpkg.com/back/-/back-0.1.5.tgz#342b96b804657b03ec9a31f248a11f200608dcc2" integrity sha1-NCuWuARlewPsmjHySKEfIAYI3MI= -backbone@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.4.0.tgz#54db4de9df7c3811c3f032f34749a4cd27f3bd12" - integrity sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ== - dependencies: - underscore ">=1.8.3" - balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" @@ -3144,6 +3145,15 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +bl@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.2.tgz#52b71e9088515d0606d9dd9cc7aa48dc1f98e73a" + integrity sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -3383,6 +3393,14 @@ buffer@^5.2.1: base64-js "^1.0.2" ieee754 "^1.1.4" +buffer@^5.5.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + buffers@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" @@ -4438,6 +4456,17 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cp-file@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d" + integrity sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA== + dependencies: + graceful-fs "^4.1.2" + make-dir "^2.0.0" + nested-error-stacks "^2.0.0" + pify "^4.0.1" + safe-buffer "^5.0.1" + create-ecdh@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" @@ -4771,6 +4800,13 @@ decompress-response@^3.2.0, decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" @@ -5090,16 +5126,15 @@ dot-prop@^5.0.0: dependencies: is-obj "^2.0.0" -drivelist@^6.4.3: - version "6.4.6" - resolved "https://registry.yarnpkg.com/drivelist/-/drivelist-6.4.6.tgz#3d092dd8b771fbcfda170784ba0d72db58c7554a" - integrity sha512-FVeQE8GQppabnXm5J3tz3+nNZUWBixLYl2jGuLnCI/LhpopOj6+/fvPMgaWXC/SW/gceVALCx/EBRk8HiXqB5w== +drivelist@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/drivelist/-/drivelist-9.0.2.tgz#8c7cd67b3710e4b282e7f0f35290009e63bc46e8" + integrity sha512-B68AttNDXsew8M2viM99I4sNMJ2T5/lZ2fDlVsEAAzaJ1pyZN2Ibhhd37LAqsNRPhhPtpyrCi9+C6AfJ2kwo4g== dependencies: bindings "^1.3.0" debug "^3.1.0" - fast-plist "^0.1.2" - nan "^2.10.0" - prebuild-install "^4.0.0" + nan "^2.14.0" + prebuild-install "^5.2.4" dugite-extra@0.1.14: version "0.1.14" @@ -5298,7 +5333,7 @@ encodeurl@^1.0.2, encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -end-of-stream@^1.0.0, end-of-stream@^1.1.0: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -5413,11 +5448,6 @@ escape-html@^1.0.3, escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-applescript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-applescript/-/escape-string-applescript-2.0.0.tgz#760bca838668e408fe5ee52ce42caf7cb46c5273" - integrity sha1-dgvKg4Zo5Aj+XuUs5CyvfLRsUnM= - escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -5456,6 +5486,15 @@ eslint-module-utils@^2.4.1: debug "^2.6.9" pkg-dir "^2.0.0" +eslint-plugin-deprecation@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-deprecation/-/eslint-plugin-deprecation-1.1.0.tgz#46793ebbec9555d167d59c851ae3da0a21532189" + integrity sha512-+oDa6JbdZXyh7Bx2zx7VoDFZvFnV1pZVPVo/bEGVkuXlLih/evX0LQG2/nSuNg83CmwZTcAFZXXpLgsX4ctIDQ== + dependencies: + "@typescript-eslint/experimental-utils" "^2.19.2 || ^3.0.0" + tslib "^1.10.0" + tsutils "^3.0.0" + eslint-plugin-import@^2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.0.tgz#d749a7263fb6c29980def8e960d380a6aa6aecaa" @@ -5502,6 +5541,13 @@ eslint-utils@^1.4.3: dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" + integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA== + dependencies: + eslint-visitor-keys "^1.1.0" + eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" @@ -5629,19 +5675,6 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -execa@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" - integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== - dependencies: - cross-spawn "^6.0.0" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/execa/-/execa-0.2.2.tgz#e2ead472c2c31aad6f73f1ac956eef45e12320cb" @@ -5737,10 +5770,10 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" -expand-template@^1.0.2: - version "1.1.1" - resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-1.1.1.tgz#981f188c0c3a87d2e28f559bc541426ff94f21dd" - integrity sha512-cebqLtV8KOZfw0UI8TEFWxtczxxC1jvyUvx6H4fyp1K1FN7A4Q+uggVUlOsI1K8AGU0rwOGqP8nCapdrw8CYQg== +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" @@ -6291,17 +6324,6 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" - integrity sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A= - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" - fs-extra@^4.0.1, fs-extra@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" @@ -6583,17 +6605,6 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^6.0.1: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -6786,7 +6797,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2: +graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== @@ -6808,7 +6819,7 @@ growl@1.10.5: resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== -handlebars@^4.0.2, handlebars@^4.5.3, handlebars@^4.7.0: +handlebars@^4.0.2, handlebars@^4.5.3: version "4.7.1" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.1.tgz#052bd2618964dcb8aebad0940bfeb2d8d1cfbfde" integrity sha512-2dd6soo60cwKNJ90VewNLIzdZPR/E2YhszOTgHpN9V0YuwZk7x33/iZoIBnASwDFVHMY7iJ6NPL8d9f/DWYCTA== @@ -6819,6 +6830,18 @@ handlebars@^4.0.2, handlebars@^4.5.3, handlebars@^4.7.0: optionalDependencies: uglify-js "^3.1.4" +handlebars@^4.7.6: + version "4.7.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" + integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -6948,7 +6971,12 @@ he@1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -highlight.js@^9.12.0, highlight.js@^9.17.1: +highlight.js@^10.0.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.1.1.tgz#691a2148a8d922bf12e52a294566a0d993b94c57" + integrity sha512-b4L09127uVa+9vkMgPpdUQP78ickGbHEQTWeBrQFTJZ4/n2aihWOGS0ZoUqAwjVmfjhq/C76HRzkqwZhK4sBbg== + +highlight.js@^9.12.0: version "9.17.1" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.17.1.tgz#14a4eded23fd314b05886758bb906e39dd627f9a" integrity sha512-TA2/doAur5Ol8+iM3Ov7qy3jYcr/QiJ2eDTdRF4dfbjG7AaaB99J5G+zSl11ljbl6cIcahgPY6SKb3sC3EJ0fw== @@ -7091,13 +7119,6 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -iconv-lite@0.4.23: - version "0.4.23" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" - integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -7105,6 +7126,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.0.tgz#66a93b80df0bd05d2a43a7426296b7f91073f125" + integrity sha512-43ZpGYZ9QtuutX5l6WC1DSO8ane9N+Ct5qPLF2OV7vM9abM69gnAbVkh66ibaZd3aOGkoP1ZmringlKhLBkw2Q== + dependencies: + safer-buffer ">= 2.1.2 < 3" + icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -7205,7 +7233,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7584,6 +7612,11 @@ is-observable@^0.2.0: dependencies: symbol-observable "^0.2.2" +is-path-inside@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== + is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -7816,11 +7849,6 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" -jquery@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" - integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== - js-base64@^2.1.9: version "2.5.1" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121" @@ -7862,10 +7890,10 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jschardet@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.6.0.tgz#c7d1a71edcff2839db2f9ec30fc5d5ebd3c1a678" - integrity sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ== +jschardet@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.1.1.tgz#af6f8fd0b3b0f5d46a8fd9614a4fce490575c184" + integrity sha512-pA5qG9Zwm8CBpGlK/lo2GE9jPxwqRgMV7Lzc/1iaPccw6v4Rhj8Zg2BTyrdmHmxlJojnbLupLeRnaPLsq03x6Q== jscodeshift@^0.4.0: version "0.4.1" @@ -8027,13 +8055,6 @@ jsonc-parser@^2.0.2: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.0.tgz#f206f87f9d49d644b7502052c04e82dd6392e9ef" integrity sha512-4fLQxW1j/5fWj6p78vAlAafoCKtuBm6ghv+Ij5W2DrDx0qE+ZdEl2c6Ko1mgJNF5ftX1iEWQQ4Ap7+3GlhjkOA== -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" - integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug= - optionalDependencies: - graceful-fs "^4.1.6" - jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -8056,13 +8077,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jszip@^2.4.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-2.6.1.tgz#b88f3a7b2e67a2a048152982c7a3756d9c4828f0" - integrity sha1-uI86ey5noqBIFSmCx6N1bZxIKPA= - dependencies: - pako "~1.0.2" - just-extend@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc" @@ -8116,13 +8130,6 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk= - optionalDependencies: - graceful-fs "^4.1.9" - kuler@1.0.x: version "1.0.1" resolved "https://registry.yarnpkg.com/kuler/-/kuler-1.0.1.tgz#ef7c784f36c9fb6e16dd3150d152677b2b0228a6" @@ -8672,10 +8679,10 @@ markdown-it@^8.4.0: mdurl "^1.0.1" uc.micro "^1.0.5" -marked@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.8.0.tgz#ec5c0c9b93878dc52dd54be8d0e524097bd81a99" - integrity sha512-MyUe+T/Pw4TZufHkzAfDj6HarCBWia2y27/bhuYkTaiUnfDYFnCP3KUN+9oM7Wi6JA2rymtVYbQu3spE0GCmxQ== +marked@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-1.0.0.tgz#d35784245a04871e5988a491e28867362e941693" + integrity sha512-Wo+L1pWTVibfrSr+TTtMuiMfNzmZWiOPeO7rZsQUY5bgsxpHesBEcIWJloWVTFnrMXnf/TL30eTFSGJddmQAng== matcher@^3.0.0: version "3.0.0" @@ -8900,6 +8907,11 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -8910,7 +8922,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -8940,7 +8952,7 @@ minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minimist@^1.2.5: +minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -9005,6 +9017,11 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -9078,6 +9095,15 @@ move-concurrently@^1.0.1: rimraf "^2.5.4" run-queue "^1.0.3" +move-file@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/move-file/-/move-file-1.2.0.tgz#789f92d276c62511d214b1b285aa16e015c2f2fc" + integrity sha512-USHrRmxzGowUWAGBbJPdFjHzEqtxDU03pLHY0Rfqgtnq+q8FOIs8wvkkf+Udmg77SJKs47y9sI0jJvQeYsmiCA== + dependencies: + cp-file "^6.1.0" + make-dir "^3.0.0" + path-exists "^3.0.0" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -9113,16 +9139,7 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -mv@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" - integrity sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI= - dependencies: - mkdirp "~0.5.1" - ncp "~2.0.0" - rimraf "~2.4.0" - -nan@^2.0.0, nan@^2.10.0, nan@^2.12.1, nan@^2.14.0: +nan@^2.0.0, nan@^2.12.1, nan@^2.14.0: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -9144,6 +9161,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + native-keymap@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-2.1.2.tgz#9773313f619d4c2b66b452cf036310a145523b59" @@ -9159,11 +9181,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -ncp@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" - integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -9174,6 +9191,11 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== +nested-error-stacks@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" + integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -9190,7 +9212,7 @@ nise@^1.0.1: lolex "^5.0.1" path-to-regexp "^1.7.0" -node-abi@^2.11.0, node-abi@^2.18.0, node-abi@^2.2.0: +node-abi@^2.11.0, node-abi@^2.18.0, node-abi@^2.7.0: version "2.18.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.18.0.tgz#1f5486cfd7d38bd4f5392fa44a4ad4d9a0dffbf4" integrity sha512-yi05ZoiuNNEbyT/xXfSySZE+yVnQW6fxPZuFbLyS1s6b5Kw3HzV2PHOM4XR+nsjzkHxByK+2Wg+yCQbe35l8dw== @@ -9326,13 +9348,6 @@ nopt@^4.0.1: abbrev "1" osenv "^0.1.4" -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= - dependencies: - abbrev "1" - normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -9683,7 +9698,7 @@ os-browserify@^0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= -os-homedir@^1.0.0, os-homedir@^1.0.1: +os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= @@ -9806,7 +9821,7 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" -p-map@^1.1.1, p-map@^1.2.0: +p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== @@ -9847,7 +9862,7 @@ p-try@^1.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= -p-try@^2.0.0: +p-try@^2.0.0, p-try@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== @@ -9872,7 +9887,7 @@ package-json@^4.0.1: registry-url "^3.0.3" semver "^5.1.0" -pako@~1.0.2, pako@~1.0.5: +pako@~1.0.5: version "1.0.10" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== @@ -10450,24 +10465,24 @@ postcss@^7.0.27: source-map "^0.6.1" supports-color "^6.1.0" -prebuild-install@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-4.0.0.tgz#206ce8106ce5efa4b6cf062fc8a0a7d93c17f3a8" - integrity sha512-7tayxeYboJX0RbVzdnKyGl2vhQRWr6qfClEXDhOkXjuaOKCw2q8aiuFhONRYVsG/czia7KhpykIlI2S2VaPunA== +prebuild-install@^5.2.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.4.tgz#6982d10084269d364c1856550b7d090ea31fa293" + integrity sha512-AkKN+pf4fSEihjapLEEj8n85YIw/tN6BQqkhzbDc0RvEZGdkpJBGMUYx66AAMcPG2KzmPQS7Cm16an4HVBRRMA== dependencies: detect-libc "^1.0.3" - expand-template "^1.0.2" + expand-template "^2.0.3" github-from-package "0.0.0" - minimist "^1.2.0" + minimist "^1.2.3" mkdirp "^0.5.1" - node-abi "^2.2.0" + napi-build-utils "^1.0.1" + node-abi "^2.7.0" noop-logger "^0.1.1" npmlog "^4.0.1" - os-homedir "^1.0.1" - pump "^2.0.1" - rc "^1.1.6" - simple-get "^2.7.0" - tar-fs "^1.13.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" tunnel-agent "^0.6.0" which-pm-runs "^1.0.0" @@ -10792,7 +10807,7 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -rc@^1.0.1, rc@^1.1.6, rc@^1.2.1: +rc@^1.0.1, rc@^1.1.6, rc@^1.2.1, rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -10958,6 +10973,15 @@ readable-stream@^3.1.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@~1.1.9: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -11406,13 +11430,6 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" -rimraf@~2.4.0: - version "2.4.5" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" - integrity sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto= - dependencies: - glob "^6.0.1" - ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -11438,13 +11455,6 @@ route-parser@^0.0.5: resolved "https://registry.yarnpkg.com/route-parser/-/route-parser-0.0.5.tgz#7d1d09d335e49094031ea16991a4a79b01bbe1f4" integrity sha1-fR0J0zXkkJQDHqFpkaSnmwG74fQ= -run-applescript@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-3.2.0.tgz#73fb34ce85d3de8076d511ea767c30d4fdfc918b" - integrity sha512-Ep0RsvAjnRcBX1p5vogbaBdAGu/8j/ewpvGqnQYunnLd9SM0vWcPJewPKNnWFggf0hF0pwIgwV5XK7qQ7UZ8Qg== - dependencies: - execa "^0.10.0" - run-async@^2.0.0, run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -11507,7 +11517,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@^2.1.2, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -11731,7 +11741,7 @@ shell-path@^2.1.0: dependencies: shell-env "^0.3.0" -shelljs@^0.8.0, shelljs@^0.8.3: +shelljs@^0.8.0: version "0.8.3" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A== @@ -11740,6 +11750,15 @@ shelljs@^0.8.0, shelljs@^0.8.3: interpret "^1.0.0" rechoir "^0.6.2" +shelljs@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + showdown@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/showdown/-/showdown-1.9.1.tgz#134e148e75cd4623e09c21b0511977d79b5ad0ef" @@ -11757,12 +11776,12 @@ simple-concat@^1.0.0: resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY= -simple-get@^2.7.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.1.tgz#0e22e91d4575d87620620bc91308d57a77f44b5d" - integrity sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw== +simple-get@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== dependencies: - decompress-response "^3.3.0" + decompress-response "^4.2.0" once "^1.3.1" simple-concat "^1.0.0" @@ -12429,7 +12448,7 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tar-fs@^1.13.0, tar-fs@^1.16.2: +tar-fs@^1.16.2: version "1.16.3" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw== @@ -12439,6 +12458,16 @@ tar-fs@^1.13.0, tar-fs@^1.16.2: pump "^1.0.0" tar-stream "^1.1.2" +tar-fs@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" + integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + tar-stream@^1.1.2, tar-stream@^1.5.2: version "1.6.2" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" @@ -12452,6 +12481,17 @@ tar-stream@^1.1.2, tar-stream@^1.5.2: to-buffer "^1.1.1" xtend "^4.0.0" +tar-stream@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.2.tgz#6d5ef1a7e5783a95ff70b69b97455a5968dc1325" + integrity sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q== + dependencies: + bl "^4.0.1" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar@^2.0.0: version "2.2.2" resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" @@ -12695,13 +12735,6 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -touch@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" - integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== - dependencies: - nopt "~1.0.10" - tough-cookie@^2.3.3, tough-cookie@^2.3.4: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -12725,19 +12758,19 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -trash@^4.0.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/trash/-/trash-4.3.0.tgz#6ebeecdea4d666b06e389b47d135ea88e1de5075" - integrity sha512-f36TKwIaBiXm63xSrn8OTNghg5CYHBsFVJvcObMo76LRpgariuRi2CqXQHw1VzfeximD0igdGaonOG6N760BtQ== +trash@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/trash/-/trash-6.1.1.tgz#8fb863421b31f32571f2650b53534934d5e63025" + integrity sha512-4i56lCmz2RG6WZN018hf4L75L5HboaFuKkHx3wDG/ihevI99e0OgFyl8w6G4ioqBm62V4EJqCy5xw3vQSNXU8A== dependencies: - escape-string-applescript "^2.0.0" - fs-extra "^0.30.0" + "@stroncium/procfs" "^1.0.0" globby "^7.1.1" - p-map "^1.2.0" - p-try "^1.0.0" - pify "^3.0.0" - run-applescript "^3.0.0" - uuid "^3.1.0" + is-path-inside "^3.0.2" + make-dir "^3.0.0" + move-file "^1.1.0" + p-map "^3.0.0" + p-try "^2.2.0" + uuid "^3.3.2" xdg-trashdir "^2.1.1" "traverse@>=0.3.0 <0.4": @@ -12794,6 +12827,11 @@ ts-md5@^1.2.2: resolved "https://registry.yarnpkg.com/ts-md5/-/ts-md5-1.2.7.tgz#b76471fc2fd38f0502441f6c3b9494ed04537401" integrity sha512-emODogvKGWi1KO1l9c6YxLMBn6CEH3VrH5mVPIyOtxBG52BvV4jP3GWz6bOZCz61nLgBc3ffQYE4+EHfCD+V7w== +tslib@^1.10.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" @@ -12825,7 +12863,7 @@ tsutils@^2.29.0: dependencies: tslib "^1.8.1" -tsutils@^3.17.1: +tsutils@^3.0.0, tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== @@ -12901,42 +12939,33 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typedoc-default-themes@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.6.3.tgz#c214ce5bbcc6045558448a8fd422b90e3e9b6782" - integrity sha512-rouf0TcIA4M2nOQFfC7Zp4NEwoYiEX4vX/ZtudJWU9IHA29MPC+PPgSXYLPESkUo7FuB//GxigO3mk9Qe1xp3Q== +typedoc-default-themes@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.10.1.tgz#eb27b7d689457c7ec843e47ec0d3e500581296a7" + integrity sha512-SuqAQI0CkwhqSJ2kaVTgl37cWs733uy9UGUqwtcds8pkFK8oRF4rZmCq+FXTGIb9hIUOu40rf5Kojg0Ha6akeg== dependencies: - backbone "^1.4.0" - jquery "^3.4.1" lunr "^2.3.8" - underscore "^1.9.1" -typedoc-plugin-external-module-map@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/typedoc-plugin-external-module-map/-/typedoc-plugin-external-module-map-1.0.0.tgz#7021d0e2bc9a98b7266f4ea2eab593b7c63802ce" - integrity sha512-OtlTOmanX0yqRYUVLBuGSBjrffLLAjWNn8mqh6k6FkvfXAIIe3Yfg0kCeKZDN/65v4dt3MJ9AuGXTGLPue3Kqg== +typedoc-plugin-external-module-map@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/typedoc-plugin-external-module-map/-/typedoc-plugin-external-module-map-1.2.1.tgz#32669a6b81e57962d2dae80d7a6ef8f5d0be65dd" + integrity sha512-ha+he4JFhCufF6wnpMpeH2XwsMgnYR6IrRUBCiMbZoYoudn6zICX7NA40pMjA35A6afxWNhKZU19pXnvysPK7A== -typedoc@^0.15.0-0: - version "0.15.8" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.15.8.tgz#d83195445a718d173e0d5c73b5581052cb47d4d9" - integrity sha512-a0zypcvfIFsS7Gqpf2MkC1+jNND3K1Om38pbDdy/gYWX01NuJZhC5+O0HkIp0oRIZOo7PWrA5+fC24zkANY28Q== +typedoc@^0.17.7: + version "0.17.7" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.17.7.tgz#70797401140403a5f91589ed3f4f24c03841bf7a" + integrity sha512-PEnzjwQAGjb0O8a6VDE0lxyLAadqNujN5LltsTUhZETolRMiIJv6Ox+Toa8h0XhKHqAOh8MOmB0eBVcWz6nuAw== dependencies: - "@types/minimatch" "3.0.3" fs-extra "^8.1.0" - handlebars "^4.7.0" - highlight.js "^9.17.1" + handlebars "^4.7.6" + highlight.js "^10.0.0" lodash "^4.17.15" - marked "^0.8.0" + lunr "^2.3.8" + marked "1.0.0" minimatch "^3.0.0" progress "^2.0.3" - shelljs "^0.8.3" - typedoc-default-themes "^0.6.3" - typescript "3.7.x" - -typescript@3.7.x: - version "3.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19" - integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw== + shelljs "^0.8.4" + typedoc-default-themes "^0.10.1" typescript@^3.9.2: version "3.9.2" @@ -12978,11 +13007,6 @@ unbzip2-stream@^1.0.9: buffer "^5.2.1" through "^2.3.8" -underscore@>=1.8.3, underscore@^1.9.1: - version "1.9.2" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.2.tgz#0c8d6f536d6f378a5af264a72f7bec50feb7cf2f" - integrity sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ== - underscore@~1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" @@ -13195,7 +13219,7 @@ uuid@^2.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= -uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.3.3: +uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== @@ -13315,6 +13339,11 @@ vscode-languageserver-protocol@^3.15.0-next.8: vscode-jsonrpc "^5.0.0-next.7" vscode-languageserver-types "^3.15.0-next.10" +vscode-languageserver-textdocument@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f" + integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA== + vscode-languageserver-types@^3.15.0-next, vscode-languageserver-types@^3.15.0-next.10: version "3.15.0-next.10" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.0-next.10.tgz#fcd22bb5e8415f52134cf9eb1a5d90778558bc5c" @@ -13556,6 +13585,11 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" @@ -13988,11 +14022,3 @@ yeoman-generator@^2.0.3: text-table "^0.2.0" through2 "^2.0.0" yeoman-environment "^2.0.5" - -zip-dir@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/zip-dir/-/zip-dir-1.0.2.tgz#253f907aead62a21acd8721d8b88032b2411c051" - integrity sha1-JT+QeurWKiGs2HIdi4gDKyQRwFE= - dependencies: - async "^1.5.2" - jszip "^2.4.0"