From c666a5f721b05ac2e63ade5243fb306c6f143c8b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 29 Apr 2025 00:07:20 -0700 Subject: [PATCH] COPY FROM voideditor-test/void (void-test)!!!! --- .config/guardian/.gdnsuppress | 46 + .devcontainer/README.md | 2 +- .eslint-ignore | 3 +- .eslint-plugin-local/code-amd-node-module.ts | 2 +- .../code-declare-service-brand.ts | 2 +- ...code-ensure-no-disposables-leak-in-test.ts | 2 +- .eslint-plugin-local/code-import-patterns.ts | 2 +- .../code-limited-top-functions.ts | 10 +- .eslint-plugin-local/code-must-use-result.ts | 8 +- .../code-must-use-super-dispose.ts | 2 +- .../code-no-dangerous-type-assertions.ts | 2 +- .../code-no-global-document-listener.ts | 2 +- .../code-no-nls-in-standalone-editor.ts | 2 +- .../code-no-runtime-import.ts | 8 +- .../code-no-standalone-editor.ts | 2 +- .../code-no-static-self-ref.ts | 6 +- .../code-no-unused-expressions.ts | 8 +- .eslint-plugin-local/index.js | 4 + .../vscode-dts-string-type-literals.ts | 4 +- .gitignore | 5 - .npmrc | 2 +- .vscode-test.js | 25 +- .vscode/launch.json | 61 +- .vscode/notebooks/api.github-issues | 2 +- .vscode/notebooks/endgame.github-issues | 2 +- .vscode/notebooks/my-endgame.github-issues | 2 +- .vscode/notebooks/my-work.github-issues | 2 +- .vscode/settings.json | 2 + .vscode/tasks.json | 6 +- HOW_TO_CONTRIBUTE.md | 37 +- README.md | 16 +- ThirdPartyNotices.txt | 53 +- VOID_CODEBASE_GUIDE.md | 14 +- build/.moduleignore | 1 + build/.npmrc | 1 + build/azure-pipelines/cli/cli-compile.yml | 23 +- .../cli/install-rust-posix.yml | 2 +- .../cli/install-rust-win32.yml | 2 +- build/azure-pipelines/common/publish.js | 12 +- build/azure-pipelines/common/publish.ts | 10 +- .../darwin/product-build-darwin-test.yml | 8 +- .../darwin/product-build-darwin.yml | 5 + .../product-build-linux-legacy-server.yml | 233 - build/azure-pipelines/linux/setup-env.sh | 82 +- build/azure-pipelines/product-build.yml | 81 +- build/azure-pipelines/product-compile.yml | 4 +- build/azure-pipelines/product-publish.yml | 1 - .../win32/product-build-win32-test.yml | 4 +- .../win32/product-build-win32.yml | 57 +- .../azure-pipelines/win32/sdl-scan-win32.yml | 31 +- build/buildfile.js | 29 +- build/checksums/electron.txt | 2 +- build/checksums/nodejs.txt | 14 +- build/checksums/vscode-sysroot.txt | 3 - build/darwin/create-universal-app.js | 1 + build/darwin/create-universal-app.ts | 1 + build/darwin/sign.js | 6 +- build/darwin/sign.ts | 6 +- build/filters.js | 8 +- build/gulpfile.compile.js | 16 +- build/gulpfile.editor.js | 237 +- build/gulpfile.extensions.js | 4 - build/gulpfile.reh.js | 32 +- build/gulpfile.vscode.js | 34 +- build/gulpfile.vscode.linux.js | 47 +- build/gulpfile.vscode.web.js | 7 +- build/gulpfile.vscode.win32.js | 5 +- build/lib/builtInExtensions.js | 5 +- build/lib/builtInExtensions.ts | 4 +- build/lib/bundle.js | 430 - build/lib/bundle.ts | 618 - build/lib/compilation.js | 1 + build/lib/compilation.ts | 2 +- build/lib/electron.js | 4 +- build/lib/electron.ts | 4 +- build/lib/i18n.js | 5 +- build/lib/i18n.ts | 5 +- build/lib/layersChecker.js | 5 +- build/lib/layersChecker.ts | 5 +- build/lib/optimize.js | 7 - build/lib/optimize.ts | 8 - build/lib/policies.js | 447 +- build/lib/policies.ts | 485 +- build/lib/propertyInitOrderChecker.js | 367 + build/lib/propertyInitOrderChecker.ts | 417 + build/lib/standalone.js | 100 +- build/lib/standalone.ts | 124 +- .../lib/stylelint/vscode-known-variables.json | 20 +- build/linux/dependencies-generator.js | 2 +- build/linux/dependencies-generator.ts | 2 +- build/linux/rpm/dep-lists.js | 3 + build/linux/rpm/dep-lists.ts | 3 + build/monaco/monaco.d.ts.recipe | 4 +- build/monaco/monaco.usage.recipe | 6 +- build/npm/postinstall.js | 2 +- build/package-lock.json | 231 +- build/package.json | 4 +- build/win32/Cargo.lock | 2 +- build/win32/Cargo.toml | 2 +- build/win32/inno_updater.exe | Bin 437760 -> 455168 bytes cgmanifest.json | 12 +- cli/ThirdPartyNotices.txt | 18 +- cli/src/auth.rs | 4 +- cli/src/log.rs | 2 +- cli/src/tunnels/singleton_server.rs | 2 +- cli/src/util/io.rs | 3 + cli/src/util/prereqs.rs | 36 +- eslint.config.js | 11 +- extensions/configuration-editing/package.json | 5 + extensions/csharp/language-configuration.json | 14 +- .../css-language-features/package-lock.json | 78 +- extensions/css-language-features/package.json | 2 +- .../server/package-lock.json | 8 +- .../css-language-features/server/package.json | 4 +- .../debug-server-ready/src/extension.ts | 11 +- extensions/git/package-lock.json | 6 - extensions/git/package.json | 15 +- extensions/git/package.nls.json | 1 + extensions/git/src/api/git.d.ts | 1 + extensions/git/src/blame.ts | 90 +- extensions/git/src/commands.ts | 224 +- extensions/git/src/decorators.ts | 2 +- extensions/git/src/git.ts | 197 +- extensions/git/src/historyProvider.ts | 2 +- extensions/git/src/repository.ts | 85 +- extensions/git/src/staging.ts | 9 +- extensions/git/src/timelineProvider.ts | 1 - extensions/git/tsconfig.json | 1 - .../html-language-features/package-lock.json | 82 +- .../html-language-features/package.json | 2 +- .../server/package-lock.json | 60 +- .../server/package.json | 8 +- extensions/ipynb/package.json | 5 +- extensions/ipynb/src/constants.ts | 2 +- extensions/ipynb/src/helper.ts | 18 +- extensions/ipynb/src/ipynbMain.ts | 5 +- .../src/test/notebookModelStoreSync.test.ts | 5 +- .../javascript-language-configuration.json | 2 +- extensions/javascript/syntaxes/Readme.md | 2 +- .../client/src/jsonClient.ts | 52 +- .../client/src/languageStatus.ts | 6 +- .../json-language-features/package-lock.json | 82 +- .../json-language-features/package.json | 5 +- .../server/package-lock.json | 52 +- .../server/package.json | 6 +- .../server/src/jsonServer.ts | 22 +- extensions/mangle-loader.js | 2 +- .../package-lock.json | 51 +- .../markdown-language-features/package.json | 4 +- .../preview-src/index.ts | 143 +- .../package-lock.json | 6 +- .../microsoft-authentication/package.json | 20 +- .../microsoft-authentication/package.nls.json | 3 - .../src/common/accountAccess.ts | 108 +- .../src/common/cachePlugin.ts | 2 +- .../src/common/publicClientCache.ts | 9 +- .../src/common/scopeData.ts | 52 +- .../src/common/test/scopeData.test.ts | 17 + .../src/extensionV2.ts | 6 +- .../src/node/authProvider.ts | 110 +- .../src/node/cachedPublicClientApplication.ts | 102 +- .../src/node/flows.ts | 7 +- .../src/node/publicClientCache.ts | 166 +- extensions/open-remote-ssh/package.json | 4 +- .../open-remote-ssh/src/serverConfig.ts | 4 +- extensions/open-remote-ssh/src/serverSetup.ts | 2 +- extensions/open-remote-wsl/package.json | 2 +- .../open-remote-wsl/src/serverConfig.ts | 4 +- extensions/open-remote-wsl/src/serverSetup.ts | 2 +- extensions/terminal-suggest/.vscodeignore | 3 + .../terminal-suggest/ThirdPartyNotices.txt | 2 - extensions/terminal-suggest/cgmanifest.json | 57 + extensions/terminal-suggest/package.json | 6 +- .../scripts/pullFishBuiltins.ts | 260 + .../scripts/pullZshBuiltins.ts | 19 +- .../scripts/terminalScriptHelpers.ts | 51 + .../terminal-suggest/scripts/update-specs.js | 31 +- .../src/completions/code-insiders.ts | 7 +- .../src/completions/code-tunnel-insiders.ts | 23 + .../src/completions/code-tunnel.ts | 94 + .../terminal-suggest/src/completions/code.ts | 680 +- .../terminal-suggest/src/completions/npx.ts | 145 + .../src/completions/upstream/apt.ts | 5 +- .../src/completions/upstream/brew.ts | 3292 ++-- .../src/completions/upstream/cat.ts | 78 +- .../src/completions/upstream/chmod.ts | 166 +- .../src/completions/upstream/chown.ts | 218 +- .../src/completions/upstream/cp.ts | 150 +- .../src/completions/upstream/curl.ts | 1820 +- .../src/completions/upstream/df.ts | 122 +- .../src/completions/upstream/du.ts | 180 +- .../src/completions/upstream/echo.ts | 70 +- .../src/completions/upstream/find.ts | 134 +- .../src/completions/upstream/git.ts | 185 +- .../src/completions/upstream/grep.ts | 680 +- .../src/completions/upstream/head.ts | 62 +- .../src/completions/upstream/killall.ts | 290 +- .../src/completions/upstream/less.ts | 560 +- .../src/completions/upstream/mkdir.ts | 66 +- .../src/completions/upstream/more.ts | 102 +- .../src/completions/upstream/mv.ts | 72 +- .../src/completions/upstream/nano.ts | 10 +- .../src/completions/upstream/node.ts | 833 +- .../src/completions/upstream/npm.ts | 142 +- .../src/completions/upstream/npx.ts | 317 - .../src/completions/upstream/nvm.ts | 4 - .../src/completions/upstream/pnpm.ts | 12 +- .../src/completions/upstream/ps.ts | 298 +- .../src/completions/upstream/pwd.ts | 24 +- .../src/completions/upstream/python.ts | 1 + .../src/completions/upstream/rm.ts | 76 +- .../src/completions/upstream/rmdir.ts | 26 +- .../src/completions/upstream/scp.ts | 424 +- .../src/completions/upstream/ssh.ts | 6 +- .../src/completions/upstream/tail.ts | 32 +- .../src/completions/upstream/top.ts | 90 +- .../src/completions/upstream/touch.ts | 110 +- .../src/completions/upstream/uname.ts | 64 +- .../src/completions/upstream/vim.ts | 480 +- .../src/completions/upstream/wget.ts | 790 +- .../src/completions/upstream/yarn.ts | 14 +- extensions/terminal-suggest/src/constants.ts | 1 - .../src/env/pathExecutableCache.ts | 55 +- .../fig/autocomplete-parser/parseArguments.ts | 17 +- .../generators/scriptSuggestionsGenerator.ts | 3 +- .../terminal-suggest/src/fig/execute.ts | 2 +- .../terminal-suggest/src/fig/figInterface.ts | 39 +- .../terminal-suggest/src/helpers/promise.ts | 8 + extensions/terminal-suggest/src/shell/bash.ts | 3 +- extensions/terminal-suggest/src/shell/fish.ts | 78 +- .../src/shell/fishBuiltinsCache.ts | 301 + extensions/terminal-suggest/src/shell/zsh.ts | 3 +- .../src/terminalSuggestMain.ts | 145 +- .../src/test/completions/code.test.ts | 66 +- .../src/test/completions/upstream/ls.test.ts | 2 +- .../test/completions/upstream/mkdir.test.ts | 8 +- .../test/completions/upstream/touch.test.ts | 6 +- .../src/test/env/pathExecutableCache.test.ts | 9 + .../terminal-suggest/src/test/fig.test.ts | 2 +- .../src/test/terminalSuggestMain.test.ts | 23 +- extensions/terminal-suggest/src/tokens.ts | 15 +- .../language-configuration.json | 2 +- .../typescript-basics/syntaxes/Readme.md | 2 +- .../typescript-language-features/package.json | 6 - .../package.nls.json | 1 - .../src/filesystems/autoInstallerFs.ts | 12 +- .../src/ui/intellisenseStatus.ts | 10 +- .../src/ui/versionStatus.ts | 2 +- .../src/singlefolder-tests/chat.test.ts | 2 +- .../src/singlefolder-tests/terminal.test.ts | 4 +- .../src/colorizer.test.ts | 8 +- .../colorize-fixtures/test-issue241715.ts | 47 + .../test/colorize-fixtures/test.css | 6 +- .../test/colorize-fixtures/test.regexp.ts | 6 + .../colorize-results/test-issue241715_ts.json | 6876 +++++++ .../test/colorize-results/test_css.json | 308 + .../test/colorize-results/test_regexp.ts.json | 1598 ++ .../test-241001_ts.json | 398 +- .../test-function-inv_ts.json | 36 +- .../test-issue11_ts.json | 452 +- .../test-issue241715_ts.json | 4692 +++++ .../test-issue5431_ts.json | 74 +- .../test-issue5465_ts.json | 66 +- .../test-issue5566_ts.json | 50 +- .../test-jsdoc-multiline-type_ts.json | 36 +- .../test-keywords_ts.json | 28 +- .../test-members_ts.json | 44 +- .../test-object-literals_ts.json | 34 +- .../test-strings_ts.json | 96 +- .../test-this_ts.json | 16 +- .../test-variables_css.json | 507 +- .../test_css.json | 7773 +++++++- .../test_regexp.ts.json | 2144 +++ .../colorize-tree-sitter-results/test_ts.json | 1458 +- package-lock.json | 9282 ++++------ package.json | 86 +- product.json | 16 +- remote/.npmrc | 4 +- remote/package-lock.json | 156 +- remote/package.json | 29 +- remote/web/package-lock.json | 96 +- remote/web/package.json | 20 +- resources/linux/code.desktop | 1 + resources/server/bin/code-server-linux.sh | 10 + .../check-requirements-linux-legacy.sh | 9 - .../bin/helpers/check-requirements-linux.sh | 15 +- src/bootstrap-fork.ts | 3 - src/main.ts | 35 +- src/tsconfig.json | 1 - src/tsec.exemptions.json | 10 +- src/vs/amdX.ts | 2 +- src/vs/base/browser/contextmenu.ts | 4 + src/vs/base/browser/dnd.ts | 25 +- src/vs/base/browser/fonts.ts | 35 +- src/vs/base/browser/markdownRenderer.ts | 4 +- src/vs/base/browser/ui/button/button.ts | 36 +- .../browser/ui/codicons/codicon/codicon.ttf | Bin 84152 -> 84220 bytes .../browser/ui/contextview/contextview.ts | 4 +- src/vs/base/browser/ui/dialog/dialog.css | 38 +- src/vs/base/browser/ui/dialog/dialog.ts | 86 +- src/vs/base/browser/ui/dnd/dnd.css | 28 + src/vs/base/browser/ui/dnd/dnd.ts | 31 + src/vs/base/browser/ui/dropdown/dropdown.ts | 6 + src/vs/base/browser/ui/hover/hover.ts | 56 +- .../base/browser/ui/hover/hoverDelegate2.ts | 2 +- .../base/browser/ui/iconLabel/iconlabel.css | 5 - src/vs/base/browser/ui/inputbox/inputBox.ts | 2 +- src/vs/base/browser/ui/list/list.css | 10 - src/vs/base/browser/ui/list/listView.ts | 47 +- src/vs/base/browser/ui/list/listWidget.ts | 8 +- src/vs/base/browser/ui/menu/menu.ts | 1 + .../ui/severityIcon}/media/severityIcon.css | 0 .../browser/ui/severityIcon}/severityIcon.ts | 6 +- src/vs/base/browser/ui/splitview/paneview.ts | 9 +- ...ltWorkerFactory.ts => webWorkerFactory.ts} | 103 +- src/vs/base/common/arrays.ts | 30 + src/vs/base/common/codecs/baseDecoder.ts | 6 +- src/vs/base/common/diff/diff.ts | 176 + src/vs/base/common/envfile.ts | 100 + src/vs/base/common/hotReload.ts | 6 +- src/vs/base/common/htmlContent.ts | 15 +- src/vs/base/common/iterator.ts | 8 + src/vs/base/common/lifecycle.ts | 16 + src/vs/base/common/marshallingIds.ts | 1 + src/vs/base/common/mime.ts | 1 + src/vs/base/common/network.ts | 10 +- src/vs/base/common/numbers.ts | 2 +- src/vs/base/common/objects.ts | 35 - .../base/common/observableInternal/autorun.ts | 67 +- src/vs/base/common/observableInternal/base.ts | 52 +- .../base/common/observableInternal/derived.ts | 63 +- .../base/common/observableInternal/index.ts | 7 + .../logging/debugger/debuggerApi.d.ts | 7 + .../logging/debugger/devToolsLogger.ts | 229 +- .../base/common/observableInternal/utils.ts | 40 +- src/vs/base/common/policy.ts | 34 + src/vs/base/common/product.ts | 41 +- src/vs/base/common/strings.ts | 16 +- .../worker/{simpleWorker.ts => webWorker.ts} | 151 +- ...rkerBootstrap.ts => webWorkerBootstrap.ts} | 14 +- src/vs/base/node/powershell.ts | 12 + src/vs/base/node/processes.ts | 26 +- src/vs/base/parts/ipc/common/ipc.mp.ts | 14 +- src/vs/base/parts/ipc/node/ipc.mp.ts | 14 +- src/vs/base/parts/storage/node/storage.ts | 15 +- .../test/browser/markdownRenderer.test.ts | 2 +- src/vs/base/test/common/arrays.test.ts | 59 + src/vs/base/test/common/date.test.ts | 2 + src/vs/base/test/common/envfile.test.ts | 130 + src/vs/base/test/common/lifecycle.test.ts | 28 +- .../base/test/common/markdownString.test.ts | 8 +- src/vs/base/test/common/numbers.test.ts | 20 +- src/vs/base/test/common/strings.test.ts | 328 +- src/vs/base/worker/workerMain.ts | 50 - src/vs/code/browser/workbench/workbench.ts | 58 +- src/vs/code/electron-main/app.ts | 70 +- src/vs/code/electron-main/main.ts | 13 +- .../electron-sandbox/workbench/workbench.ts | 16 +- .../sharedProcess/contrib/codeCacheCleaner.ts | 16 +- .../contrib/languagePackCachedDataCleaner.ts | 18 +- .../sharedProcess/sharedProcessMain.ts | 14 +- src/vs/code/node/cli.ts | 1 + src/vs/code/node/cliProcessMain.ts | 24 +- .../editor/browser/config/migrateOptions.ts | 7 + .../editContext/native/nativeEditContext.css | 4 +- .../editContext/native/nativeEditContext.ts | 45 +- .../native/nativeEditContextUtils.ts | 37 +- .../editContext/native/screenReaderSupport.ts | 32 +- src/vs/editor/browser/rect.ts | 62 +- .../browser/services/editorWorkerService.ts | 32 +- .../services/hoverService/hoverService.ts | 45 +- src/vs/editor/browser/view.ts | 79 +- .../widget/codeEditor/codeEditorWidget.ts | 102 +- .../diffEditorViewZones/renderLines.ts | 17 + .../browser/widget/diffEditor/style.css | 3 +- .../codecs/markdownCodec/markdownDecoder.ts | 274 +- .../markdownCodec/parsers/markdownComment.ts | 177 + .../markdownCodec/parsers/markdownImage.ts | 99 + .../markdownCodec/parsers/markdownLink.ts | 213 + .../markdownCodec/tokens/markdownComment.ts | 56 + .../markdownCodec/tokens/markdownImage.ts | 141 + .../markdownCodec/tokens/markdownLink.ts | 8 +- .../common/codecs/simpleCodec/parserBase.ts | 59 +- .../codecs/simpleCodec/simpleDecoder.ts | 30 +- .../simpleCodec/tokens/angleBrackets.ts | 102 + .../codecs/simpleCodec/tokens/brackets.ts | 7 +- .../common/codecs/simpleCodec/tokens/colon.ts | 5 +- .../common/codecs/simpleCodec/tokens/dash.ts | 53 + .../simpleCodec/tokens/exclamationMark.ts | 53 + .../common/codecs/simpleCodec/tokens/hash.ts | 5 +- .../codecs/simpleCodec/tokens/parentheses.ts | 11 +- .../common/codecs/simpleCodec/tokens/tab.ts | 5 +- .../common/codecs/simpleCodec/tokens/word.ts | 2 +- .../config/editorConfigurationSchema.ts | 25 + src/vs/editor/common/config/editorOptions.ts | 57 +- src/vs/editor/common/languages.ts | 46 +- .../common/languages/highlights/css.scm | 90 + .../common/languages/highlights/regex.scm | 96 + .../languages/highlights/typescript.scm | 296 +- .../languages/injections/typescript.scm | 2 + src/vs/editor/common/model/tokenStore.ts | 21 +- .../model/treeSitterTokenStoreService.ts | 65 +- .../editor/common/model/treeSitterTokens.ts | 21 +- ...itorSimpleWorker.ts => editorWebWorker.ts} | 95 +- .../common/services/editorWebWorkerMain.ts | 9 + src/vs/editor/common/services/editorWorker.ts | 4 +- .../common/services/editorWorkerBootstrap.ts | 51 - .../common/services/editorWorkerHost.ts | 6 +- .../textModelSync/textModelSync.impl.ts | 8 +- .../services/textResourceConfiguration.ts | 2 +- .../textResourceConfigurationService.ts | 4 +- .../common/services/treeSitter/cursorUtils.ts | 84 + .../treeSitter/textModelTreeSitter.ts | 781 + .../treeSitter/treeSitterLanguages.ts | 126 + .../treeSitter/treeSitterParserService.ts | 599 +- .../services/treeSitterParserService.ts | 24 +- src/vs/editor/common/tokens/lineTokens.ts | 5 + src/vs/editor/common/viewModel.ts | 1 + .../editor/common/viewModel/viewModelImpl.ts | 10 +- .../editor/common/viewModelEventDispatcher.ts | 26 + .../contrib/clipboard/browser/clipboard.ts | 12 +- .../contrib/codelens/browser/codelens.ts | 7 + .../codelens/browser/codelensController.ts | 2 +- .../codelens/browser/codelensWidget.css | 3 +- .../colorPicker/browser/colorDetector.ts | 8 +- .../colorPickerParts/colorPickerHeader.ts | 4 +- .../contrib/find/browser/findController.ts | 4 +- .../folding/browser/syntaxRangeProvider.ts | 3 + .../gotoError/browser/gotoErrorWidget.ts | 10 +- .../browser/peek/referencesWidget.ts | 8 +- .../controller/inlineCompletionsController.ts | 12 +- .../inlineCompletionsAccessibleView.ts | 27 +- .../browser/model/changeRecorder.ts | 2 +- .../browser/model/ghostText.ts | 35 +- .../browser/model/inlineCompletionsModel.ts | 114 +- .../browser/model/inlineCompletionsSource.ts | 305 +- .../browser/model/inlineEdit.ts | 2 - .../browser/model/provideInlineCompletions.ts | 30 +- .../browser/structuredLogger.ts | 58 + .../browser/view/ghostText/ghostTextView.ts | 24 +- .../browser/view/inlineCompletionsView.ts | 2 +- .../components/gutterIndicatorMenu.ts | 105 +- .../components/gutterIndicatorView.ts | 415 +- .../view/inlineEdits/inlineEditWithChanges.ts | 2 - .../view/inlineEdits/inlineEditsModel.ts | 103 + .../view/inlineEdits/inlineEditsView.ts | 280 +- .../inlineEdits/inlineEditsViewInterface.ts | 26 +- ...Producer.ts => inlineEditsViewProducer.ts} | 56 +- .../inlineEditsCollapsedView.ts | 119 + .../inlineEditsDeletionView.ts | 145 +- .../inlineEditsInsertionView.ts | 193 +- .../inlineEditsLineReplacementView.ts | 107 +- .../inlineEditsSideBySideView.ts | 341 +- .../inlineEditsWordInsertView.ts | 7 +- .../inlineEditsWordReplacementView.ts | 171 +- .../originalEditorInlineDiffView.ts | 78 +- .../browser/view/inlineEdits/theme.ts | 134 +- .../browser/view/inlineEdits/utils/utils.ts | 23 +- .../browser/view/inlineEdits/view.css | 93 +- .../editor/contrib/links/browser/getLinks.ts | 5 +- src/vs/editor/contrib/links/browser/links.ts | 4 - .../multicursor/browser/multicursor.ts | 15 +- .../browser/gotoSymbolQuickAccess.ts | 10 +- .../contrib/rename/browser/renameWidget.ts | 2 +- .../sectionHeaders/browser/sectionHeaders.ts | 5 +- .../browser/stickyScrollController.ts | 29 +- .../browser/stickyScrollProvider.ts | 25 +- .../browser/stickyScrollWidget.ts | 58 +- .../test/browser/stickyScroll.test.ts | 6 +- .../contrib/suggest/browser/suggestModel.ts | 2 + .../contrib/suggest/browser/suggestWidget.ts | 16 +- .../suggest/test/browser/wordDistance.test.ts | 4 +- .../browser/unicodeHighlighter.ts | 56 +- .../contrib/zoneWidget/browser/zoneWidget.ts | 6 +- src/vs/editor/editor.worker.start.ts | 35 + .../standalone/browser/standalone-tokens.css | 20 +- .../standalone/browser/standaloneEditor.ts | 4 +- .../standalone/browser/standaloneServices.ts | 5 +- .../browser/standaloneThemeService.ts | 6 +- .../browser/standaloneTreeSitterService.ts | 4 +- .../standalone/browser/standaloneWebWorker.ts | 80 +- .../test/browser/standaloneLanguages.test.ts | 4 +- .../services/treeSitterParserService.test.ts | 7 +- .../common/codecs/markdownDecoder.test.ts | 1000 +- .../test/common/codecs/simpleDecoder.test.ts | 43 +- ...Worker.test.ts => editorWebWorker.test.ts} | 6 +- .../common/services/testTreeSitterService.ts | 4 +- .../editor/test/common/utils/testDecoder.ts | 34 +- src/vs/monaco.d.ts | 20 +- .../accessibility/browser/accessibleView.ts | 6 + .../browser/accessibilitySignalService.ts | 16 + .../browser/media/editsKept.mp3 | Bin 0 -> 49600 bytes .../browser/media/editsUndone.mp3 | Bin 0 -> 35200 bytes .../actionWidget/browser/actionList.ts | 7 +- .../dropdownWithPrimaryActionViewItem.ts | 6 + .../browser/menuEntryActionViewItem.ts | 4 +- src/vs/platform/actions/common/actions.ts | 1 + .../electron-main/auxiliaryWindow.ts | 4 +- .../backup/electron-main/backupMainService.ts | 5 +- .../configuration/common/configuration.ts | 24 + .../common/configurationModels.ts | 8 +- .../common/configurationRegistry.ts | 46 +- .../configuration/common/configurations.ts | 33 +- .../test/common/policyConfiguration.test.ts | 23 + .../platform/contextkey/common/contextkey.ts | 4 + .../contextview/browser/contextMenuHandler.ts | 2 +- .../contextview/browser/contextViewService.ts | 4 +- .../electron-main/diagnosticsMainService.ts | 4 +- .../diagnostics/node/diagnosticsService.ts | 3 +- src/vs/platform/dialogs/browser/dialog.ts | 41 + src/vs/platform/dialogs/common/dialogs.ts | 1 + src/vs/platform/environment/common/argv.ts | 4 +- src/vs/platform/environment/node/argv.ts | 14 +- .../abstractExtensionManagementService.ts | 41 +- .../common/extensionGalleryManifest.ts | 87 + .../common/extensionGalleryManifestService.ts | 231 + .../extensionGalleryManifestServiceIpc.ts | 50 + .../common/extensionGalleryService.ts | 696 +- .../common/extensionManagement.ts | 44 +- .../common/extensionManagementCLI.ts | 6 +- .../common/extensionManagementUtil.ts | 6 +- .../common/extensionsScannerService.ts | 2 + .../node/extensionManagementService.ts | 33 +- .../node/extensionsScannerService.test.ts | 5 +- .../browser/extensionResourceLoaderService.ts | 8 +- .../common/extensionResourceLoader.ts | 47 +- .../common/extensionResourceLoaderService.ts | 8 +- .../extensions/common/extensionValidator.ts | 1 - .../platform/extensions/common/extensions.ts | 6 + .../common/extensionsApiProposals.ts | 25 +- .../node/diskFileSystemProviderServer.ts | 6 +- .../node/watcher/nodejs/nodejsWatcherLib.ts | 17 +- .../node/watcher/parcel/parcelWatcher.ts | 9 +- src/vs/platform/hover/browser/hover.ts | 2 +- .../hover/test/browser/nullHoverService.ts | 2 +- .../common/jsonContributionRegistry.ts | 56 +- .../languagePacks/browser/languagePacks.ts | 2 +- .../electron-main/lifecycleMainService.ts | 3 +- src/vs/platform/log/common/log.ts | 11 +- .../platform/markers/common/markerService.ts | 8 +- .../markers/test/common/markerService.test.ts | 2 +- .../platform/mcp/common/mcpManagementCli.ts | 63 + .../platform/mcp/common/mcpPlatformTypes.ts | 27 + .../mcp/common/nativeMcpDiscoveryHelper.ts | 26 + .../node/nativeMcpDiscoveryHelperChannel.ts | 33 + .../node/nativeMcpDiscoveryHelperService.ts | 33 + .../electron-main/menubarMainService.ts | 4 +- src/vs/platform/native/common/native.ts | 3 + .../electron-main/nativeHostMainService.ts | 127 +- .../observable/common/observableMemento.ts | 95 + .../policy/common/filePolicyService.ts | 3 +- src/vs/platform/policy/common/policy.ts | 12 +- src/vs/platform/policy/common/policyIpc.ts | 5 +- src/vs/platform/product/common/product.ts | 5 +- .../common/profilingTelemetrySpec.ts | 12 +- .../electron-sandbox/profileAnalysisWorker.ts | 10 +- .../profileAnalysisWorkerMain.ts | 4 +- .../profileAnalysisWorkerService.ts | 7 +- src/vs/platform/prompts/common/config.ts | 27 +- src/vs/platform/prompts/common/constants.ts | 40 +- .../prompts/test/common/config.test.ts | 2 +- .../prompts/test/common/constants.test.ts | 11 +- .../electron-main/protocolMainService.ts | 14 +- .../quickinput/browser/media/quickInput.css | 11 + .../platform/quickinput/browser/quickInput.ts | 2 +- .../browser/quickInputController.ts | 30 +- .../quickinput/browser/quickInputTree.ts | 7 +- .../platform/quickinput/common/quickInput.ts | 3 + .../remote/common/remote.ts} | 5 +- .../test/common/testSecretStorageService.ts | 36 + src/vs/platform/state/node/stateService.ts | 6 +- src/vs/platform/storage/common/storage.ts | 6 +- .../platform/storage/common/storageService.ts | 25 +- .../electron-main/storageMainService.ts | 8 +- .../common/serverTelemetryService.ts | 24 +- .../telemetry/common/telemetryService.ts | 104 +- .../telemetry/common/telemetryUtils.ts | 2 +- .../telemetry/electron-main/errorTelemetry.ts | 38 + .../common/capabilities/capabilities.ts | 4 + .../commandDetection/promptInputModel.ts | 103 +- .../commandDetectionCapability.ts | 31 +- src/vs/platform/terminal/common/terminal.ts | 2 + .../terminal/common/terminalRecorder.ts | 1 + .../common/xterm/shellIntegrationAddon.ts | 42 +- src/vs/platform/terminal/node/ptyService.ts | 17 +- .../terminal/node/terminalEnvironment.ts | 48 +- .../platform/terminal/node/terminalProcess.ts | 2 +- .../commandDetection/promptInputModel.test.ts | 109 +- .../test/node/terminalEnvironment.test.ts | 113 +- src/vs/platform/theme/common/colorUtils.ts | 2 +- src/vs/platform/theme/common/themeService.ts | 6 +- .../theme/electron-main/themeMainService.ts | 3 +- .../theme/test/common/testThemeService.ts | 8 +- src/vs/platform/update/common/update.ts | 41 +- .../electron-main/abstractUpdateService.ts | 84 +- .../electron-main/updateService.darwin.ts | 46 +- .../electron-main/updateService.linux.ts | 5 +- .../electron-main/updateService.win32.ts | 72 +- .../userData/common/fileUserDataProvider.ts | 20 +- .../userDataSync/common/userDataSync.ts | 8 +- .../common/userDataSyncEnablementService.ts | 12 +- .../test/common/userDataSyncClient.ts | 5 + .../utilityProcessWorkerMainService.ts | 10 +- .../common/webContentExtractor.ts | 46 + .../electron-main/cdpAccessibilityDomain.ts | 199 + .../webContentExtractorService.ts | 73 + .../webContentExtractorService.ts | 10 + .../node/sharedWebContentExtractorService.ts | 38 + src/vs/platform/window/common/window.ts | 28 + .../platform/window/electron-main/window.ts | 7 +- .../windows/electron-main/windowImpl.ts | 143 +- .../platform/windows/electron-main/windows.ts | 37 +- .../electron-main/windowsMainService.ts | 6 +- .../electron-main/windowsStateHandler.ts | 4 +- src/vs/platform/workspace/common/workspace.ts | 28 +- .../electron-main/workspacesMainService.ts | 4 +- .../workspacesManagementMainService.ts | 4 +- .../server/node/remoteAgentEnvironmentImpl.ts | 2 +- .../node/remoteExtensionHostAgentCli.ts | 3 + .../node/remoteExtensionHostAgentServer.ts | 3 +- src/vs/server/node/serverServices.ts | 9 + .../api/browser/extensionHost.contribution.ts | 2 + .../api/browser/mainThreadChatAgents2.ts | 10 +- .../api/browser/mainThreadChatStatus.ts | 33 + .../api/browser/mainThreadComments.ts | 62 +- .../api/browser/mainThreadCustomEditors.ts | 4 +- .../api/browser/mainThreadLanguageModels.ts | 16 +- src/vs/workbench/api/browser/mainThreadMcp.ts | 197 + .../browser/mainThreadNotebookDocuments.ts | 3 +- .../workbench/api/browser/mainThreadTask.ts | 62 +- .../mainThreadTerminalShellIntegration.ts | 28 +- .../workbench/api/browser/mainThreadUrls.ts | 31 +- .../api/browser/mainThreadWebviewPanels.ts | 4 +- .../api/browser/mainThreadWorkspace.ts | 3 +- .../api/common/configurationExtensionPoint.ts | 21 +- .../workbench/api/common/extHost.api.impl.ts | 53 +- .../api/common/extHost.common.services.ts | 2 + .../workbench/api/common/extHost.protocol.ts | 45 +- .../api/common/extHostAuthentication.ts | 4 +- .../api/common/extHostChatAgents2.ts | 97 +- .../workbench/api/common/extHostChatStatus.ts | 99 + .../api/common/extHostConfiguration.ts | 4 +- .../api/common/extHostDebugService.ts | 2 +- .../common/extHostFileSystemEventService.ts | 8 +- .../api/common/extHostLanguageModelTools.ts | 50 +- .../api/common/extHostLanguageModels.ts | 19 +- src/vs/workbench/api/common/extHostMcp.ts | 242 + src/vs/workbench/api/common/extHostTask.ts | 37 + .../api/common/extHostTerminalService.ts | 28 +- .../common/extHostTerminalShellIntegration.ts | 194 +- .../api/common/extHostTypeConverters.ts | 140 +- src/vs/workbench/api/common/extHostTypes.ts | 161 +- src/vs/workbench/api/common/shared/tasks.ts | 60 + .../api/node/extHost.node.services.ts | 3 + .../api/node/extHostDiskFileSystemProvider.ts | 6 +- src/vs/workbench/api/node/extHostMpcNode.ts | 158 + .../api/node/extensionHostProcess.ts | 5 +- .../test/browser/extHostConfiguration.test.ts | 23 + .../extHostTerminalShellIntegration.test.ts | 307 + .../api/worker/extensionHostWorker.ts | 4 - .../browser/actions/layoutActions.ts | 2 +- src/vs/workbench/browser/labels.ts | 35 +- src/vs/workbench/browser/layout.ts | 35 +- src/vs/workbench/browser/media/code-icon.svg | 1 + .../parts/activitybar/activitybarPart.ts | 3 +- .../parts/auxiliarybar/auxiliaryBarActions.ts | 2 +- .../parts/auxiliarybar/auxiliaryBarPart.ts | 8 +- .../browser/parts/compositeBarActions.ts | 4 +- .../browser/parts/dialogs/dialogHandler.ts | 40 +- .../parts/editor/auxiliaryEditorPart.ts | 8 +- .../parts/editor/breadcrumbsControl.ts | 18 +- .../parts/editor/editor.contribution.ts | 5 + .../browser/parts/editor/editorActions.ts | 2 +- .../parts/editor/editorConfiguration.ts | 3 +- .../browser/parts/editor/editorDropTarget.ts | 8 +- .../browser/parts/editor/editorGroupView.ts | 23 +- .../parts/editor/editorGroupWatermark.ts | 196 + .../browser/parts/editor/editorPanes.ts | 29 +- .../browser/parts/editor/editorPart.ts | 10 +- .../browser/parts/editor/editorParts.ts | 7 +- .../browser/parts/editor/editorPlaceholder.ts | 8 +- .../browser/parts/editor/editorStatus.ts | 12 +- .../browser/parts/editor/editorTabsControl.ts | 10 +- .../parts/editor/editorTitleControl.ts | 5 +- .../parts/editor/media/editortabscontrol.css | 18 - .../editor/media/multieditortabscontrol.css | 25 +- .../parts/editor/multiEditorTabsControl.ts | 48 +- .../browser/parts/editor/sideBySideEditor.ts | 4 +- .../parts/editor/singleEditorTabsControl.ts | 5 +- .../browser/parts/globalCompositeBar.ts | 2 +- .../notifications/notificationsCenter.ts | 18 +- .../parts/notifications/notificationsList.ts | 5 +- .../notifications/notificationsToasts.ts | 19 +- .../notifications/notificationsViewer.ts | 33 +- .../browser/parts/paneCompositePart.ts | 6 +- .../browser/parts/panel/panelActions.ts | 12 +- .../browser/parts/statusbar/statusbarItem.ts | 17 +- .../browser/parts/statusbar/statusbarPart.ts | 94 +- .../parts/titlebar/media/titlebarpart.css | 53 +- .../browser/parts/titlebar/titlebarPart.ts | 27 +- .../workbench/browser/parts/views/viewPane.ts | 28 +- .../browser/parts/views/viewPaneContainer.ts | 9 +- .../browser/workbench.contribution.ts | 2 +- .../common/editor/diffEditorInput.ts | 20 +- .../common/editor/resourceEditorInput.ts | 1 + .../common/editor/sideBySideEditorInput.ts | 4 +- src/vs/workbench/common/release.ts | 15 - src/vs/workbench/common/theme.ts | 4 +- .../browser/accessibilityConfiguration.ts | 29 + .../accessibility/browser/accessibleView.ts | 8 +- .../browser/accessibleViewActions.ts | 2 +- .../browser/callHierarchyPeek.ts | 2 +- .../browser/actions/chatAccessibilityHelp.ts | 50 +- .../chat/browser/actions/chatActions.ts | 315 +- .../chatAttachPromptAction.ts | 20 +- .../dialogs/askToSelectPrompt.ts | 331 - .../askToSelectPrompt/askToSelectPrompt.ts | 199 + .../dialogs/askToSelectPrompt/constants.ts | 57 + .../askToSelectPrompt/utils/attachPrompts.ts | 126 + .../utils/createPlaceholderText.ts | 52 + .../utils/createPromptPickItem.ts | 47 + .../utils/handleButtonClick.ts | 114 + .../chat/browser/actions/chatClearActions.ts | 172 +- .../browser/actions/chatCodeblockActions.ts | 2 +- .../browser/actions/chatContextActions.ts | 212 +- .../browser/actions/chatDeveloperActions.ts | 26 +- .../browser/actions/chatExecuteActions.ts | 288 +- .../browser/actions/chatGettingStarted.ts | 5 +- .../chat/browser/actions/chatMoveActions.ts | 30 +- .../browser/actions/chatQuickInputActions.ts | 1 + .../chat/browser/actions/chatTitleActions.ts | 52 +- .../chat/browser/actions/chatToolActions.ts | 325 +- .../chat/browser/actions/chatTransfer.ts | 19 + .../promptAttachmentWidget.ts | 16 +- .../contrib/chat/browser/chat.contribution.ts | 218 +- src/vs/workbench/contrib/chat/browser/chat.ts | 18 +- .../chat/browser/chatAccessibilityProvider.ts | 39 +- .../chat/browser/chatAttachmentModel.ts | 45 +- .../chatPromptAttachmentsCollection.ts | 7 +- .../chat/browser/chatAttachmentWidgets.ts | 398 + .../chatAttachmentsContentPart.ts | 8 +- .../chatCollapsibleContentPart.ts | 184 + .../chatConfirmationContentPart.ts | 4 +- .../chatConfirmationWidget.ts | 76 +- .../chatMarkdownContentPart.ts | 92 +- .../chatProgressContentPart.ts | 7 +- .../chatContentParts/chatQuotaExceededPart.ts | 9 +- .../chatReferencesContentPart.ts | 95 +- .../chatToolInvocationPart.ts | 171 +- .../media/chatConfirmationWidget.css | 12 + .../contrib/chat/browser/chatDragAndDrop.ts | 185 +- .../browser/chatEditing/chatEditingActions.ts | 145 +- .../chatEditingCodeEditorIntegration.ts | 29 +- .../chatEditing/chatEditingEditorActions.ts | 6 +- .../chatEditing/chatEditingEditorOverlay.ts | 4 +- .../chatEditingModifiedDocumentEntry.ts | 66 +- .../chatEditingModifiedFileEntry.ts | 25 +- .../chatEditingModifiedNotebookEntry.ts | 1302 +- .../chatEditingModifiedNotebookSnapshot.ts | 103 - .../chatEditingNotebookEditorIntegration.ts | 582 - .../chatEditing/chatEditingServiceImpl.ts | 72 +- .../browser/chatEditing/chatEditingSession.ts | 188 +- .../chatEditingModifiedNotebookDiff.ts | 69 + .../chatEditingModifiedNotebookSnapshot.ts | 182 + .../chatEditingNewNotebookContentEdits.ts | 86 + .../notebook/chatEditingNotebookCellEntry.ts | 345 + .../chatEditingNotebookEditorIntegration.ts | 674 + .../chatEditingNotebookFileSystemProvider.ts | 26 +- .../browser/chatEditing/notebook/helpers.ts | 418 + .../notebook/notebookCellChanges.ts | 122 + .../contrib/chat/browser/chatEditor.ts | 26 +- .../contrib/chat/browser/chatEditorInput.ts | 33 +- .../contrib/chat/browser/chatFollowups.ts | 3 +- .../contrib/chat/browser/chatInputPart.ts | 757 +- .../contrib/chat/browser/chatListRenderer.ts | 82 +- .../chatMarkdownDecorationsRenderer.ts | 6 +- .../browser/chatParticipant.contribution.ts | 193 +- .../contrib/chat/browser/chatQuick.ts | 2 +- .../contrib/chat/browser/chatSelectedTools.ts | 133 + .../contrib/chat/browser/chatSetup.ts | 1560 +- .../contrib/chat/browser/chatStatus.ts | 619 +- .../chat/browser/chatStatusItemService.ts | 62 + .../contrib/chat/browser/chatVariables.ts | 14 +- .../contrib/chat/browser/chatViewPane.ts | 120 +- .../contrib/chat/browser/chatWidget.ts | 155 +- .../contrib/chat/browser/codeBlockPart.ts | 17 +- .../browser/contrib/chatDynamicVariables.ts | 13 +- .../browser/contrib/chatImplicitContext.ts | 4 +- .../browser/contrib/chatInputCompletions.ts | 19 +- .../browser/contrib/chatInputEditorContrib.ts | 4 +- .../contrib/chatInputRelatedFilesContrib.ts | 74 +- .../chat/browser/languageModelToolsService.ts | 243 +- .../contrib/chat/browser/media/chat.css | 100 +- .../contrib/chat/browser/media/chatSetup.css | 76 + .../contrib/chat/browser/media/chatStatus.css | 117 +- .../chat/browser/media/chatViewWelcome.css | 8 - .../createPromptCommand.ts | 85 +- .../utils/createPromptFile.ts | 8 +- .../contributions/usePromptCommand.ts | 19 +- .../viewsWelcome/chatViewWelcomeController.ts | 15 +- .../contrib/chat/common/annotations.ts | 18 +- .../workbench/contrib/chat/common/chat.ts} | 17 +- .../contrib/chat/common/chatAgents.ts | 98 +- .../contrib/chat/common/chatContextKeys.ts | 66 +- .../contrib/chat/common/chatEditingService.ts | 15 +- .../chat/common/chatEntitlementService.ts | 886 + .../chat/common/chatEntitlementsService.ts | 46 - .../contrib/chat/common/chatModel.ts | 54 +- .../contrib/chat/common/chatParserTypes.ts | 3 +- .../common/chatParticipantContribTypes.ts | 4 +- .../chatProgressTypes/chatToolInvocation.ts | 9 +- .../contrib/chat/common/chatQuotasService.ts | 135 - .../contrib/chat/common/chatRequestParser.ts | 14 +- .../contrib/chat/common/chatService.ts | 38 +- .../contrib/chat/common/chatServiceImpl.ts | 282 +- .../contrib/chat/common/chatSessionStore.ts | 474 + .../contrib/chat/common/chatSlashCommands.ts | 11 +- .../chat/common/chatTransferService.ts | 36 + .../contrib/chat/common/chatVariables.ts | 2 +- .../contrib/chat/common/chatViewModel.ts | 5 +- .../chat/common/chatWidgetHistoryService.ts | 3 +- .../chat/common/codeBlockModelCollection.ts | 14 +- .../contrib/chat/common/constants.ts | 52 + .../chat/common/languageModelToolsService.ts | 68 +- .../contrib/chat/common/languageModels.ts | 40 +- .../chat/common/promptFileReferenceErrors.ts | 113 +- .../promptSyntax/codecs/chatPromptDecoder.ts | 215 +- .../codecs/parsers/promptVariableParser.ts | 251 + .../codecs/tokens/fileReference.ts | 94 +- .../promptSyntax/codecs/tokens/promptToken.ts | 11 + .../codecs/tokens/promptVariable.ts | 152 + .../chat/common/promptSyntax/constants.ts | 23 +- .../filePromptContentsProvider.ts | 16 +- .../promptContentsProviderBase.ts | 59 +- .../textModelContentsProvider.ts | 28 +- .../promptSyntax/contentProviders/types.d.ts | 18 +- .../promptLinkDiagnosticsProvider.ts | 224 + .../promptPathAutocompletion.ts | 16 +- .../promptSyntax/parsers/basePromptParser.ts | 374 +- .../promptSyntax/parsers/filePromptParser.ts | 2 + .../parsers/textModelPromptParser.ts | 5 +- .../common/promptSyntax/parsers/topError.ts | 102 + .../common/promptSyntax/parsers/types.d.ts | 87 +- .../promptSyntax/service/promptsService.ts | 8 +- .../promptSyntax/utils/promptFilesLocator.ts | 417 +- .../contrib/chat/common/tools/editFileTool.ts | 57 +- .../common/tools/insertNotebookCellsTool.ts | 219 + .../tools/languageModelToolsContribution.ts | 24 +- .../chat/common/tools/promptTsxTypes.ts | 74 + .../contrib/chat/common/tools/tools.ts | 2 + .../contrib/chat/common/voiceChatService.ts | 8 +- .../actions/chatDeveloperActions.ts | 36 + .../actions/voiceChatActions.ts | 32 +- .../electron-sandbox/chat.contribution.ts | 26 +- .../electron-sandbox/tools/fetchPageTool.ts | 185 + .../chatEditingModifiedNotebookEntry.test.ts | 2075 +++ .../test/browser/chatEditingService.test.ts | 19 +- .../browser/languageModelToolsService.test.ts | 21 +- .../chat/test/browser/mockChatWidget.ts | 6 +- ...atRequestParser_agent_but_edit_mode.0.snap | 19 + .../chat/test/common/chatAgents.test.ts | 3 +- .../chat/test/common/chatModel.test.ts | 8 +- .../test/common/chatRequestParser.test.ts | 21 +- .../chat/test/common/chatService.test.ts | 28 +- .../chat/test/common/mockChatService.ts | 29 +- .../chat/test/common/mockChatVariables.ts | 2 +- .../common/mockLanguageModelToolsService.ts | 8 + .../codecs/chatPromptCodec.test.ts | 8 +- .../codecs/chatPromptDecoder.test.ts | 7 +- .../codecs/tokens/fileReference.test.ts | 25 +- .../parsers/textModelPromptParser.test.ts | 10 +- .../promptSyntax/promptFileReference.test.ts | 4 +- .../testUtils/expectedReference.ts | 28 +- .../utils/promptFilesLocator.test.ts | 2679 ++- .../chat/test/common/voiceChatService.test.ts | 10 +- .../browser/dictation/editorDictation.ts | 13 +- .../emptyTextEditorHint.ts | 3 +- .../inspectEditorTokens.ts | 43 +- .../browser/workbenchEditorWorkerService.ts | 5 +- .../comments/browser/commentService.ts | 14 + .../browser/commentThreadZoneWidget.ts | 2 +- .../browser/commentsAccessibleView.ts | 32 +- .../comments/browser/commentsViewActions.ts | 9 +- .../configurationExportHelper.contribution.ts | 26 - .../configurationExportHelper.ts | 120 - .../customEditor/browser/customEditorInput.ts | 1 + .../contrib/debug/browser/breakpointsView.ts | 2 +- .../browser/callStackEditorContribution.ts | 5 +- .../debug/browser/debugEditorContribution.ts | 7 +- .../contrib/debug/browser/debugHover.ts | 4 +- .../contrib/debug/browser/debugService.ts | 2 +- .../contrib/debug/browser/debugSession.ts | 13 +- .../contrib/debug/browser/debugToolBar.ts | 10 +- .../contrib/debug/browser/disassemblyView.ts | 4 +- .../contrib/debug/browser/exceptionWidget.ts | 8 +- .../workbench/contrib/debug/common/debug.ts | 2 + .../contrib/debug/test/common/mockDebug.ts | 4 + .../browser/editSessionsStorageService.ts | 45 +- .../editSessions/common/workspaceStateSync.ts | 1 + .../extensions/browser/extensionEditor.ts | 123 +- .../browser/extensionFeaturesTab.ts | 4 +- .../browser/extensions.contribution.ts | 83 +- .../extensions/browser/extensionsActions.ts | 40 +- .../extensions/browser/extensionsIcons.ts | 1 + .../extensions/browser/extensionsList.ts | 21 +- .../extensions/browser/extensionsViewlet.ts | 87 +- .../extensions/browser/extensionsViews.ts | 2 +- .../extensions/browser/extensionsWidgets.ts | 228 +- .../browser/extensionsWorkbenchService.ts | 85 +- .../extensions/browser/media/extension.css | 23 +- .../browser/media/extensionEditor.css | 19 +- .../browser/media/extensionsWidgets.css | 4 +- .../extensions/common/extensionQuery.ts | 31 +- .../contrib/extensions/common/extensions.ts | 5 +- .../extensions.contribution.ts | 4 +- .../electron-sandbox/remoteExtensionsInit.ts | 46 +- .../test/common/extensionQuery.test.ts | 8 +- .../editors/textFileSaveErrorHandler.ts | 8 +- .../contrib/files/browser/explorerService.ts | 44 +- .../contrib/files/browser/fileCommands.ts | 9 +- .../files/browser/files.contribution.ts | 15 + .../browser/inlayHintsAccessibilty.ts | 2 +- .../inlineChat/browser/inlineChatActions.ts | 17 +- .../browser/inlineChatController.ts | 13 +- .../browser/inlineChatCurrentLine.ts | 9 +- .../browser/inlineChatSessionServiceImpl.ts | 57 +- .../browser/inlineChatZoneWidget.ts | 3 +- .../contrib/inlineChat/common/inlineChat.ts | 2 + .../test/browser/inlineChatController.test.ts | 3 +- .../test/browser/inlineChatSession.test.ts | 3 +- .../test/browser/testWorkerService.ts | 4 +- ...CompletionLanguageStatusBarContribution.ts | 2 +- .../issue/browser/baseIssueReporterService.ts | 29 +- .../contrib/issue/browser/issueFormService.ts | 2 + .../issue/browser/issueReporterPage.ts | 14 +- .../issue/browser/media/issueReporter.css | 13 +- .../issue/common/issue.contribution.ts | 19 +- .../electron-sandbox/issue.contribution.ts | 26 +- .../electron-sandbox/issueReporterService.ts | 19 +- .../electron-sandbox/media/issueReporter.css | 22 +- .../nativeIssueFormService.ts | 13 +- .../browser/limitIndicator.contribution.ts | 4 +- .../contrib/markers/browser/markersTable.ts | 2 +- .../markers/browser/markersTreeViewer.ts | 21 +- .../contrib/mcp/browser/mcp.contribution.ts | 60 + .../contrib/mcp/browser/mcpCommands.ts | 559 + .../browser/mcpCommandsAddConfiguration.ts | 498 + .../contrib/mcp/browser/mcpDiscovery.ts | 38 + .../mcp/browser/mcpLanguageFeatures.ts | 372 + .../contrib/mcp/browser/mcpUrlHandler.ts | 69 + .../common/discovery/configMcpDiscovery.ts | 166 + .../common/discovery/extensionMcpDiscovery.ts | 128 + .../mcp/common/discovery/mcpDiscovery.ts | 29 + .../discovery/nativeMcpDiscoveryAbstract.ts | 159 + .../discovery/nativeMcpDiscoveryAdapters.ts | 110 + .../discovery/nativeMcpRemoteDiscovery.ts | 51 + .../discovery/workspaceMcpDiscoveryAdapter.ts | 76 + .../contrib/mcp/common/mcpConfigFileUtils.ts | 41 + .../mcp/common/mcpConfigPathsService.ts | 152 + .../contrib/mcp/common/mcpConfiguration.ts | 169 + .../contrib/mcp/common/mcpContextKeys.ts | 63 + .../contrib/mcp/common/mcpRegistry.ts | 372 + .../mcp/common/mcpRegistryInputStorage.ts | 181 + .../contrib/mcp/common/mcpRegistryTypes.ts | 79 + .../workbench/contrib/mcp/common/mcpServer.ts | 512 + .../contrib/mcp/common/mcpServerConnection.ts | 128 + .../mcp/common/mcpServerRequestHandler.ts | 486 + .../contrib/mcp/common/mcpService.ts | 275 + .../workbench/contrib/mcp/common/mcpTypes.ts | 413 + .../mcp/common/modelContextProtocol.ts | 1138 ++ .../mcp/electron-sandbox/mcp.contribution.ts | 10 + .../electron-sandbox/nativeMpcDiscovery.ts | 42 + .../mcp/test/common/mcpRegistry.test.ts | 566 + .../common/mcpRegistryInputStorage.test.ts | 178 + .../mcp/test/common/mcpRegistryTypes.ts | 108 + .../test/common/mcpServerConnection.test.ts | 389 + .../common/mcpServerRequestHandler.test.ts | 398 + .../browser/mergeEditor.contribution.ts | 4 + .../browser/mergeEditorAccessibilityHelp.ts | 42 + .../browser/mergeEditorInputModel.ts | 29 +- .../contrib/mergeEditor/browser/utils.ts | 16 +- .../mergeEditor/browser/view/mergeEditor.ts | 37 +- .../cellDiagnosticEditorContrib.ts | 11 +- .../diagnosticCellStatusBarContrib.ts | 9 +- .../executionStatusBarItemController.ts | 2 +- .../browser/contrib/chatEdit/contribution.ts | 17 - .../chatEdit/notebookCellDecorators.ts | 361 - .../chatEdit/notebookChatActionsOverlay.ts | 323 - .../chatEdit/notebookChatEditContext.ts | 9 - .../chatEdit/notebookChatEditController.ts | 211 - .../contrib/chatEdit/notebookSynchronizer.ts | 500 - .../chatEdit/notebookSynchronizerService.ts | 94 - .../contrib/debug/notebookDebugDecorations.ts | 25 +- .../multicursor/notebookMulticursor.ts | 2 +- .../browser/controller/cellOutputActions.ts | 2 +- .../chat/notebook.chat.contribution.ts | 93 +- .../controller/chat/notebookChatController.ts | 2 +- .../browser/controller/executeActions.ts | 4 +- .../inlineDiff/notebookCellDiffDecorator.ts | 2 +- .../notebookDeletedCellDecorator.ts | 21 +- .../diff/inlineDiff/notebookInlineDiff.ts | 8 +- .../notebookInsertedCellDecorator.ts | 12 +- .../notebookModifiedCellDecorator.ts | 21 +- .../notebookOriginalCellModelFactory.ts | 0 .../notebookOriginalModelRefFactory.ts | 0 .../browser/diff/notebookDiffEditor.ts | 4 +- .../notebook/browser/diff/notebookDiffList.ts | 6 +- .../browser/diff/notebookDiffOverviewRuler.ts | 2 +- .../browser/diff/notebookDiffViewModel.ts | 94 +- .../notebook/browser/media/notebook.css | 4 - .../notebook/browser/notebook.contribution.ts | 6 - .../notebook/browser/notebookBrowser.ts | 42 +- .../notebook/browser/notebookEditor.ts | 1 + .../notebook/browser/notebookEditorWidget.ts | 49 +- .../browser/services/notebookServiceImpl.ts | 66 +- .../services/notebookWorkerServiceImpl.ts | 25 +- .../browser/view/cellParts/cellDecorations.ts | 4 +- .../browser/view/cellParts/cellOutput.ts | 2 +- .../browser/view/cellParts/cellStatusPart.ts | 2 +- .../view/cellParts/cellToolbarStickyScroll.ts | 10 +- .../browser/view/cellParts/codeCell.ts | 2 +- .../notebook/browser/view/notebookCellList.ts | 37 +- .../browser/view/notebookCellListView.ts | 15 +- .../browser/view/notebookRenderingCommon.ts | 4 +- .../view/renderers/backLayerWebView.ts | 17 +- .../browser/view/renderers/cellRenderer.ts | 16 +- .../browser/view/renderers/webviewMessages.ts | 2 +- .../browser/view/renderers/webviewPreloads.ts | 10 + .../viewModel/notebookViewModelImpl.ts | 32 +- .../browser/viewParts/notebookCellOverlays.ts | 204 + .../viewParts/notebookEditorStickyScroll.ts | 36 +- .../notebookEditorWidgetContextKeys.ts | 1 + .../notebookKernelQuickPickStrategy.ts | 35 +- .../browser/viewParts/notebookKernelView.ts | 4 +- .../viewParts/notebookOverviewRuler.ts | 39 + .../browser/viewParts/notebookViewZones.ts | 84 +- .../common/model/notebookCellTextModel.ts | 3 - .../common/model/notebookTextModel.ts | 50 +- .../contrib/notebook/common/notebookCommon.ts | 58 +- .../contrib/notebook/common/notebookDiff.ts | 133 + .../notebook/common/notebookEditorModel.ts | 7 +- .../notebookEditorModelResolverServiceImpl.ts | 11 +- .../common/notebookSynchronizerService.ts | 16 - .../common/services/notebookCellMatching.ts | 344 + .../common/services/notebookSimpleWorker.ts | 370 - .../common/services/notebookWebWorker.ts | 576 + ...WorkerMain.ts => notebookWebWorkerMain.ts} | 6 +- .../contrib/notebookCellDiagnostics.test.ts | 5 +- .../browser/diff/notebookDiffService.test.ts | 14750 ++++++++++++++++ .../test/browser/notebookTextModel.test.ts | 70 + .../output/browser/outputLinkProvider.ts | 9 +- .../output/common/outputLinkComputer.ts | 12 +- .../output/common/outputLinkComputerMain.ts | 4 +- .../browser/keybindingsEditorContribution.ts | 4 +- .../browser/preferences.contribution.ts | 27 +- .../browser/preferencesRenderers.ts | 3 +- .../preferences/browser/preferencesSearch.ts | 196 +- .../preferences/browser/preferencesWidgets.ts | 4 +- .../preferences/browser/settingsEditor2.ts | 84 +- .../settingsEditorSettingIndicators.ts | 12 +- .../preferences/browser/settingsLayout.ts | 10 +- .../preferences/browser/settingsTree.ts | 32 +- .../preferences/browser/settingsTreeModels.ts | 23 +- .../preferences/browser/settingsWidgets.ts | 52 +- .../contrib/preferences/browser/tocTree.ts | 7 +- .../contrib/preferences/common/preferences.ts | 2 +- .../common/preferencesContribution.ts | 7 - .../common/settingsFilesystemProvider.ts | 18 + .../browser/commandsQuickAccess.ts | 3 +- .../quickaccess/browser/viewQuickAccess.ts | 2 +- .../browser/relauncher.contribution.ts | 22 +- .../contrib/remote/browser/remoteIndicator.ts | 68 +- .../remote/common/remote.contribution.ts | 17 +- .../workbench/contrib/scm/browser/activity.ts | 119 +- .../contrib/scm/browser/media/scm.css | 4 + .../contrib/scm/browser/quickDiffDecorator.ts | 9 +- .../contrib/scm/browser/quickDiffWidget.ts | 2 +- .../contrib/scm/browser/scm.contribution.ts | 19 +- .../scm/browser/scmAccessibilityHelp.ts | 18 +- .../contrib/scm/browser/scmHistoryViewPane.ts | 64 +- .../scm/browser/scmRepositoriesViewPane.ts | 7 + .../contrib/scm/browser/scmViewPane.ts | 7 +- .../contrib/scm/browser/scmViewService.ts | 135 +- .../contrib/scm/browser/workingSet.ts | 6 +- .../workbench/contrib/scm/common/quickDiff.ts | 11 +- src/vs/workbench/contrib/scm/common/scm.ts | 1 + .../search/browser/AISearch/aiSearchModel.ts | 18 + .../search/browser/searchActionsCopy.ts | 29 + .../search/browser/searchActionsTopBar.ts | 32 +- .../contrib/search/browser/searchMessage.ts | 2 +- .../search/browser/searchResultsView.ts | 8 +- .../browser/searchTreeModel/searchModel.ts | 5 + .../browser/searchTreeModel/searchResult.ts | 4 +- .../searchTreeModel/searchTreeCommon.ts | 10 +- .../searchTreeModel/textSearchHeading.ts | 4 +- .../contrib/search/browser/searchView.ts | 78 +- .../contrib/search/common/constants.ts | 3 + .../searchEditor/browser/searchEditor.ts | 2 +- .../contrib/speech/browser/speechService.ts | 14 +- .../surveys/browser/nps.contribution.ts | 6 +- .../tasks/browser/abstractTaskService.ts | 3 +- .../tasks/browser/task.contribution.ts | 2 +- .../tasks/browser/taskTerminalStatus.ts | 4 +- .../tasks/browser/terminalTaskSystem.ts | 21 +- .../contrib/tasks/common/problemCollectors.ts | 6 + .../contrib/tasks/common/taskService.ts | 5 + .../workbench/contrib/tasks/common/tasks.ts | 67 +- .../browser/telemetry.contribution.ts | 19 +- .../browser/environmentVariableInfo.ts | 3 +- .../terminal/browser/terminal.contribution.ts | 5 +- .../terminal/browser/terminalActions.ts | 12 +- .../terminal/browser/terminalIconPicker.ts | 2 +- .../terminal/browser/terminalInstance.ts | 26 +- .../terminal/browser/terminalService.ts | 5 +- .../terminal/browser/terminalStatusList.ts | 1 + .../terminal/browser/terminalTabbedView.ts | 4 +- .../terminal/browser/terminalTabsList.ts | 13 +- .../terminal/browser/terminalTelemetry.ts | 143 + .../terminal/browser/terminalTooltip.ts | 109 +- .../contrib/terminal/browser/terminalView.ts | 8 +- .../browser/widgets/terminalHoverWidget.ts | 2 +- .../terminal/browser/xterm/decorationAddon.ts | 23 +- .../terminal/browser/xterm/xtermTerminal.ts | 2 +- .../common/remote/remoteTerminalChannel.ts | 16 +- .../common/scripts/shellIntegration-bash.sh | 63 +- .../common/scripts/shellIntegration-rc.zsh | 18 +- .../common/scripts/shellIntegration.fish | 13 + .../common/scripts/shellIntegration.ps1 | 27 +- .../contrib/terminal/common/terminal.ts | 4 + .../terminal/common/terminalConfiguration.ts | 33 +- .../terminal/common/terminalStorageKeys.ts | 1 + .../terminal/common/terminalStrings.ts | 6 +- .../terminal.initialHint.contribution.ts | 3 +- .../chat/browser/terminalChatActions.ts | 15 +- .../chat/browser/terminalChatEnabler.ts | 7 +- .../chat/browser/terminalChatWidget.ts | 31 +- .../test/browser/terminalInitialHint.test.ts | 3 +- .../browser/media/terminalSymbolIcons.css | 30 +- .../browser/pwshCompletionProviderAddon.ts | 91 +- .../browser/terminal.suggest.contribution.ts | 62 +- .../browser/terminalCompletionModel.ts | 48 +- .../browser/terminalCompletionService.ts | 6 +- .../suggest/browser/terminalSuggestAddon.ts | 126 +- .../browser/terminalSuggestTelemetry.ts | 112 + .../suggest/browser/terminalSymbolIcons.ts | 22 +- .../suggest/common/terminal.suggest.ts | 3 +- .../common/terminalSuggestConfiguration.ts | 12 +- .../themes/browser/themes.contribution.ts | 6 +- .../browser/themes.test.contribution.ts | 112 +- .../timeline/common/timelineService.ts | 9 +- .../browser/typeHierarchyPeek.ts | 2 +- .../update/browser/releaseNotesEditor.ts | 3 +- .../contrib/update/browser/update.ts | 8 +- .../url/browser/trustedDomainService.ts | 60 +- .../url/browser/trustedDomainsValidator.ts | 3 +- .../contrib/url/common/trustedDomains.ts | 59 + .../test/browser/mockTrustedDomainService.ts | 6 +- .../url/test/browser/trustedDomains.test.ts | 2 +- .../browser/userDataProfilesEditor.ts | 33 +- .../browser/userDataProfilesEditorModel.ts | 32 +- .../browser/userDataSync.contribution.ts | 20 +- .../userDataSync/browser/userDataSync.ts | 79 +- .../userDataSync/browser/userDataSyncViews.ts | 8 +- .../contrib/void/browser/chatThreadService.ts | 304 +- .../browser/convertToLLMMessageService.ts | 2 +- .../react/src/markdown/ChatMarkdownRender.tsx | 8 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 3 + .../react/src/sidebar-tsx/SidebarChat.tsx | 696 +- .../void/browser/react/src/util/services.tsx | 6 +- .../void/browser/terminalToolService.ts | 151 +- .../contrib/void/browser/toolsService.ts | 88 +- .../contrib/void/common/modelCapabilities.ts | 11 +- .../contrib/void/common/prompt/prompts.ts | 370 +- .../contrib/void/common/toolsServiceTypes.ts | 7 +- .../llmMessage/extractGrammar.ts | 2 +- .../llmMessage/sendLLMMessage.ts | 18 +- .../void/electron-main/metricsMainService.ts | 5 +- .../electron-main/voidUpdateMainService.ts | 2 +- .../contrib/webview/browser/overlayWebview.ts | 2 +- .../webview/browser/pre/index-no-csp.html | 2 +- .../contrib/webview/browser/pre/index.html | 4 +- .../webview/browser/pre/service-worker.js | 37 +- .../browser/gettingStarted.contribution.ts | 13 +- .../browser/gettingStarted.ts | 17 +- .../browser/gettingStartedDetailsRenderer.ts | 4 +- .../browser/gettingStartedService.ts | 10 +- .../browser/startupPage.ts | 4 - .../common/gettingStartedContent.ts | 6 +- .../actions/developerActions.ts | 2 +- .../electron-sandbox/desktop.contribution.ts | 14 + .../electron-sandbox/desktop.main.ts | 24 +- .../parts/dialogs/dialogHandler.ts | 9 +- .../parts/titlebar/titlebarPart.ts | 92 +- src/vs/workbench/electron-sandbox/window.ts | 27 +- .../accounts/common/defaultAccount.ts | 306 + .../actions/common/menusExtensionPoint.ts | 7 + .../authenticationExtensionsService.ts | 16 +- .../browser/auxiliaryWindowService.ts | 5 +- .../configuration/browser/configuration.ts | 4 +- .../configuration/common/configuration.ts | 4 + .../common/configurationEditing.ts | 41 +- .../common/jsonEditingService.ts | 31 +- .../baseConfigurationResolverService.ts | 291 +- .../common/configurationResolver.ts | 31 +- .../common/configurationResolverExpression.ts | 234 + .../common/variableResolver.ts | 231 +- .../configurationResolverService.test.ts | 157 +- .../editor/browser/editorResolverService.ts | 17 - .../browser/extensionEnablementService.ts | 57 +- .../extensionGalleryManifestService.ts | 31 + .../browser/webExtensionsScannerService.ts | 6 +- .../common/extensionGalleryService.ts | 38 + .../common/extensionManagement.ts | 1 + .../common/extensionManagementService.ts | 21 +- .../common/webExtensionManagementService.ts | 6 + .../extensionGalleryManifestService.ts | 186 + .../extensionEnablementService.test.ts | 43 +- .../services/extensions/common/extensions.ts | 1 + .../extensions/common/extensionsRegistry.ts | 5 + .../diskFileSystemProvider.ts | 6 +- .../common/filesConfigurationService.ts | 39 +- .../history/browser/historyService.ts | 77 +- .../electron-sandbox/nativeHostService.ts | 20 +- .../electron-sandbox/integrityService.ts | 17 +- ...orker.ts => languageDetectionWebWorker.ts} | 30 +- ...n.ts => languageDetectionWebWorkerMain.ts} | 6 +- .../languageDetectionWorker.protocol.ts | 6 +- .../languageDetectionWorkerServiceImpl.ts | 10 +- .../services/layout/browser/layoutService.ts | 6 + .../lifecycle/browser/lifecycleService.ts | 1 + .../services/lifecycle/common/lifecycle.ts | 5 + .../lifecycle/common/lifecycleService.ts | 3 + .../electron-sandbox/lifecycleService.ts | 2 + .../electron-sandbox/lifecycleService.test.ts | 17 + .../common/notebookDocumentService.ts | 39 + .../common/notificationService.ts | 4 +- .../policies/common/accountPolicyService.ts | 60 + .../policies/common/multiplexPolicyService.ts | 61 + .../preferences/browser/preferencesService.ts | 19 +- .../preferences/common/preferences.ts | 4 +- .../preferences/common/preferencesModels.ts | 15 - .../progress/browser/progressService.ts | 40 +- .../services/search/browser/searchService.ts | 18 +- .../common/localFileSearchWorkerTypes.ts | 14 +- .../services/search/worker/localFileSearch.ts | 20 +- .../search/worker/localFileSearchMain.ts | 4 +- .../storage/browser/storageService.ts | 4 +- .../suggest/browser/media/suggest.css | 7 + .../suggest/browser/simpleCompletionItem.ts | 5 + .../suggest/browser/simpleSuggestWidget.ts | 41 +- .../browser/simpleSuggestWidgetDetails.ts | 7 + .../browser/simpleSuggestWidgetRenderer.ts | 57 +- .../textMateWorkerTokenizerController.ts | 2 +- .../threadedBackgroundTokenizerFactory.ts | 8 +- .../textMateTokenizationWorker.worker.ts | 12 +- .../textMateTokenizationWorker.workerMain.ts | 4 +- .../worker/textMateWorkerHost.ts | 6 +- .../textfile/browser/textFileService.ts | 9 +- .../textfile/common/textFileEditorModel.ts | 7 +- .../common/textFileEditorModelManager.ts | 4 +- .../themes/browser/workbenchThemeService.ts | 20 +- .../services/themes/common/colorThemeData.ts | 8 +- .../themes/common/workbenchThemeService.ts | 8 +- .../test/node/tokenStyleResolving.test.ts | 3 +- .../services/timer/browser/timerService.ts | 2 +- .../browser/treeSitterCodeEditors.ts | 60 +- .../browser/treeSitterTokenizationFeature.ts | 438 +- .../common/untitledTextEditorModel.ts | 4 +- .../common/untitledFileWorkingCopy.ts | 7 +- .../common/workingCopyBackupTracker.ts | 4 +- .../common/workingCopyFileService.ts | 9 +- .../workspaces/common/workspaceUtils.ts | 30 + .../parts/editor/editorGroupModel.test.ts | 4 +- .../editor/filteredEditorGroupModel.test.ts | 3 +- .../test/browser/workbenchTestServices.ts | 27 +- .../test/common/workbenchTestServices.ts | 4 +- .../treeSitterTokenizationFeature.test.ts | 149 +- .../electron-sandbox/workbenchTestServices.ts | 3 + src/vs/workbench/workbench.common.main.ts | 10 +- src/vs/workbench/workbench.desktop.main.ts | 8 +- .../workbench/workbench.web.main.internal.ts | 6 + src/vscode-dts/vscode.d.ts | 27 +- ...ode.proposed.chatParticipantAdditions.d.ts | 23 +- ...scode.proposed.chatParticipantPrivate.d.ts | 10 +- .../vscode.proposed.chatProvider.d.ts | 8 +- .../vscode.proposed.chatStatusItem.d.ts | 61 + ...scode.proposed.defaultChatParticipant.d.ts | 10 +- ...e.proposed.inlineCompletionsAdditions.d.ts | 2 +- ...vscode.proposed.languageModelDataPart.d.ts | 98 + ....proposed.languageModelToolsForAgent.d.ts} | 4 +- ...ode.proposed.mcpConfigurationProvider.d.ts | 45 + ...ode.proposed.taskProblemMatcherStatus.d.ts | 42 + .../vscode.proposed.terminalShellEnv.d.ts | 2 +- .../vscode.proposed.terminalShellType.d.ts | 40 - test/automation/package-lock.json | 327 +- test/automation/package.json | 6 +- test/automation/src/code.ts | 4 +- test/automation/src/debug.ts | 21 +- test/automation/src/editor.ts | 2 +- test/automation/src/editors.ts | 14 +- test/automation/src/explorer.ts | 4 +- test/automation/src/keybindings.ts | 14 +- test/automation/src/notebook.ts | 4 +- test/automation/src/peek.ts | 3 +- test/automation/src/playwrightDriver.ts | 12 +- test/automation/src/profiler.ts | 21 +- test/automation/src/quickaccess.ts | 31 +- test/automation/src/quickinput.ts | 14 +- test/automation/src/scm.ts | 3 +- test/automation/src/search.ts | 15 +- test/automation/src/settings.ts | 22 +- test/automation/src/task.ts | 13 +- test/automation/src/terminal.ts | 17 +- test/monaco/core.js | 2 +- test/monaco/webpack.config.js | 3 +- test/smoke/package-lock.json | 40 +- test/smoke/package.json | 3 +- .../src/areas/preferences/preferences.test.ts | 8 +- test/smoke/src/areas/search/search.test.ts | 10 +- .../terminal-shellIntegration.test.ts | 7 +- 1320 files changed, 103154 insertions(+), 34202 deletions(-) create mode 100644 .config/guardian/.gdnsuppress delete mode 100644 build/azure-pipelines/linux/product-build-linux-legacy-server.yml create mode 100644 build/lib/propertyInitOrderChecker.js create mode 100644 build/lib/propertyInitOrderChecker.ts create mode 100644 extensions/terminal-suggest/scripts/pullFishBuiltins.ts create mode 100644 extensions/terminal-suggest/scripts/terminalScriptHelpers.ts create mode 100644 extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts create mode 100644 extensions/terminal-suggest/src/completions/code-tunnel.ts create mode 100644 extensions/terminal-suggest/src/completions/npx.ts delete mode 100644 extensions/terminal-suggest/src/completions/upstream/npx.ts create mode 100644 extensions/terminal-suggest/src/helpers/promise.ts create mode 100644 extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts create mode 100644 extensions/vscode-colorize-tests/test/colorize-fixtures/test-issue241715.ts create mode 100644 extensions/vscode-colorize-tests/test/colorize-fixtures/test.regexp.ts create mode 100644 extensions/vscode-colorize-tests/test/colorize-results/test-issue241715_ts.json create mode 100644 extensions/vscode-colorize-tests/test/colorize-results/test_regexp.ts.json create mode 100644 extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue241715_ts.json create mode 100644 extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_regexp.ts.json delete mode 100755 resources/server/bin/helpers/check-requirements-linux-legacy.sh create mode 100644 src/vs/base/browser/ui/dnd/dnd.css create mode 100644 src/vs/base/browser/ui/dnd/dnd.ts rename src/vs/{platform/severityIcon/browser => base/browser/ui/severityIcon}/media/severityIcon.css (100%) rename src/vs/{platform/severityIcon/browser => base/browser/ui/severityIcon}/severityIcon.ts (82%) rename src/vs/base/browser/{defaultWorkerFactory.ts => webWorkerFactory.ts} (70%) create mode 100644 src/vs/base/common/envfile.ts create mode 100644 src/vs/base/common/policy.ts rename src/vs/base/common/worker/{simpleWorker.ts => webWorker.ts} (77%) rename src/vs/base/common/worker/{simpleWorkerBootstrap.ts => webWorkerBootstrap.ts} (64%) create mode 100644 src/vs/base/test/common/envfile.test.ts delete mode 100644 src/vs/base/worker/workerMain.ts create mode 100644 src/vs/editor/common/codecs/markdownCodec/parsers/markdownComment.ts create mode 100644 src/vs/editor/common/codecs/markdownCodec/parsers/markdownImage.ts create mode 100644 src/vs/editor/common/codecs/markdownCodec/parsers/markdownLink.ts create mode 100644 src/vs/editor/common/codecs/markdownCodec/tokens/markdownComment.ts create mode 100644 src/vs/editor/common/codecs/markdownCodec/tokens/markdownImage.ts create mode 100644 src/vs/editor/common/codecs/simpleCodec/tokens/angleBrackets.ts create mode 100644 src/vs/editor/common/codecs/simpleCodec/tokens/dash.ts create mode 100644 src/vs/editor/common/codecs/simpleCodec/tokens/exclamationMark.ts create mode 100644 src/vs/editor/common/languages/highlights/css.scm create mode 100644 src/vs/editor/common/languages/highlights/regex.scm create mode 100644 src/vs/editor/common/languages/injections/typescript.scm rename src/vs/editor/common/services/{editorSimpleWorker.ts => editorWebWorker.ts} (87%) create mode 100644 src/vs/editor/common/services/editorWebWorkerMain.ts delete mode 100644 src/vs/editor/common/services/editorWorkerBootstrap.ts create mode 100644 src/vs/editor/common/services/treeSitter/cursorUtils.ts create mode 100644 src/vs/editor/common/services/treeSitter/textModelTreeSitter.ts create mode 100644 src/vs/editor/common/services/treeSitter/treeSitterLanguages.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/structuredLogger.ts create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts rename src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/{viewAndDiffProducer.ts => inlineEditsViewProducer.ts} (53%) create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts create mode 100644 src/vs/editor/editor.worker.start.ts rename src/vs/editor/test/common/services/{editorSimpleWorker.test.ts => editorWebWorker.test.ts} (98%) create mode 100644 src/vs/platform/accessibilitySignal/browser/media/editsKept.mp3 create mode 100644 src/vs/platform/accessibilitySignal/browser/media/editsUndone.mp3 create mode 100644 src/vs/platform/dialogs/browser/dialog.ts create mode 100644 src/vs/platform/extensionManagement/common/extensionGalleryManifest.ts create mode 100644 src/vs/platform/extensionManagement/common/extensionGalleryManifestService.ts create mode 100644 src/vs/platform/extensionManagement/common/extensionGalleryManifestServiceIpc.ts create mode 100644 src/vs/platform/mcp/common/mcpManagementCli.ts create mode 100644 src/vs/platform/mcp/common/mcpPlatformTypes.ts create mode 100644 src/vs/platform/mcp/common/nativeMcpDiscoveryHelper.ts create mode 100644 src/vs/platform/mcp/node/nativeMcpDiscoveryHelperChannel.ts create mode 100644 src/vs/platform/mcp/node/nativeMcpDiscoveryHelperService.ts create mode 100644 src/vs/platform/observable/common/observableMemento.ts rename src/vs/{editor/common/services/editorSimpleWorkerMain.ts => platform/remote/common/remote.ts} (68%) create mode 100644 src/vs/platform/secrets/test/common/testSecretStorageService.ts create mode 100644 src/vs/platform/telemetry/electron-main/errorTelemetry.ts create mode 100644 src/vs/platform/webContentExtractor/common/webContentExtractor.ts create mode 100644 src/vs/platform/webContentExtractor/electron-main/cdpAccessibilityDomain.ts create mode 100644 src/vs/platform/webContentExtractor/electron-main/webContentExtractorService.ts create mode 100644 src/vs/platform/webContentExtractor/electron-sandbox/webContentExtractorService.ts create mode 100644 src/vs/platform/webContentExtractor/node/sharedWebContentExtractorService.ts create mode 100644 src/vs/workbench/api/browser/mainThreadChatStatus.ts create mode 100644 src/vs/workbench/api/browser/mainThreadMcp.ts create mode 100644 src/vs/workbench/api/common/extHostChatStatus.ts create mode 100644 src/vs/workbench/api/common/extHostMcp.ts create mode 100644 src/vs/workbench/api/node/extHostMpcNode.ts create mode 100644 src/vs/workbench/api/test/common/extHostTerminalShellIntegration.test.ts create mode 100644 src/vs/workbench/browser/media/code-icon.svg delete mode 100644 src/vs/workbench/common/release.ts delete mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt.ts create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/askToSelectPrompt.ts create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/constants.ts create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/attachPrompts.ts create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/createPlaceholderText.ts create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/createPromptPickItem.ts create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatAttachPromptAction/dialogs/askToSelectPrompt/utils/handleButtonClick.ts create mode 100644 src/vs/workbench/contrib/chat/browser/actions/chatTransfer.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatContentParts/chatCollapsibleContentPart.ts delete mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookSnapshot.ts delete mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingNotebookEditorIntegration.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookDiff.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingModifiedNotebookSnapshot.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNewNotebookContentEdits.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts rename src/vs/workbench/contrib/chat/browser/chatEditing/{ => notebook}/chatEditingNotebookFileSystemProvider.ts (83%) create mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/notebook/helpers.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatEditing/notebook/notebookCellChanges.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts create mode 100644 src/vs/workbench/contrib/chat/browser/media/chatSetup.css rename src/{vscode-dts/vscode.proposed.chatReadonlyPromptReference.d.ts => vs/workbench/contrib/chat/common/chat.ts} (54%) create mode 100644 src/vs/workbench/contrib/chat/common/chatEntitlementService.ts delete mode 100644 src/vs/workbench/contrib/chat/common/chatEntitlementsService.ts delete mode 100644 src/vs/workbench/contrib/chat/common/chatQuotasService.ts create mode 100644 src/vs/workbench/contrib/chat/common/chatSessionStore.ts create mode 100644 src/vs/workbench/contrib/chat/common/chatTransferService.ts create mode 100644 src/vs/workbench/contrib/chat/common/constants.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptVariableParser.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptToken.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptVariable.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/languageFeatures/promptLinkDiagnosticsProvider.ts create mode 100644 src/vs/workbench/contrib/chat/common/promptSyntax/parsers/topError.ts create mode 100644 src/vs/workbench/contrib/chat/common/tools/insertNotebookCellsTool.ts create mode 100644 src/vs/workbench/contrib/chat/common/tools/promptTsxTypes.ts create mode 100644 src/vs/workbench/contrib/chat/electron-sandbox/actions/chatDeveloperActions.ts create mode 100644 src/vs/workbench/contrib/chat/electron-sandbox/tools/fetchPageTool.ts create mode 100644 src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts create mode 100644 src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_but_edit_mode.0.snap delete mode 100644 src/vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.contribution.ts delete mode 100644 src/vs/workbench/contrib/configExporter/electron-sandbox/configurationExportHelper.ts create mode 100644 src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts create mode 100644 src/vs/workbench/contrib/mcp/browser/mcpCommands.ts create mode 100644 src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts create mode 100644 src/vs/workbench/contrib/mcp/browser/mcpDiscovery.ts create mode 100644 src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts create mode 100644 src/vs/workbench/contrib/mcp/browser/mcpUrlHandler.ts create mode 100644 src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts create mode 100644 src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts create mode 100644 src/vs/workbench/contrib/mcp/common/discovery/mcpDiscovery.ts create mode 100644 src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAbstract.ts create mode 100644 src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts create mode 100644 src/vs/workbench/contrib/mcp/common/discovery/nativeMcpRemoteDiscovery.ts create mode 100644 src/vs/workbench/contrib/mcp/common/discovery/workspaceMcpDiscoveryAdapter.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpConfigFileUtils.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpConfigPathsService.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpRegistry.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpServer.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpService.ts create mode 100644 src/vs/workbench/contrib/mcp/common/mcpTypes.ts create mode 100644 src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts create mode 100644 src/vs/workbench/contrib/mcp/electron-sandbox/mcp.contribution.ts create mode 100644 src/vs/workbench/contrib/mcp/electron-sandbox/nativeMpcDiscovery.ts create mode 100644 src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts create mode 100644 src/vs/workbench/contrib/mcp/test/common/mcpRegistryInputStorage.test.ts create mode 100644 src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts create mode 100644 src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts create mode 100644 src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts create mode 100644 src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditContext.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts delete mode 100644 src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts rename src/vs/workbench/contrib/notebook/browser/{contrib/chatEdit => diff/inlineDiff}/notebookOriginalCellModelFactory.ts (100%) rename src/vs/workbench/contrib/notebook/browser/{contrib/chatEdit => diff/inlineDiff}/notebookOriginalModelRefFactory.ts (100%) create mode 100644 src/vs/workbench/contrib/notebook/browser/viewParts/notebookCellOverlays.ts create mode 100644 src/vs/workbench/contrib/notebook/common/notebookDiff.ts delete mode 100644 src/vs/workbench/contrib/notebook/common/notebookSynchronizerService.ts create mode 100644 src/vs/workbench/contrib/notebook/common/services/notebookCellMatching.ts delete mode 100644 src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts create mode 100644 src/vs/workbench/contrib/notebook/common/services/notebookWebWorker.ts rename src/vs/workbench/contrib/notebook/common/services/{notebookSimpleWorkerMain.ts => notebookWebWorkerMain.ts} (65%) create mode 100644 src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiffService.test.ts create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalTelemetry.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts create mode 100644 src/vs/workbench/contrib/url/common/trustedDomains.ts create mode 100644 src/vs/workbench/services/accounts/common/defaultAccount.ts create mode 100644 src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts create mode 100644 src/vs/workbench/services/extensionManagement/browser/extensionGalleryManifestService.ts create mode 100644 src/vs/workbench/services/extensionManagement/common/extensionGalleryService.ts create mode 100644 src/vs/workbench/services/extensionManagement/electron-sandbox/extensionGalleryManifestService.ts rename src/vs/workbench/services/languageDetection/browser/{languageDetectionSimpleWorker.ts => languageDetectionWebWorker.ts} (88%) rename src/vs/workbench/services/languageDetection/browser/{languageDetectionSimpleWorkerMain.ts => languageDetectionWebWorkerMain.ts} (65%) create mode 100644 src/vs/workbench/services/policies/common/accountPolicyService.ts create mode 100644 src/vs/workbench/services/policies/common/multiplexPolicyService.ts create mode 100644 src/vs/workbench/services/workspaces/common/workspaceUtils.ts create mode 100644 src/vscode-dts/vscode.proposed.chatStatusItem.d.ts create mode 100644 src/vscode-dts/vscode.proposed.languageModelDataPart.d.ts rename src/{vs/editor/editor.worker.ts => vscode-dts/vscode.proposed.languageModelToolsForAgent.d.ts} (78%) create mode 100644 src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts create mode 100644 src/vscode-dts/vscode.proposed.taskProblemMatcherStatus.d.ts delete mode 100644 src/vscode-dts/vscode.proposed.terminalShellType.d.ts diff --git a/.config/guardian/.gdnsuppress b/.config/guardian/.gdnsuppress new file mode 100644 index 00000000..d1d93c2a --- /dev/null +++ b/.config/guardian/.gdnsuppress @@ -0,0 +1,46 @@ +{ + "hydrated": false, + "properties": { + "helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/suppressions" + }, + "version": "1.0.0", + "suppressionSets": { + "default": { + "name": "default", + "createdDate": "2025-03-17 11:52:32Z", + "lastUpdatedDate": "2025-03-17 11:52:32Z" + } + }, + "results": { + "216e2ac9cb596796224b47799f656570a01fa0d9b5f935608b47d15ab613c8e8": { + "signature": "216e2ac9cb596796224b47799f656570a01fa0d9b5f935608b47d15ab613c8e8", + "alternativeSignatures": [ + "07746898f43afab7cc50931b33154c2d9e1a35f82a649dbe8aecf785b3d5a813" + ], + "memberOf": [ + "default" + ], + "createdDate": "2025-03-17 11:52:32Z" + }, + "77797a3e44634bb2994bd13ccc95ff4575bba474585dbd2cf3068a1c16bc0624": { + "signature": "77797a3e44634bb2994bd13ccc95ff4575bba474585dbd2cf3068a1c16bc0624", + "alternativeSignatures": [ + "4a6cb67bd4b401e9669c13a2162660aaefc0a94a4122e5b50c198414db545672" + ], + "memberOf": [ + "default" + ], + "createdDate": "2025-03-17 11:52:32Z" + }, + "30418bcc5269eaeb2832a2404465784431d4e72a2af332320c2b1db4768902ad": { + "signature": "30418bcc5269eaeb2832a2404465784431d4e72a2af332320c2b1db4768902ad", + "alternativeSignatures": [ + "b7b9eb974d7d3a4ae14df8695ca5a62592c8c9d20b7eda70a6535d50cbda3e7f" + ], + "memberOf": [ + "default" + ], + "createdDate": "2025-03-17 11:52:32Z" + } + } +} diff --git a/.devcontainer/README.md b/.devcontainer/README.md index dd59adb6..3d1e15e5 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -1,6 +1,6 @@ # Code - OSS Development Container -[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/microsoft/vscode) +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/microsoft/vscode) This repository includes configuration for a development container for working with Code - OSS in a local container or using [GitHub Codespaces](https://github.com/features/codespaces). diff --git a/.eslint-ignore b/.eslint-ignore index 969f7a5b..e4931981 100644 --- a/.eslint-ignore +++ b/.eslint-ignore @@ -11,10 +11,10 @@ **/extensions/markdown-language-features/notebook-out/** **/extensions/markdown-math/notebook-out/** **/extensions/notebook-renderers/renderer-out/index.js -**/extensions/open-remote-ssh/out/extension.js **/extensions/simple-browser/media/index.js **/extensions/terminal-suggest/src/completions/upstream/** **/extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts +**/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts **/extensions/terminal-suggest/third_party/** **/extensions/typescript-language-features/test-workspace/** **/extensions/typescript-language-features/extension.webpack.config.js @@ -37,4 +37,5 @@ **/test/unit/assert.js **/test/automation/out/** **/typings/** +**/.build/** !.vscode diff --git a/.eslint-plugin-local/code-amd-node-module.ts b/.eslint-plugin-local/code-amd-node-module.ts index ff7ef6ab..b622c98a 100644 --- a/.eslint-plugin-local/code-amd-node-module.ts +++ b/.eslint-plugin-local/code-amd-node-module.ts @@ -51,7 +51,7 @@ export = new class ApiProviderNaming implements eslint.Rule.RuleModule { node, messageId: 'amdX' }); - } + }; return { ['ImportExpression Literal']: checkImport, diff --git a/.eslint-plugin-local/code-declare-service-brand.ts b/.eslint-plugin-local/code-declare-service-brand.ts index f2d28bbf..85cf0671 100644 --- a/.eslint-plugin-local/code-declare-service-brand.ts +++ b/.eslint-plugin-local/code-declare-service-brand.ts @@ -19,7 +19,7 @@ export = new class DeclareServiceBrand implements eslint.Rule.RuleModule { node, message: `The '_serviceBrand'-property should not have a value`, fix: (fixer) => { - return fixer.replaceText(node, 'declare _serviceBrand: undefined;') + return fixer.replaceText(node, 'declare _serviceBrand: undefined;'); } }); } diff --git a/.eslint-plugin-local/code-ensure-no-disposables-leak-in-test.ts b/.eslint-plugin-local/code-ensure-no-disposables-leak-in-test.ts index 56a1d4a7..c657df9b 100644 --- a/.eslint-plugin-local/code-ensure-no-disposables-leak-in-test.ts +++ b/.eslint-plugin-local/code-ensure-no-disposables-leak-in-test.ts @@ -27,7 +27,7 @@ export = new class EnsureNoDisposablesAreLeakedInTestSuite implements eslint.Rul return { [`Program > ExpressionStatement > CallExpression[callee.name='suite']`]: (node: Node) => { - const src = context.getSourceCode().getText(node) + const src = context.getSourceCode().getText(node); if (!src.includes('ensureNoDisposablesAreLeakedInTestSuite(')) { context.report({ node, diff --git a/.eslint-plugin-local/code-import-patterns.ts b/.eslint-plugin-local/code-import-patterns.ts index e4fe5241..ed14fa35 100644 --- a/.eslint-plugin-local/code-import-patterns.ts +++ b/.eslint-plugin-local/code-import-patterns.ts @@ -44,7 +44,7 @@ export = new class implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { badImport: 'Imports violates \'{{restrictions}}\' restrictions. See https://github.com/microsoft/vscode/wiki/Source-Code-Organization', - badFilename: 'Missing definition in `code-import-patterns` for this file. Define rules at https://github.com/microsoft/vscode/blob/main/.eslintrc.json', + badFilename: 'Missing definition in `code-import-patterns` for this file. Define rules at https://github.com/microsoft/vscode/blob/main/eslint.config.js', badAbsolute: 'Imports have to be relative to support ESM', badExtension: 'Imports have to end with `.js` or `.css` to support ESM', }, diff --git a/.eslint-plugin-local/code-limited-top-functions.ts b/.eslint-plugin-local/code-limited-top-functions.ts index 97eef9a6..7b48d02a 100644 --- a/.eslint-plugin-local/code-limited-top-functions.ts +++ b/.eslint-plugin-local/code-limited-top-functions.ts @@ -14,13 +14,13 @@ export = new class implements eslint.Rule.RuleModule { layerbreaker: 'You are only allowed to define limited top level functions.' }, schema: { - type: "array", + type: 'array', items: { - type: "object", + type: 'object', additionalProperties: { - type: "array", + type: 'array', items: { - type: "string" + type: 'string' } } } @@ -65,6 +65,6 @@ export = new class implements eslint.Rule.RuleModule { } } } - } + }; } }; diff --git a/.eslint-plugin-local/code-must-use-result.ts b/.eslint-plugin-local/code-must-use-result.ts index e59b1920..e249f36d 100644 --- a/.eslint-plugin-local/code-must-use-result.ts +++ b/.eslint-plugin-local/code-must-use-result.ts @@ -14,22 +14,22 @@ const VALID_USES = new Set([ export = new class MustUseResults implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { schema: false - } + }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - const config = <{ message: string, functions: string[] }[]>context.options[0]; + const config = <{ message: string; functions: string[] }[]>context.options[0]; const listener: eslint.Rule.RuleListener = {}; for (const { message, functions } of config) { for (const fn of functions) { - const query = `CallExpression[callee.property.name='${fn}'], CallExpression[callee.name='${fn}']` + const query = `CallExpression[callee.property.name='${fn}'], CallExpression[callee.name='${fn}']`; listener[query] = (node: any) => { const cast: TSESTree.CallExpression = node; if (!VALID_USES.has(cast.parent?.type)) { context.report({ node, message }); } - } + }; } } diff --git a/.eslint-plugin-local/code-must-use-super-dispose.ts b/.eslint-plugin-local/code-must-use-super-dispose.ts index 4f7f9646..ca776d8a 100644 --- a/.eslint-plugin-local/code-must-use-super-dispose.ts +++ b/.eslint-plugin-local/code-must-use-super-dispose.ts @@ -14,7 +14,7 @@ export = new class NoAsyncSuite implements eslint.Rule.RuleModule { return; } - const body = context.getSourceCode().getText(node) + const body = context.getSourceCode().getText(node); if (body.includes('super.dispose')) { return; diff --git a/.eslint-plugin-local/code-no-dangerous-type-assertions.ts b/.eslint-plugin-local/code-no-dangerous-type-assertions.ts index 233fae02..f900d778 100644 --- a/.eslint-plugin-local/code-no-dangerous-type-assertions.ts +++ b/.eslint-plugin-local/code-no-dangerous-type-assertions.ts @@ -32,7 +32,7 @@ export = new class NoDangerousTypeAssertions implements eslint.Rule.RuleModule { context.report({ node, - message: "Don't use type assertions for creating objects as this can hide type errors." + message: `Don't use type assertions for creating objects as this can hide type errors.` }); }, }; diff --git a/.eslint-plugin-local/code-no-global-document-listener.ts b/.eslint-plugin-local/code-no-global-document-listener.ts index 6b3e83fe..049426a5 100644 --- a/.eslint-plugin-local/code-no-global-document-listener.ts +++ b/.eslint-plugin-local/code-no-global-document-listener.ts @@ -25,6 +25,6 @@ export = new class NoGlobalDocumentListener implements eslint.Rule.RuleModule { }); } }, - } + }; } }; diff --git a/.eslint-plugin-local/code-no-nls-in-standalone-editor.ts b/.eslint-plugin-local/code-no-nls-in-standalone-editor.ts index 19ad65ee..c0d60985 100644 --- a/.eslint-plugin-local/code-no-nls-in-standalone-editor.ts +++ b/.eslint-plugin-local/code-no-nls-in-standalone-editor.ts @@ -24,7 +24,7 @@ export = new class NoNlsInStandaloneEditorRule implements eslint.Rule.RuleModule || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(fileName) || /vs(\/|\\)editor(\/|\\)editor.api/.test(fileName) || /vs(\/|\\)editor(\/|\\)editor.main/.test(fileName) - || /vs(\/|\\)editor(\/|\\)editor.worker/.test(fileName) + || /vs(\/|\\)editor(\/|\\)editor.worker.start/.test(fileName) ) { return createImportRuleListener((node, path) => { // resolve relative paths diff --git a/.eslint-plugin-local/code-no-runtime-import.ts b/.eslint-plugin-local/code-no-runtime-import.ts index 61597236..afebe0b0 100644 --- a/.eslint-plugin-local/code-no-runtime-import.ts +++ b/.eslint-plugin-local/code-no-runtime-import.ts @@ -16,13 +16,13 @@ export = new class implements eslint.Rule.RuleModule { layerbreaker: 'You are only allowed to import {{import}} from here using `import type ...`.' }, schema: { - type: "array", + type: 'array', items: { - type: "object", + type: 'object', additionalProperties: { - type: "array", + type: 'array', items: { - type: "string" + type: 'string' } } } diff --git a/.eslint-plugin-local/code-no-standalone-editor.ts b/.eslint-plugin-local/code-no-standalone-editor.ts index 3fad6719..36bf48b1 100644 --- a/.eslint-plugin-local/code-no-standalone-editor.ts +++ b/.eslint-plugin-local/code-no-standalone-editor.ts @@ -38,7 +38,7 @@ export = new class NoNlsInStandaloneEditorRule implements eslint.Rule.RuleModule || /vs(\/|\\)editor(\/|\\)common(\/|\\)standalone(\/|\\)/.test(path) || /vs(\/|\\)editor(\/|\\)editor.api/.test(path) || /vs(\/|\\)editor(\/|\\)editor.main/.test(path) - || /vs(\/|\\)editor(\/|\\)editor.worker/.test(path) + || /vs(\/|\\)editor(\/|\\)editor.worker.start/.test(path) ) { context.report({ loc: node.loc, diff --git a/.eslint-plugin-local/code-no-static-self-ref.ts b/.eslint-plugin-local/code-no-static-self-ref.ts index 52edfb25..f6206455 100644 --- a/.eslint-plugin-local/code-no-static-self-ref.ts +++ b/.eslint-plugin-local/code-no-static-self-ref.ts @@ -26,19 +26,19 @@ export = new class implements eslint.Rule.RuleModule { return; } - const classCtor = classDeclaration.body.body.find(node => node.type === 'MethodDefinition' && node.kind === 'constructor') + const classCtor = classDeclaration.body.body.find(node => node.type === 'MethodDefinition' && node.kind === 'constructor'); if (!classCtor) { return; } const name = classDeclaration.id.name; - const valueText = context.sourceCode.getText(propertyDefinition.value) + const valueText = context.sourceCode.getText(propertyDefinition.value); if (valueText.includes(name + '.')) { if (classCtor.value?.type === 'FunctionExpression' && !classCtor.value.params.find((param: any) => param.type === 'TSParameterProperty' && param.decorators?.length > 0)) { - return + return; } context.report({ diff --git a/.eslint-plugin-local/code-no-unused-expressions.ts b/.eslint-plugin-local/code-no-unused-expressions.ts index 14f2f53d..bd632884 100644 --- a/.eslint-plugin-local/code-no-unused-expressions.ts +++ b/.eslint-plugin-local/code-no-unused-expressions.ts @@ -58,7 +58,7 @@ module.exports = { allowTernary = config.allowTernary || false, allowTaggedTemplates = config.allowTaggedTemplates || false; - // eslint-disable-next-line jsdoc/require-description + /** * @param node any node * @returns whether the given node structurally represents a directive @@ -68,7 +68,7 @@ module.exports = { node.expression.type === 'Literal' && typeof node.expression.value === 'string'; } - // eslint-disable-next-line jsdoc/require-description + /** * @param predicate ([a] -> Boolean) the function used to make the determination * @param list the input list @@ -83,7 +83,7 @@ module.exports = { return list.slice(); } - // eslint-disable-next-line jsdoc/require-description + /** * @param node a Program or BlockStatement node * @returns the leading sequence of directive nodes in the given node's body @@ -92,7 +92,7 @@ module.exports = { return takeWhile(looksLikeDirective, node.body); } - // eslint-disable-next-line jsdoc/require-description + /** * @param node any node * @param ancestors the given node's ancestors diff --git a/.eslint-plugin-local/index.js b/.eslint-plugin-local/index.js index 9f453168..198cb836 100644 --- a/.eslint-plugin-local/index.js +++ b/.eslint-plugin-local/index.js @@ -1,3 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ const glob = require('glob'); const path = require('path'); diff --git a/.eslint-plugin-local/vscode-dts-string-type-literals.ts b/.eslint-plugin-local/vscode-dts-string-type-literals.ts index bca084c4..0f6d711a 100644 --- a/.eslint-plugin-local/vscode-dts-string-type-literals.ts +++ b/.eslint-plugin-local/vscode-dts-string-type-literals.ts @@ -19,7 +19,7 @@ export = new class ApiTypeDiscrimination implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { ['TSPropertySignature[optional=false] TSTypeAnnotation TSLiteralType Literal']: (node: any) => { - const raw = String((node).raw) + const raw = String((node).raw); if (/^('|").*\1$/.test(raw)) { @@ -29,6 +29,6 @@ export = new class ApiTypeDiscrimination implements eslint.Rule.RuleModule { }); } } - } + }; } }; diff --git a/.gitignore b/.gitignore index a40dd0d1..b73ce578 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,3 @@ vscode.db product.overrides.json *.snap.actual .vscode-test -.tmp/ -.tmp2/ -.tool-versions -src/vs/workbench/contrib/void/browser/react/out/** -src/vs/workbench/contrib/void/browser/react/src2/** diff --git a/.npmrc b/.npmrc index 74a42c31..71d98e6c 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" target="34.3.2" -ms_build_id="11044223" +ms_build_id="11161073" runtime="electron" build_from_source="true" legacy-peer-deps="true" diff --git a/.vscode-test.js b/.vscode-test.js index 917413ed..823ef615 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -19,7 +19,7 @@ const { defineConfig } = require('@vscode/test-cli'); * A list of extension folders who have opted into tests, or configuration objects. * Edit me to add more! * - * @type {Array & { label: string })>} + * @type {Array & { label: string }>} */ const extensions = [ { @@ -65,6 +65,20 @@ const extensions = [ { label: 'microsoft-authentication', mocha: { timeout: 60_000 } + }, + { + label: 'vscode-api-tests-folder', + extensionDevelopmentPath: `extensions/vscode-api-tests`, + workspaceFolder: `extensions/vscode-api-tests/testWorkspace`, + mocha: { timeout: 60_000 }, + files: 'extensions/vscode-api-tests/out/singlefolder-tests/**/*.test.js', + }, + { + label: 'vscode-api-tests-workspace', + extensionDevelopmentPath: `extensions/vscode-api-tests`, + workspaceFolder: `extensions/vscode-api-tests/testworkspace.code-workspace`, + mocha: { timeout: 60_000 }, + files: 'extensions/vscode-api-tests/out/workspace-tests/**/*.test.js', } ]; @@ -75,9 +89,12 @@ const defaultLaunchArgs = process.env.API_TESTS_EXTRA_ARGS?.split(' ') || [ const config = defineConfig(extensions.map(extension => { /** @type {import('@vscode/test-cli').TestConfiguration} */ - const config = typeof extension === 'object' - ? { files: `extensions/${extension.label}/out/**/*.test.js`, ...extension } - : { files: `extensions/${extension}/out/**/*.test.js`, label: extension }; + const config = { + platform: 'desktop', + files: `extensions/${extension.label}/out/**/*.test.js`, + extensionDevelopmentPath: `extensions/${extension.label}`, + ...extension, + }; config.mocha ??= {}; if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { diff --git a/.vscode/launch.json b/.vscode/launch.json index afd57934..f922b5d9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -251,7 +251,51 @@ "timeout": 0, "env": { "VSCODE_EXTHOST_WILL_SEND_SOCKET": null, - "VSCODE_SKIP_PRELAUNCH": "1" + "VSCODE_SKIP_PRELAUNCH": "1", + }, + "cleanUp": "wholeBrowser", + "runtimeArgs": [ + "--inspect-brk=5875", + "--no-cached-data", + "--crash-reporter-directory=${workspaceFolder}/.profile-oss/crashes", + // for general runtime freezes: https://github.com/microsoft/vscode/issues/127861#issuecomment-904144910 + "--disable-features=CalculateNativeWinOcclusion", + "--disable-extension=vscode.vscode-api-tests" + ], + "userDataDir": "${userHome}/.vscode-oss-dev", + "webRoot": "${workspaceFolder}", + "cascadeTerminateToConfigurations": [ + "Attach to Extension Host" + ], + "pauseForSourceMap": false, + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "browserLaunchLocation": "workspace", + "presentation": { + "hidden": true, + }, + }, + { + // To debug observables you also need the extension "ms-vscode.debug-value-editor" + "type": "chrome", + "request": "launch", + "name": "Launch VS Code Internal (Dev Debug)", + "windows": { + "runtimeExecutable": "${workspaceFolder}/scripts/code.bat" + }, + "osx": { + "runtimeExecutable": "${workspaceFolder}/scripts/code.sh" + }, + "linux": { + "runtimeExecutable": "${workspaceFolder}/scripts/code.sh" + }, + "port": 9222, + "timeout": 0, + "env": { + "VSCODE_EXTHOST_WILL_SEND_SOCKET": null, + "VSCODE_SKIP_PRELAUNCH": "1", + "VSCODE_DEV_DEBUG": "1", }, "cleanUp": "wholeBrowser", "runtimeArgs": [ @@ -568,6 +612,21 @@ "order": 1 } }, + { + "name": "VS Code (Debug Observables)", + "stopAll": true, + "configurations": [ + "Launch VS Code Internal (Dev Debug)", + "Attach to Main Process", + "Attach to Extension Host", + "Attach to Shared Process", + ], + "preLaunchTask": "Ensure Prelaunch Dependencies", + "presentation": { + "group": "0_vscode", + "order": 1 + } + }, { "name": "Search, Renderer, and Main processes", "configurations": [ diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index 09d6480e..3a0eaec9 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"February 2025\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"March 2025\"" }, { "kind": 1, diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index ca93d503..d778179a 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\r\n\r\n$MILESTONE=milestone:\"February 2025\"" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\r\n\r\n$MILESTONE=milestone:\"March 2025\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 0fd05ece..c9890631 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"February 2025\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n$MILESTONE=milestone:\"March 2025\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index c7674cef..401fde7e 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"February 2025\"\n" + "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"March 2025\"\n" }, { "kind": 1, diff --git a/.vscode/settings.json b/.vscode/settings.json index 59a78245..80c37853 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -49,6 +49,7 @@ "out-vscode-reh/**": true, "extensions/**/dist/**": true, "extensions/**/out/**": true, + "extensions/terminal-suggest/src/completions/upstream/**": true, "test/smoke/out/**": true, "test/automation/out/**": true, "test/integration/browser/out/**": true @@ -156,6 +157,7 @@ "application.experimental.rendererProfiling": true, "editor.experimental.asyncTokenization": true, "editor.experimental.asyncTokenizationVerification": true, + "terminal.integrated.suggest.enabled": true, "typescript.preferences.autoImportFileExcludePatterns": [ "@xterm/xterm", "@xterm/headless", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bfce54d4..0ca8d299 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -29,7 +29,7 @@ } } }, - { + { // Void added this "type": "npm", "script": "watchreactd", "label": "React - Build", @@ -89,8 +89,8 @@ "label": "VS Code - Build", "dependsOn": [ "Core - Build", - "Ext - Build", - "React - Build" + "React - Build", + "Ext - Build" ], "group": { "kind": "build", diff --git a/HOW_TO_CONTRIBUTE.md b/HOW_TO_CONTRIBUTE.md index 693ddd5c..a9ce1ab5 100644 --- a/HOW_TO_CONTRIBUTE.md +++ b/HOW_TO_CONTRIBUTE.md @@ -4,15 +4,15 @@ This is the official guide on how to contribute to Void. We want to make it as e There are a few ways to contribute: -- 💫 Complete items on the [Roadmap](https://github.com/orgs/voideditor/projects/2). +- 💫 Complete items on the [Roadmap](https://github.com/orgs/voideditor-test/projects/2). - 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs). -- 🪴 Start new Issues - see [Issues](https://github.com/voideditor/void/issues). +- 🪴 Start new Issues - see [Issues](https://github.com/voideditor-test/void/issues). ### Codebase Guide -We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to contribute! +We [highly recommend reading this](https://github.com/voideditor-test/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to contribute! The repo is not as intimidating as it first seems if you read the guide! @@ -23,11 +23,11 @@ Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`. ## Building Void -### a. Mac - Build Prerequisites +### a. Build Prerequisites - Mac If you're using a Mac, you need Python and XCode. You probably have these by default. -### b. Windows - Build Prerequisites +### b. Build Prerequisites - Windows If you're using a Windows computer, first get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community) (recommended) or [VS Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools) (not recommended). If you already have both, you might need to run the next few steps on both of them. @@ -42,7 +42,7 @@ Go to the "Individual Components" tab and select: Finally, click Install. -### c. Linux - Build Prerequisites +### c. Build Prerequisites - Linux First, run `npm install -g node-gyp`. Then: @@ -50,28 +50,27 @@ First, run `npm install -g node-gyp`. Then: - Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`. - Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute). -### d. Building Void from inside VSCode +### d. Building Void To build Void, open `void/` inside VSCode. Then open your terminal and run: 1. `npm install` to install all dependencies. -2. Build Void. +2. `npm run watchreact` to build Void's browser dependencies like React. (If this doesn't work, try `npm run buildreact`). +3. Build Void. - Press Cmd+Shift+B (Mac). - Press Ctrl+Shift+B (Windows/Linux). - This step can take ~5 min. The build is done when you see two check marks (one of the items will continue spinning indefinitely - it compiles our React code). -3. Run Void. +4. Run Void. - Run `./scripts/code.sh` (Mac/Linux). - Run `./scripts/code.bat` (Windows). -4. Nice-to-knows. +6. Nice-to-knows. - You can always press Ctrl+R (Cmd+R) inside the new window to reload and see your new changes. It's faster than Ctrl+Shift+P and `Reload Window`. - You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing. - You can kill any of the build scripts by pressing `Ctrl+D` in VSCode terminal. If you press `Ctrl+C` the script will close but will keep running in the background (to open all background scripts, just re-build). -If you get any errors, scroll down for common fixes. - #### Building Void from Terminal -To build Void from the terminal instead of from inside VSCode, follow the steps above, but instead of pressing Cmd+Shift+B, run `npm run watch`. The build is done when you see something like this: +Alternatively, if you want to build Void from the terminal, instead of pressing Cmd+Shift+B you can run `npm run watch`. The build is done when you see something like this: ``` [watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms @@ -81,17 +80,15 @@ To build Void from the terminal instead of from inside VSCode, follow the steps ``` + #### Common Fixes -- Make sure you followed the prerequisite steps above. +- Make sure you followed the prerequisite steps. - Make sure you have Node version `20.18.2` (the version in `.nvmrc`)! - If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`. -- If you get an error with React, try running `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`. - If you see missing styles, wait a few seconds and then reload. -- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, when running ./scripts/code.sh, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos) -- If you get erorrs like `The SUID sandbox helper binary was found, but is not configured correctly` when running ./scripts/code.sh, run -`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` and then run `./scripts/code.sh` again. -- If you have any other questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. +- If you have any questions, feel free to [submit an issue](https://github.com/voideditor-test/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. +- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos) ## Packaging @@ -122,7 +119,7 @@ workspace/ ``` ### Distributing -Void's maintainers distribute Void on our website and in releases. Our build pipeline is a fork of VSCodium, and it works by running GitHub Actions which create the downloadables. The build repo with more instructions lives [here](https://github.com/voideditor/void-builder). +Void's maintainers distribute Void on our website and in releases. Our build pipeline is a fork of VSCodium, and it works by running GitHub Actions which create the downloadables. The build repo with more instructions lives [here](https://github.com/voideditor-test/void-builder). ## Pull Request Guidelines diff --git a/README.md b/README.md index a4467387..eeae9f69 100644 --- a/README.md +++ b/README.md @@ -11,30 +11,30 @@ Void is the open-source Cursor alternative. -This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/email) for Discord members (see the `announcements` channel), with a waitlist for our official release. If you're new, welcome! +This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor-test.com/email) for Discord members (see the `announcements` channel), with a waitlist for our official release. If you're new, welcome! - 👋 [Discord](https://discord.gg/RSNjgaugJs) -- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md) +- 🔨 [Contribute](https://github.com/voideditor-test/void/blob/main/HOW_TO_CONTRIBUTE.md) -- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2) +- 🚙 [Roadmap](https://github.com/orgs/voideditor-test/projects/2) -- 📝 [Changelog](https://voideditor.com/changelog) +- 📝 [Changelog](https://voideditor-test.com/changelog) -- 🧭 [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) +- 🧭 [Codebase Guide](https://github.com/voideditor-test/void/blob/main/VOID_CODEBASE_GUIDE.md) ## Contributing 1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute! -2. To get started working on Void, see [`HOW_TO_CONTRIBUTE`](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md). +2. To get started working on Void, see [`HOW_TO_CONTRIBUTE`](https://github.com/voideditor-test/void/blob/main/HOW_TO_CONTRIBUTE.md). 3. We're open to collaborations and suggestions of all types - just reach out. ## Reference -Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [`VOID_CODEBASE_GUIDE`](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md). +Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [`VOID_CODEBASE_GUIDE`](https://github.com/voideditor-test/void/blob/main/VOID_CODEBASE_GUIDE.md). ## Support -Feel free to reach out in our Discord or contact us via email: hello@voideditor.com. +Feel free to reach out in our Discord or contact us via email: hello@voideditor-test.com. diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 0f17c5ed..6be514a4 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -225,7 +225,7 @@ OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- -atom/language-sass 0.62.1 - MIT +atom/language-sass 0.61.4 - MIT https://github.com/atom/language-sass The MIT License (MIT) @@ -1203,6 +1203,33 @@ to the base-name name of the original file, and an extension of txt, html, or si --------------------------------------------------------- +fish-shell 3.7.1 +https://github.com/fish-shell/fish-shell + +Fish is a smart and user-friendly command line shell. + +Copyright (C) 2005-2009 Axel Liljencrantz +Copyright (C) 2009- fish-shell contributors + +fish is free software. + +Most of fish is licensed under the GNU General Public License version 2, and +you can redistribute it and/or modify it under the terms of the GNU GPL as +published by the Free Software Foundation. + +fish also includes software licensed under the Python Software Foundation License version 2, the MIT +license, and the GNU Library General Public License version 2. + +Full licensing information is contained in doc_src/license.rst. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +more details. +--------------------------------------------------------- + +--------------------------------------------------------- + go-syntax 0.7.9 - MIT https://github.com/worlpaker/go-syntax @@ -1519,7 +1546,7 @@ SOFTWARE. --------------------------------------------------------- -jlelong/vscode-latex-basics 1.10.0 - MIT +jlelong/vscode-latex-basics 1.9.0 - MIT https://github.com/jlelong/vscode-latex-basics Copyright (c) vscode-latex-basics authors @@ -2101,7 +2128,7 @@ SOFTWARE. --------------------------------------------------------- -microsoft/vscode-mssql 1.23.0 - MIT +microsoft/vscode-mssql 1.29.0 - MIT https://github.com/microsoft/vscode-mssql ------------------------------------------ START OF LICENSE ----------------------------------------- @@ -3485,4 +3512,24 @@ Apache License WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +--------------------------------------------------------- + +--------------------------------------------------------- + +zsh 5.9 +https://github.com/zsh-users/zsh + +Unless otherwise noted in the header of specific files, files in this distribution have the licence shown below. + +However, note that certain shell functions are licensed under versions of the GNU General Public Licence. Anyone distributing the shell as a binary including those files needs to take account of this. Search shell functions for "Copyright" for specific copyright information. None of the core functions are affected by this, so those files may simply be omitted. + +-- + +The Z Shell is copyright (c) 1992-2017 Paul Falstad, Richard Coleman, Zoltán Hidvégi, Andrew Main, Peter Stephenson, Sven Wischnowsky, and others. All rights reserved. Individual authors, whether or not specifically named, retain copyright in all changes; in what follows, they are referred to as `the Zsh Development Group'. This is for convenience only and this body has no legal status. The Z shell is distributed under the following licence; any provisions made in individual files take precedence. + +Permission is hereby granted, without written agreement and without licence or royalty fees, to use, copy, modify, and distribute this software and to distribute modified versions of this software for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software. + +In no event shall the Zsh Development Group be liable to any party for direct, indirect, special, incidental, or consequential damages arising out of the use of this software and its documentation, even if the Zsh Development Group have been advised of the possibility of such damage. + +The Zsh Development Group specifically disclaim any warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The software provided hereunder is on an "as is" basis, and the Zsh Development Group have no obligation to provide maintenance, support, updates, enhancements, or modifications. --------------------------------------------------------- \ No newline at end of file diff --git a/VOID_CODEBASE_GUIDE.md b/VOID_CODEBASE_GUIDE.md index 5c70cef6..e50087cf 100644 --- a/VOID_CODEBASE_GUIDE.md +++ b/VOID_CODEBASE_GUIDE.md @@ -4,7 +4,7 @@ The Void codebase is not as intimidating as it seems! Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`. -The purpose of this document is to explain how Void's codebase works. If you want build instructions instead, see [Contributing](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md). +The purpose of this document is to explain how Void's codebase works. If you want build instructions instead, see [Contributing](https://github.com/voideditor-test/void/blob/main/HOW_TO_CONTRIBUTE.md). @@ -64,7 +64,7 @@ Sending LLM messages from the main process avoids CSP issues with local provider -**Notes:** `modelCapabilities` is an important file that must be updated when new models come out! +**Notes:** `modelCapabilities` is an important file that must be updated when new models come out! ### Apply @@ -74,16 +74,16 @@ Void has two types of Apply: **Fast Apply** (uses Search/Replace, see below), an When you click Apply and Fast Apply is enabled, we prompt the LLM to output Search/Replace block(s) like this: ``` <<<<<<< ORIGINAL -// original code goes here +// original code goes here ======= // replaced code goes here >>>>>>> UPDATED ``` -This is what allows Void to quickly apply code even on 1000-line files. It's the same as asking the LLM to press Ctrl+F and enter in a search/replace query. +This is what allows Void to quickly apply code even on 1000-line files. It's the same as asking the LLM to press Ctrl+F and enter in a search/replace query. ### Apply Inner Workings -The `editCodeService` file runs Apply. The same exact code is also used when the LLM calls the Edit tool, and when you submit Cmd+K. Just different versions of Fast/Slow Apply mode. +The `editCodeService` file runs Apply. The same exact code is also used when the LLM calls the Edit tool, and when you submit Cmd+K. Just different versions of Fast/Slow Apply mode. Here is some important terminology: - A **DiffZone** is a {startLine, endLine} region of text where we compute and show red/green areas, or **Diffs**. When any changes are made to a file, we loop through all the DiffAreas on that file and refresh its Diffs. @@ -110,7 +110,7 @@ Here's a guide to some of the terminology we're using: - **FeatureName**: Autocomplete | Chat | CtrlK | Apply - **ModelSelection**: a {providerName, modelName} pair. - **ProviderName**: The name of a provider: `'ollama'`, `'openAI'`, etc. -- **ModelName**: The name of a model (string type, eg `'gpt-4o'`). +- **ModelName**: The name of a model (string type, eg `'gpt-4o'`). - **RefreshProvider**: a provider that we ping repeatedly to update the models list. - **ChatMode** = normal | gather | agent @@ -126,7 +126,7 @@ Here's a guide to some of the terminology we're using: ### Build process -If you want to know how our build pipeline works, see our build repo [here](https://github.com/voideditor/void-builder). +If you want to know how our build pipeline works, see our build repo [here](https://github.com/voideditor-test/void-builder). diff --git a/build/.moduleignore b/build/.moduleignore index 6b7f3657..3e654cfe 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -59,6 +59,7 @@ fsevents/test/** !@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm !@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm !@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +!@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm native-keymap/binding.gyp native-keymap/build/** diff --git a/build/.npmrc b/build/.npmrc index 1b073e71..551822f7 100644 --- a/build/.npmrc +++ b/build/.npmrc @@ -2,4 +2,5 @@ disturl="https://nodejs.org/dist" runtime="node" build_from_source="true" legacy-peer-deps="true" +force_process_config="true" timeout=180000 diff --git a/build/azure-pipelines/cli/cli-compile.yml b/build/azure-pipelines/cli/cli-compile.yml index 71cd3f71..a5d8bdc1 100644 --- a/build/azure-pipelines/cli/cli-compile.yml +++ b/build/azure-pipelines/cli/cli-compile.yml @@ -42,7 +42,6 @@ steps: - script: | set -e if [ -n "$SYSROOT_ARCH" ]; then - export VSCODE_SYSROOT_PREFIX='-glibc-2.17' export VSCODE_SYSROOT_DIR=$(Build.SourcesDirectory)/.build/sysroots node -e '(async () => { const { getVSCodeSysroot } = require("../build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' if [ "$SYSROOT_ARCH" == "arm64" ]; then @@ -73,7 +72,7 @@ steps: # verify glibc requirement if [ -n "$SYSROOT_ARCH" ]; then - glibc_version="2.17" + glibc_version="2.28" while IFS= read -r line; do if [[ $line == *"GLIBC_"* ]]; then version=$(echo "$line" | awk '{print $5}' | tr -d '()') @@ -83,8 +82,8 @@ steps: fi fi done < <("$OBJDUMP" -T "$PWD/target/${{ parameters.VSCODE_CLI_TARGET }}/release/code") - if [[ "$glibc_version" != "2.17" ]]; then - echo "Error: binary has dependency on GLIBC > 2.17, found $glibc_version" + if [[ "$glibc_version" != "2.28" ]]; then + echo "Error: binary has dependency on GLIBC > 2.28, found $glibc_version" exit 1 fi fi @@ -120,22 +119,6 @@ steps: ArtifactServices.Symbol.UseAAD: false displayName: Publish Symbols - - task: CopyFiles@2 - inputs: - SourceFolder: $(Build.SourcesDirectory)/cli/target/${{ parameters.VSCODE_CLI_TARGET }}/release - Contents: 'code.*' - TargetFolder: $(Agent.TempDirectory)/binskim-cli - displayName: Copy files for BinSkim - - - task: BinSkim@4 - inputs: - InputType: Basic - Function: analyze - TargetPattern: guardianGlob - AnalyzeTargetGlob: $(Agent.TempDirectory)/binskim-cli/*.* - AnalyzeSymPath: $(Agent.TempDirectory)/binskim-cli - displayName: Run BinSkim - - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" diff --git a/build/azure-pipelines/cli/install-rust-posix.yml b/build/azure-pipelines/cli/install-rust-posix.yml index fee56e02..0607cde3 100644 --- a/build/azure-pipelines/cli/install-rust-posix.yml +++ b/build/azure-pipelines/cli/install-rust-posix.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.81 + default: 1.85 - name: targets default: [] type: object diff --git a/build/azure-pipelines/cli/install-rust-win32.yml b/build/azure-pipelines/cli/install-rust-win32.yml index 45a1cfd1..bff114fc 100644 --- a/build/azure-pipelines/cli/install-rust-win32.yml +++ b/build/azure-pipelines/cli/install-rust-win32.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.81 + default: 1.85 - name: targets default: [] type: object diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index 444e005d..8e052881 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -382,7 +382,7 @@ async function unzip(packagePath, outputPath) { }); } // Contains all of the logic for mapping details to our actual product names in CosmosDB -function getPlatform(product, os, arch, type, isLegacy) { +function getPlatform(product, os, arch, type) { switch (os) { case 'win32': switch (product) { @@ -427,12 +427,12 @@ function getPlatform(product, os, arch, type, isLegacy) { case 'client': return `linux-${arch}`; case 'server': - return isLegacy ? `server-linux-legacy-${arch}` : `server-linux-${arch}`; + return `server-linux-${arch}`; case 'web': if (arch === 'standalone') { return 'web-standalone'; } - return isLegacy ? `server-linux-legacy-${arch}-web` : `server-linux-${arch}-web`; + return `server-linux-${arch}-web`; default: throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } @@ -556,8 +556,7 @@ async function processArtifact(artifact, filePath) { await releaseService.createRelease(version, filePath, friendlyFileName); } const { product, os, arch, unprocessedType } = match.groups; - const isLegacy = artifact.name.includes('_legacy'); - const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); + const platform = getPlatform(product, os, arch, unprocessedType); const type = getRealType(unprocessedType); const size = fs_1.default.statSync(filePath).size; const stream = fs_1.default.createReadStream(filePath); @@ -610,9 +609,6 @@ async function main() { if (e('VSCODE_BUILD_STAGE_LINUX') === 'True') { stages.add('Linux'); } - if (e('VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER') === 'True') { - stages.add('LinuxLegacyServer'); - } if (e('VSCODE_BUILD_STAGE_ALPINE') === 'True') { stages.add('Alpine'); } diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index f061d043..37bf5ccc 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -694,7 +694,7 @@ interface Asset { } // Contains all of the logic for mapping details to our actual product names in CosmosDB -function getPlatform(product: string, os: string, arch: string, type: string, isLegacy: boolean): string { +function getPlatform(product: string, os: string, arch: string, type: string): string { switch (os) { case 'win32': switch (product) { @@ -739,12 +739,12 @@ function getPlatform(product: string, os: string, arch: string, type: string, is case 'client': return `linux-${arch}`; case 'server': - return isLegacy ? `server-linux-legacy-${arch}` : `server-linux-${arch}`; + return `server-linux-${arch}`; case 'web': if (arch === 'standalone') { return 'web-standalone'; } - return isLegacy ? `server-linux-legacy-${arch}-web` : `server-linux-${arch}-web`; + return `server-linux-${arch}-web`; default: throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } @@ -896,8 +896,7 @@ async function processArtifact( } const { product, os, arch, unprocessedType } = match.groups!; - const isLegacy = artifact.name.includes('_legacy'); - const platform = getPlatform(product, os, arch, unprocessedType, isLegacy); + const platform = getPlatform(product, os, arch, unprocessedType); const type = getRealType(unprocessedType); const size = fs.statSync(filePath).size; const stream = fs.createReadStream(filePath); @@ -956,7 +955,6 @@ async function main() { if (e('VSCODE_BUILD_STAGE_WINDOWS') === 'True') { stages.add('Windows'); } if (e('VSCODE_BUILD_STAGE_LINUX') === 'True') { stages.add('Linux'); } - if (e('VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER') === 'True') { stages.add('LinuxLegacyServer'); } if (e('VSCODE_BUILD_STAGE_ALPINE') === 'True') { stages.add('Alpine'); } if (e('VSCODE_BUILD_STAGE_MACOS') === 'True') { stages.add('macOS'); } if (e('VSCODE_BUILD_STAGE_WEB') === 'True') { stages.add('Web'); } diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index 9e054574..1eb387ae 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -23,10 +23,10 @@ steps: - script: npm run test-node displayName: Run unit tests (node.js) timeoutInMinutes: 15 - - script: npm run test-browser-no-install -- --sequential --browser chromium --browser webkit --tfs "Browser Unit Tests" + - script: npm run test-browser-no-install -- --browser webkit --tfs "Browser Unit Tests" env: DEBUG: "*browser*" - displayName: Run unit tests (Browser, Chromium & Webkit) + displayName: Run unit tests (Browser, Webkit) timeoutInMinutes: 30 - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: @@ -36,10 +36,10 @@ steps: - script: npm run test-node -- --build displayName: Run unit tests (node.js) timeoutInMinutes: 15 - - script: npm run test-browser-no-install -- --sequential --build --browser chromium --browser webkit --tfs "Browser Unit Tests" + - script: npm run test-browser-no-install -- --build --browser webkit --tfs "Browser Unit Tests" env: DEBUG: "*browser*" - displayName: Run unit tests (Browser, Chromium & Webkit) + displayName: Run unit tests (Browser, Webkit) timeoutInMinutes: 30 - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index e77000d4..bd37d675 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -121,6 +121,11 @@ steps: - template: ../common/install-builtin-extensions.yml@self + - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: + - script: node build/lib/policies darwin + displayName: Generate policy definitions + retryCountOnTaskFailure: 3 + - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - script: | set -e diff --git a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml b/build/azure-pipelines/linux/product-build-linux-legacy-server.yml deleted file mode 100644 index 4c26baf2..00000000 --- a/build/azure-pipelines/linux/product-build-linux-legacy-server.yml +++ /dev/null @@ -1,233 +0,0 @@ -parameters: - - name: VSCODE_QUALITY - type: string - - name: VSCODE_RUN_INTEGRATION_TESTS - type: boolean - - name: VSCODE_ARCH - type: string - -steps: - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - nodejsMirror: https://github.com/joaomoreno/node-mirror/releases/download - - - template: ../distro/download-distro.yml - - - task: AzureKeyVault@2 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" - - - task: DownloadPipelineArtifact@2 - inputs: - artifact: Compilation - path: $(Build.ArtifactStagingDirectory) - displayName: Download compilation output - - - script: tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz - displayName: Extract compilation output - - - script: | - set -e - # Start X server - ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get update - ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y pkg-config \ - dbus \ - xvfb \ - libgtk-3-0 \ - libxkbfile-dev \ - libkrb5-dev \ - libgbm1 \ - rpm \ - gcc-10 \ - g++-10 - sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb - sudo chmod +x /etc/init.d/xvfb - sudo update-rc.d xvfb defaults - sudo service xvfb start - # Start dbus session - sudo mkdir -p /var/run/dbus - DBUS_LAUNCH_RESULT=$(sudo dbus-daemon --config-file=/usr/share/dbus-1/system.conf --print-address) - echo "##vso[task.setvariable variable=DBUS_SESSION_BUS_ADDRESS]$DBUS_LAUNCH_RESULT" - displayName: Setup system services - - - script: node build/setup-npm-registry.js $NPM_REGISTRY - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry - - - script: | - set -e - # Set the private NPM registry to the global npmrc file - # so that authentication works for subfolders like build/, remote/, extensions/ etc - # which does not have their own .npmrc file - npm config set registry "$NPM_REGISTRY" - echo "##vso[task.setvariable variable=NPMRC_PATH]$(npm config get userconfig)" - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM - - - task: npmAuthenticate@0 - inputs: - workingFile: $(NPMRC_PATH) - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication - - - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - - task: Docker@1 - displayName: "Pull Docker image" - inputs: - azureSubscriptionEndpoint: vscode - azureContainerRegistry: vscodehub.azurecr.io - command: "Run an image" - imageName: vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) - containerCommand: uname - - - script: | - set -e - - for i in {1..5}; do # try 5 times - npm ci && break - if [ $i -eq 5 ]; then - echo "Npm install failed too many times" >&2 - exit 1 - fi - echo "Npm install failed $i, trying again..." - done - workingDirectory: build - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install build dependencies - - - script: | - set -e - - export VSCODE_SYSROOT_PREFIX='-glibc-2.17' - export CC=$(which gcc-10) - export CXX=$(which g++-10) - source ./build/azure-pipelines/linux/setup-env.sh --skip-sysroot - - for i in {1..5}; do # try 5 times - npm ci && break - if [ $i -eq 5 ]; then - echo "Npm install failed too many times" >&2 - exit 1 - fi - echo "Npm install failed $i, trying again..." - done - env: - npm_config_arch: $(NPM_ARCH) - VSCODE_ARCH: $(VSCODE_ARCH) - NPM_REGISTRY: "$(NPM_REGISTRY)" - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:centos7-devtoolset8-$(VSCODE_ARCH) - displayName: Install dependencies - - - script: node build/azure-pipelines/distro/mixin-npm - displayName: Mixin distro node modules - - - script: node build/azure-pipelines/distro/mixin-quality - displayName: Mixin distro quality - - - template: ../common/install-builtin-extensions.yml - - - script: | - set -e - npm run gulp vscode-linux-$(VSCODE_ARCH)-min-ci - ARCHIVE_PATH=".build/linux/client/code-${{ parameters.VSCODE_QUALITY }}-$(VSCODE_ARCH)-$(date +%s).tar.gz" - mkdir -p $(dirname $ARCHIVE_PATH) - echo "##vso[task.setvariable variable=CLIENT_PATH]$ARCHIVE_PATH" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build client - - - script: | - set -e - tar -czf $CLIENT_PATH -C .. VSCode-linux-$(VSCODE_ARCH) - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Archive client - - - script: | - set -e - export VSCODE_NODE_GLIBC="-glibc-2.17" - npm run gulp vscode-reh-linux-$(VSCODE_ARCH)-min-ci - mv ../vscode-reh-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH) # TODO@joaomoreno - ARCHIVE_PATH=".build/linux/server/vscode-server-linux-legacy-$(VSCODE_ARCH).tar.gz" - UNARCHIVE_PATH="`pwd`/../vscode-server-linux-$(VSCODE_ARCH)" - mkdir -p $(dirname $ARCHIVE_PATH) - tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH) - echo "##vso[task.setvariable variable=SERVER_PATH]$ARCHIVE_PATH" - echo "##vso[task.setvariable variable=SERVER_UNARCHIVE_PATH]$UNARCHIVE_PATH" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build server - - - script: | - set -e - export VSCODE_NODE_GLIBC="-glibc-2.17" - npm run gulp vscode-reh-web-linux-$(VSCODE_ARCH)-min-ci - mv ../vscode-reh-web-linux-$(VSCODE_ARCH) ../vscode-server-linux-$(VSCODE_ARCH)-web # TODO@joaomoreno - ARCHIVE_PATH=".build/linux/web/vscode-server-linux-legacy-$(VSCODE_ARCH)-web.tar.gz" - mkdir -p $(dirname $ARCHIVE_PATH) - tar --owner=0 --group=0 -czf $ARCHIVE_PATH -C .. vscode-server-linux-$(VSCODE_ARCH)-web - echo "##vso[task.setvariable variable=WEB_PATH]$ARCHIVE_PATH" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build server (web) - - - ${{ if or(eq(parameters.VSCODE_ARCH, 'x64'), eq(parameters.VSCODE_ARCH, 'arm64')) }}: - - script: | - set -e - EXPECTED_GLIBC_VERSION="2.17" \ - EXPECTED_GLIBCXX_VERSION="3.4.19" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - env: - SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) - displayName: Check GLIBC and GLIBCXX dependencies in server archive - - - ${{ else }}: - - script: | - set -e - EXPECTED_GLIBC_VERSION="2.17" \ - EXPECTED_GLIBCXX_VERSION="3.4.22" \ - ./build/azure-pipelines/linux/verify-glibc-requirements.sh - env: - SEARCH_PATH: $(SERVER_UNARCHIVE_PATH) - displayName: Check GLIBC and GLIBCXX dependencies in server archive - - - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: - - template: product-build-linux-test.yml - parameters: - VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} - VSCODE_RUN_UNIT_TESTS: false - VSCODE_RUN_INTEGRATION_TESTS: ${{ parameters.VSCODE_RUN_INTEGRATION_TESTS }} - VSCODE_RUN_SMOKE_TESTS: false - ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}: - PUBLISH_TASK_NAME: 1ES.PublishPipelineArtifact@1 - - - task: 1ES.PublishPipelineArtifact@1 - inputs: - targetPath: $(SERVER_PATH) - artifactName: $(ARTIFACT_PREFIX)vscode_server_linux_legacy_$(VSCODE_ARCH)_archive-unsigned - sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH) - sbomPackageName: "VS Code Linux $(VSCODE_ARCH) Legacy Server" - sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], '')) - displayName: Publish server archive - - - task: 1ES.PublishPipelineArtifact@1 - inputs: - targetPath: $(WEB_PATH) - artifactName: $(ARTIFACT_PREFIX)vscode_web_linux_legacy_$(VSCODE_ARCH)_archive-unsigned - sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH)-web - sbomPackageName: "VS Code Linux $(VSCODE_ARCH) Legacy Web" - sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], '')) - displayName: Publish web server archive diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index 1f198441..94105642 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -16,56 +16,50 @@ else fi if [ "$npm_config_arch" == "x64" ]; then - # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/132.0.6834.196/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + # Download clang based on chromium revision used by vscode + curl -s https://raw.githubusercontent.com/chromium/chromium/132.0.6834.210/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux - # Download libcxx headers and objects from upstream electron releases - DEBUG=libcxx-fetcher \ - VSCODE_LIBCXX_OBJECTS_DIR=$PWD/.build/libcxx-objects \ - VSCODE_LIBCXX_HEADERS_DIR=$PWD/.build/libcxx_headers \ - VSCODE_LIBCXXABI_HEADERS_DIR=$PWD/.build/libcxxabi_headers \ - VSCODE_ARCH="$npm_config_arch" \ - node build/linux/libcxx-fetcher.js + # Download libcxx headers and objects from upstream electron releases + DEBUG=libcxx-fetcher \ + VSCODE_LIBCXX_OBJECTS_DIR=$PWD/.build/libcxx-objects \ + VSCODE_LIBCXX_HEADERS_DIR=$PWD/.build/libcxx_headers \ + VSCODE_LIBCXXABI_HEADERS_DIR=$PWD/.build/libcxxabi_headers \ + VSCODE_ARCH="$npm_config_arch" \ + node build/linux/libcxx-fetcher.js - # Set compiler toolchain - # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.196:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.196:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.196:build/config/c++/BUILD.gn - export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" - export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" - export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -DSPDLOG_USE_STD_FORMAT -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" - export LDFLAGS="-stdlib=libc++ --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu -Wl,--lto-O0" + # Set compiler toolchain + # Flags for the client build are based on + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:build/config/c++/BUILD.gn + export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" + export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu" + export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -DSPDLOG_USE_STD_FORMAT -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export LDFLAGS="-stdlib=libc++ --sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -fuse-ld=lld -flto=thin -L$PWD/.build/libcxx-objects -lc++abi -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu -Wl,--lto-O0" - if [ "$(echo "$@" | grep -c -- "--skip-sysroot")" -eq 0 ]; then - # Set compiler toolchain for remote server - export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc - export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-g++ - export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" - export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu" - fi + # Set compiler toolchain for remote server + export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-gcc + export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/bin/x86_64-linux-gnu-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/usr/lib/x86_64-linux-gnu -L$VSCODE_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot/lib/x86_64-linux-gnu" elif [ "$npm_config_arch" == "arm64" ]; then - if [ "$(echo "$@" | grep -c -- "--skip-sysroot")" -eq 0 ]; then - # Set compiler toolchain for client native modules - export CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc - export CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ - export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" - export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" + # Set compiler toolchain for client native modules + export CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc + export CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ + export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" - # Set compiler toolchain for remote server - export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc - export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ - export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" - export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" - fi + # Set compiler toolchain for remote server + export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc + export VSCODE_REMOTE_CXX=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ + export VSCODE_REMOTE_CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot" + export VSCODE_REMOTE_LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/usr/lib/aarch64-linux-gnu -L$VSCODE_SYSROOT_DIR/aarch64-linux-gnu/aarch64-linux-gnu/sysroot/lib/aarch64-linux-gnu" elif [ "$npm_config_arch" == "arm" ]; then - if [ "$(echo "$@" | grep -c -- "--skip-sysroot")" -eq 0 ]; then - # Set compiler toolchain for client native modules - export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc - export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ - export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" - export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" - fi + # Set compiler toolchain for client native modules + export CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc + export CXX=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-g++ + export CXXFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot" + export LDFLAGS="--sysroot=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/usr/lib/arm-linux-gnueabihf -L$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/arm-rpi-linux-gnueabihf/sysroot/lib/arm-linux-gnueabihf" # Set compiler toolchain for remote server export VSCODE_REMOTE_CC=$VSCODE_SYSROOT_DIR/arm-rpi-linux-gnueabihf/bin/arm-rpi-linux-gnueabihf-gcc diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 0e5faf24..bf25633b 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -41,26 +41,14 @@ parameters: displayName: "🎯 Linux x64" type: boolean default: true - - name: VSCODE_BUILD_LINUX_X64_LEGACY_SERVER - displayName: "🎯 Linux x64 Legacy Server" - type: boolean - default: true - name: VSCODE_BUILD_LINUX_ARM64 displayName: "🎯 Linux arm64" type: boolean default: true - - name: VSCODE_BUILD_LINUX_ARM64_LEGACY_SERVER - displayName: "🎯 Linux arm64 Legacy Server" - type: boolean - default: true - name: VSCODE_BUILD_LINUX_ARMHF displayName: "🎯 Linux armhf" type: boolean default: true - - name: VSCODE_BUILD_LINUX_ARMHF_LEGACY_SERVER - displayName: "🎯 Linux armhf Legacy Server" - type: boolean - default: true - name: VSCODE_BUILD_ALPINE displayName: "🎯 Alpine x64" type: boolean @@ -118,8 +106,6 @@ variables: value: ${{ or(eq(parameters.VSCODE_BUILD_WIN32, true), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }} - name: VSCODE_BUILD_STAGE_LINUX value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }} - - name: VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER - value: ${{ or(eq(parameters.VSCODE_BUILD_LINUX_X64_LEGACY_SERVER, true), eq(parameters.VSCODE_BUILD_LINUX_ARMHF_LEGACY_SERVER, true), eq(parameters.VSCODE_BUILD_LINUX_ARM64_LEGACY_SERVER, true)) }} - name: VSCODE_BUILD_STAGE_ALPINE value: ${{ or(eq(parameters.VSCODE_BUILD_ALPINE, true), eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true)) }} - name: VSCODE_BUILD_STAGE_MACOS @@ -183,6 +169,8 @@ extends: tsa: enabled: true configFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/tsaoptions.json + binskim: + analyzeTargetGlob: '+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.exe;+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.node;+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.dll;-:file|$(Build.SourcesDirectory)/.build/**/system-setup/VSCodeSetup*.exe;-:file|$(Build.SourcesDirectory)/.build/**/user-setup/VSCodeUserSetup*.exe' codeql: runSourceLanguagesInSourceAnalysis: true compiled: @@ -279,6 +267,10 @@ extends: name: Azure Pipelines image: macOS-13 os: macOS + variables: + # todo@connor4312 to diagnose build flakes + - name: MSRUSTUP_LOG + value: debug steps: - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self parameters: @@ -292,6 +284,10 @@ extends: name: Azure Pipelines image: macOS-13 os: macOS + variables: + # todo@connor4312 to diagnose build flakes + - name: MSRUSTUP_LOG + value: debug steps: - template: build/azure-pipelines/darwin/cli-build-darwin.yml@self parameters: @@ -322,13 +318,13 @@ extends: VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false)) }}: - - stage: CustomSDL + - stage: APIScan dependsOn: [] pool: name: 1es-windows-2019-x64 os: windows jobs: - - job: WindowsSDL + - job: WindowsAPIScan steps: - template: build/azure-pipelines/win32/sdl-scan-win32.yml@self parameters: @@ -394,6 +390,10 @@ extends: timeoutInMinutes: 120 variables: VSCODE_ARCH: x64 + templateContext: + sdl: + suppression: + suppressionFile: $(Build.SourcesDirectory)\.config\guardian\.gdnsuppress steps: - template: build/azure-pipelines/win32/product-build-win32.yml@self parameters: @@ -418,6 +418,10 @@ extends: timeoutInMinutes: 90 variables: VSCODE_ARCH: arm64 + templateContext: + sdl: + suppression: + suppressionFile: $(Build.SourcesDirectory)\.config\guardian\.gdnsuppress steps: - template: build/azure-pipelines/win32/product-build-win32.yml@self parameters: @@ -547,51 +551,6 @@ extends: VSCODE_RUN_INTEGRATION_TESTS: false VSCODE_RUN_SMOKE_TESTS: false - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER'], true)) }}: - - stage: LinuxLegacyServer - dependsOn: - - Compile - pool: - name: 1es-ubuntu-20.04-x64 - os: linux - jobs: - - ${{ if eq(parameters.VSCODE_BUILD_LINUX_X64_LEGACY_SERVER, true) }}: - - job: Linuxx64LegacyServer - variables: - VSCODE_ARCH: x64 - NPM_ARCH: x64 - DISPLAY: ":10" - steps: - - template: build/azure-pipelines/linux/product-build-linux-legacy-server.yml@self - parameters: - VSCODE_ARCH: x64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_INTEGRATION_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - - - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARMHF_LEGACY_SERVER, true) }}: - - job: LinuxArmhfLegacyServer - variables: - VSCODE_ARCH: armhf - NPM_ARCH: arm - steps: - - template: build/azure-pipelines/linux/product-build-linux-legacy-server.yml@self - parameters: - VSCODE_ARCH: armhf - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_INTEGRATION_TESTS: false - - - ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARM64_LEGACY_SERVER, true) }}: - - job: LinuxArm64LegacyServer - variables: - VSCODE_ARCH: arm64 - NPM_ARCH: arm64 - steps: - - template: build/azure-pipelines/linux/product-build-linux-legacy-server.yml@self - parameters: - VSCODE_ARCH: arm64 - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_RUN_INTEGRATION_TESTS: false - - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_ALPINE'], true)) }}: - stage: Alpine dependsOn: diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index dcc1a622..6096157a 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -104,12 +104,12 @@ steps: - template: common/install-builtin-extensions.yml@self - ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}: - - script: npm exec -- npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check + - script: npm exec -- npm-run-all -lp core-ci-pr extensions-ci-pr hygiene eslint valid-layers-check property-init-order-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene (OSS) - ${{ else }}: - - script: npm exec -- npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check vscode-dts-compile-check tsec-compile-check + - script: npm exec -- npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check property-init-order-check vscode-dts-compile-check tsec-compile-check env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene (non-OSS) diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index 8ecf5e62..1127e521 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -102,7 +102,6 @@ steps: $stages = @( if ($env:VSCODE_BUILD_STAGE_WINDOWS -eq 'True') { 'Windows' } if ($env:VSCODE_BUILD_STAGE_LINUX -eq 'True') { 'Linux' } - if ($env:VSCODE_BUILD_STAGE_LINUX_LEGACY_SERVER -eq 'True') { 'LinuxLegacyServer' } if ($env:VSCODE_BUILD_STAGE_ALPINE -eq 'True') { 'Alpine' } if ($env:VSCODE_BUILD_STAGE_MACOS -eq 'True') { 'macOS' } if ($env:VSCODE_BUILD_STAGE_WEB -eq 'True') { 'Web' } diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/product-build-win32-test.yml index 09db30d1..ebc59303 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/product-build-win32-test.yml @@ -28,7 +28,7 @@ steps: - powershell: npm run test-node displayName: Run unit tests (node.js) timeoutInMinutes: 15 - - powershell: node test/unit/browser/index.js --sequential --browser chromium --tfs "Browser Unit Tests" + - powershell: node test/unit/browser/index.js --browser chromium --tfs "Browser Unit Tests" displayName: Run unit tests (Browser, Chromium) timeoutInMinutes: 20 @@ -39,7 +39,7 @@ steps: - powershell: npm run test-node -- --build displayName: Run unit tests (node.js) timeoutInMinutes: 15 - - powershell: npm run test-browser-no-install -- --sequential --build --browser chromium --tfs "Browser Unit Tests" + - powershell: npm run test-browser-no-install -- --build --browser chromium --tfs "Browser Unit Tests" displayName: Run unit tests (Browser, Chromium) timeoutInMinutes: 20 diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index ed7f1004..b10de41d 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -124,7 +124,7 @@ steps: - template: ../common/install-builtin-extensions.yml@self - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'oss')) }}: - - powershell: node build\lib\policies + - powershell: node build\lib\policies win32 displayName: Generate Group Policy definitions retryCountOnTaskFailure: 3 @@ -145,7 +145,7 @@ steps: exec { npm run gulp "vscode-win32-$(VSCODE_ARCH)-min-ci" } exec { npm run gulp "vscode-win32-$(VSCODE_ARCH)-inno-updater" } echo "##vso[task.setvariable variable=BUILT_CLIENT]true" - echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)" + echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build client @@ -156,7 +156,7 @@ steps: exec { npm run gulp "vscode-reh-win32-$(VSCODE_ARCH)-min-ci" } mv ..\vscode-reh-win32-$(VSCODE_ARCH) ..\vscode-server-win32-$(VSCODE_ARCH) # TODO@joaomoreno echo "##vso[task.setvariable variable=BUILT_SERVER]true" - echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(agent.builddirectory)/vscode-server-win32-$(VSCODE_ARCH)" + echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server @@ -196,10 +196,10 @@ steps: $ErrorActionPreference = "Stop" $ArtifactName = (gci -Path "$(Build.ArtifactStagingDirectory)/cli" | Select-Object -last 1).FullName Expand-Archive -Path $ArtifactName -DestinationPath "$(Build.ArtifactStagingDirectory)/cli" - $AppProductJson = Get-Content -Raw -Path "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)\resources\app\product.json" | ConvertFrom-Json + $AppProductJson = Get-Content -Raw -Path "$(Agent.BuildDirectory)\VSCode-win32-$(VSCODE_ARCH)\resources\app\product.json" | ConvertFrom-Json $CliAppName = $AppProductJson.tunnelApplicationName $AppName = $AppProductJson.applicationName - Move-Item -Path "$(Build.ArtifactStagingDirectory)/cli/$AppName.exe" -Destination "$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)/bin/$CliAppName.exe" + Move-Item -Path "$(Build.ArtifactStagingDirectory)/cli/$AppName.exe" -Destination "$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)/bin/$CliAppName.exe" displayName: Move VS Code CLI - task: UseDotNet@2 @@ -231,6 +231,11 @@ steps: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: Codesign executables and shared libraries + - powershell: node build\azure-pipelines\common\sign $env:EsrpCliDllPath sign-windows-appx $(CodeSigningFolderPath) '*.ps1' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: Codesign Powershell scripts + - ${{ if eq(parameters.VSCODE_QUALITY, 'insider') }}: - powershell: node build\azure-pipelines\common\sign $env:EsrpCliDllPath sign-windows-appx $(CodeSigningFolderPath) '*.appx' env: @@ -247,13 +252,16 @@ steps: condition: succeededOrFailed() displayName: Get product version - - powershell: | + - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $ArchivePath = ".build\win32-$(VSCODE_ARCH)\VSCode-win32-$(VSCODE_ARCH)-$(VSCODE_VERSION).zip" New-Item -ItemType Directory -Path .build\win32-$(VSCODE_ARCH) -Force - exec { 7z.exe a -tzip $ArchivePath -x!CodeSignSummary*.md ..\VSCode-win32-$(VSCODE_ARCH)\* -r } + exec { 7z.exe a -tzip $ArchivePath ..\VSCode-win32-$(VSCODE_ARCH)\* "-xr!CodeSignSummary*.md" } echo "##vso[task.setvariable variable=CLIENT_PATH]$ArchivePath" + + echo "Listing archive contents" + 7z.exe l $ArchivePath condition: and(succeededOrFailed(), eq(variables['BUILT_CLIENT'], 'true')) displayName: Package client @@ -262,8 +270,11 @@ steps: $ErrorActionPreference = "Stop" $ArchivePath = ".build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH).zip" New-Item -ItemType Directory -Path .build\win32-$(VSCODE_ARCH) -Force - exec { 7z.exe a -tzip $ArchivePath ..\vscode-server-win32-$(VSCODE_ARCH) -r } + exec { 7z.exe a -tzip $ArchivePath ..\vscode-server-win32-$(VSCODE_ARCH) } echo "##vso[task.setvariable variable=SERVER_PATH]$ArchivePath" + + echo "Listing archive contents" + 7z.exe l $ArchivePath condition: and(succeededOrFailed(), eq(variables['BUILT_SERVER'], 'true')) displayName: Package server @@ -272,32 +283,30 @@ steps: $ErrorActionPreference = "Stop" $ArchivePath = ".build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH)-web.zip" New-Item -ItemType Directory -Path .build\win32-$(VSCODE_ARCH) -Force - exec { 7z.exe a -tzip $ArchivePath ..\vscode-server-win32-$(VSCODE_ARCH)-web -r } + exec { 7z.exe a -tzip $ArchivePath ..\vscode-server-win32-$(VSCODE_ARCH)-web } echo "##vso[task.setvariable variable=WEB_PATH]$ArchivePath" + + echo "Listing archive contents" + 7z.exe l $ArchivePath condition: and(succeededOrFailed(), eq(variables['BUILT_WEB'], 'true')) displayName: Package server (web) - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { npm run -- gulp "vscode-win32-$(VSCODE_ARCH)-system-setup" --sign } - $SetupPath = ".build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup-$(VSCODE_ARCH)-$(VSCODE_VERSION).exe" - mv .build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe $SetupPath - echo "##vso[task.setvariable variable=SYSTEM_SETUP_PATH]$SetupPath" - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Build system setup + exec { npm exec -- npm-run-all -lp "gulp vscode-win32-$(VSCODE_ARCH)-system-setup -- --sign" "gulp vscode-win32-$(VSCODE_ARCH)-user-setup -- --sign" } - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm run -- gulp "vscode-win32-$(VSCODE_ARCH)-user-setup" --sign } - $SetupPath = ".build\win32-$(VSCODE_ARCH)\user-setup\VSCodeUserSetup-$(VSCODE_ARCH)-$(VSCODE_VERSION).exe" - mv .build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe $SetupPath - echo "##vso[task.setvariable variable=USER_SETUP_PATH]$SetupPath" + $SystemSetupPath = ".build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup-$(VSCODE_ARCH)-$(VSCODE_VERSION).exe" + $UserSetupPath = ".build\win32-$(VSCODE_ARCH)\user-setup\VSCodeUserSetup-$(VSCODE_ARCH)-$(VSCODE_VERSION).exe" + + mv .build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe $SystemSetupPath + mv .build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe $UserSetupPath + + echo "##vso[task.setvariable variable=SYSTEM_SETUP_PATH]$SystemSetupPath" + echo "##vso[task.setvariable variable=USER_SETUP_PATH]$UserSetupPath" env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: Build user setup + displayName: Build setup packages (system, user) - powershell: echo "##vso[task.setvariable variable=ARTIFACT_PREFIX]attempt$(System.JobAttempt)_" condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) diff --git a/build/azure-pipelines/win32/sdl-scan-win32.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml index def3cb53..bf6819a4 100644 --- a/build/azure-pipelines/win32/sdl-scan-win32.yml +++ b/build/azure-pipelines/win32/sdl-scan-win32.yml @@ -102,13 +102,6 @@ steps: - powershell: npm run compile displayName: Compile - - powershell: | - Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.exe" - Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.dll" - Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.node" - Get-ChildItem '$(Build.SourcesDirectory)' -Recurse -Filter "*.pdb" - displayName: List files - - powershell: npm run gulp "vscode-symbols-win32-${{ parameters.VSCODE_ARCH }}" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" @@ -119,16 +112,7 @@ steps: Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.dll" Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.node" Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.pdb" - displayName: List files again - - - task: BinSkim@4 - inputs: - InputType: "Basic" - Function: "analyze" - TargetPattern: "guardianGlob" - AnalyzeIgnorePdbLoadError: true - AnalyzeTargetGlob: '$(Agent.BuildDirectory)\scanbin\**.dll;$(Agent.BuildDirectory)\scanbin\**.exe;$(Agent.BuildDirectory)\scanbin\**.node' - AnalyzeLocalSymbolDirectories: '$(Agent.BuildDirectory)\scanbin\VSCode-win32-${{ parameters.VSCODE_ARCH }}\pdb' + displayName: List files - task: CopyFiles@2 displayName: 'Collect Symbols for API Scan' @@ -139,19 +123,6 @@ steps: flattenFolders: true condition: succeeded() - - task: PublishSymbols@2 - inputs: - IndexSources: false - SymbolsFolder: '$(Agent.BuildDirectory)\symbols' - SearchPattern: '**\*.pdb' - SymbolServerType: TeamServices - SymbolsProduct: 'code' - ArtifactServices.Symbol.AccountName: microsoft - ArtifactServices.Symbol.PAT: $(System.AccessToken) - ArtifactServices.Symbol.UseAAD: false - displayName: Publish Symbols - condition: succeeded() - - task: APIScan@2 inputs: softwareFolder: $(Agent.BuildDirectory)\scanbin diff --git a/build/buildfile.js b/build/buildfile.js index 683e20fc..9430fb3d 100644 --- a/build/buildfile.js +++ b/build/buildfile.js @@ -5,31 +5,22 @@ /** * @param {string} name - * @param {string[]=} exclude * @returns {import('./lib/bundle').IEntryPoint} */ -function createModuleDescription(name, exclude) { +function createModuleDescription(name) { return { - name, - exclude + name }; } -/** - * @param {string} name - */ -function createEditorWorkerModuleDescription(name) { - return createModuleDescription(name, ['vs/base/common/worker/simpleWorker', 'vs/editor/common/services/editorSimpleWorker']); -} - -exports.workerEditor = createEditorWorkerModuleDescription('vs/editor/common/services/editorSimpleWorkerMain'); -exports.workerExtensionHost = createEditorWorkerModuleDescription('vs/workbench/api/worker/extensionHostWorkerMain'); -exports.workerNotebook = createEditorWorkerModuleDescription('vs/workbench/contrib/notebook/common/services/notebookSimpleWorkerMain'); -exports.workerLanguageDetection = createEditorWorkerModuleDescription('vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorkerMain'); -exports.workerLocalFileSearch = createEditorWorkerModuleDescription('vs/workbench/services/search/worker/localFileSearchMain'); -exports.workerProfileAnalysis = createEditorWorkerModuleDescription('vs/platform/profiling/electron-sandbox/profileAnalysisWorkerMain'); -exports.workerOutputLinks = createEditorWorkerModuleDescription('vs/workbench/contrib/output/common/outputLinkComputerMain'); -exports.workerBackgroundTokenization = createEditorWorkerModuleDescription('vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.workerMain'); +exports.workerEditor = createModuleDescription('vs/editor/common/services/editorWebWorkerMain'); +exports.workerExtensionHost = createModuleDescription('vs/workbench/api/worker/extensionHostWorkerMain'); +exports.workerNotebook = createModuleDescription('vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain'); +exports.workerLanguageDetection = createModuleDescription('vs/workbench/services/languageDetection/browser/languageDetectionWebWorkerMain'); +exports.workerLocalFileSearch = createModuleDescription('vs/workbench/services/search/worker/localFileSearchMain'); +exports.workerProfileAnalysis = createModuleDescription('vs/platform/profiling/electron-sandbox/profileAnalysisWorkerMain'); +exports.workerOutputLinks = createModuleDescription('vs/workbench/contrib/output/common/outputLinkComputerMain'); +exports.workerBackgroundTokenization = createModuleDescription('vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.workerMain'); exports.workbenchDesktop = [ createModuleDescription('vs/workbench/contrib/debug/node/telemetryApp'), diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index d58fe966..d390d7a2 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -72,4 +72,4 @@ f7db8ebe91a1cc8d24ef6aad12949a18d8e4975ac296e3e5e9ecd88c9bccb143 *mksnapshot-v34 4da23a950bfcc377ef21c37d496017ab4c36da03f3b41049ac114042c42608ce *mksnapshot-v34.3.2-mas-x64.zip fab59573d3c2f9bdf31146a1896d24ac0c51f736aad86d2f3c7ecef13c05a7fd *mksnapshot-v34.3.2-win32-arm64-x64.zip 66f25e07c6f8d5d2009577a129440255a3baf63c929a5b60b2e77cd52e46105b *mksnapshot-v34.3.2-win32-ia32.zip -8168bfbf61882cfac80aed1e71e364e1c7f2fccd11eac298e6abade8b46894ea *mksnapshot-v34.3.2-win32-x64.zip \ No newline at end of file +8168bfbf61882cfac80aed1e71e364e1c7f2fccd11eac298e6abade8b46894ea *mksnapshot-v34.3.2-win32-x64.zip diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index d00d52bc..d394605d 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -fa76d5b5340f14070ebaa88ef8faa28c1e9271502725e830cb52f0cf5b6493de node-v20.18.2-darwin-arm64.tar.gz -00a16bb0a82a2ad5d00d66b466ae1afa678482283747c27e9bce96668f334744 node-v20.18.2-darwin-x64.tar.gz -319789e8a055ff80793a05e633c8c5c9226050144a09da3747225b4ec56a2a99 node-v20.18.2-linux-arm64.tar.gz -65397a4a63960bda94718099698d2961623e9ef400f60f4c3a71add2268bccfb node-v20.18.2-linux-armv7l.tar.gz -eb5b031bdd728871c3b9a82655dbfa533bc262c0b6da1d09a86842430cef07d4 node-v20.18.2-linux-x64.tar.gz -83e7ad1b8c4d4d9c5e06849c3e8f3a5948a5eb6aa34c5bd973ba700e0386f42c win-arm64/node.exe -8487a277e92282904dfe0f860dbd5d229543e97a858a223fbe9c9b8670bbe170 win-x64/node.exe +1f15b7ed18a580af31cf32bc126572292d820f547bf55bf9cdce08041a24e1d9 node-v20.18.3-darwin-arm64.tar.gz +ba668f64df9239843fefcef095ee539f5ac5aa1b0fc15a71f1ecca16abedec7a node-v20.18.3-darwin-x64.tar.gz +93a9df19238adfaa289f4784041d03edaf2fdd89fbb247faffca2fe4a1000703 node-v20.18.3-linux-arm64.tar.gz +8a84eb34287db6a273066934d7195e429f57b91686b62fc19497210204a2b3de node-v20.18.3-linux-armv7l.tar.gz +9fc3952da39b20d1fcfdb777b198cc035485afbbb1004b4df93f35245d61151e node-v20.18.3-linux-x64.tar.gz +4258e333f4b95060681d61bffa762542a8068547d3dffebe57c575b38d380dda win-arm64/node.exe +528a9aa64888a2a3ba71c6aea89434dd5ab5cb3caa9f0f31345cf5facf685ab0 win-x64/node.exe diff --git a/build/checksums/vscode-sysroot.txt b/build/checksums/vscode-sysroot.txt index 0b5f38c6..67182b07 100644 --- a/build/checksums/vscode-sysroot.txt +++ b/build/checksums/vscode-sysroot.txt @@ -1,6 +1,3 @@ -68a17006021975ff271a1dd615f9db9eda7c25f2cc65e750c87980dc57a06c94 aarch64-linux-gnu-glibc-2.17.tar.gz 0de422a81683cf9e8cf875dbd1e0c27545ac3c775b2d53015daf3ca2b31d3f15 aarch64-linux-gnu-glibc-2.28.tar.gz -3ced48cb479f2cdba95aa649710fcb7778685551c745bbd76ac706c3c0ead9fb arm-rpi-linux-gnueabihf-glibc-2.17.tar.gz 7aea163f7fad8cc50000c86b5108be880121d35e2f55d016ef8c96bbe54129eb arm-rpi-linux-gnueabihf-glibc-2.28.tar.gz -5aae21115f1d284c3cdf32c83db15771b59bc80793f1423032abf5a823c0d658 x86_64-linux-gnu-glibc-2.17.tar.gz dbb927408393041664a020661f2641c9785741be3d29b050b9dac58980967784 x86_64-linux-gnu-glibc-2.28.tar.gz diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index 535d46eb..7d3f9164 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -27,6 +27,7 @@ async function main(buildDir) { const filesToSkip = [ '**/CodeResources', '**/Credits.rtf', + '**/policies/{*.mobileconfig,**/*.plist}', // TODO: Should we consider expanding this to other files in this area? '**/node_modules/@parcel/node-addon-api/nothing.target.mk' ]; diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 9e013cdb..7872eccc 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -28,6 +28,7 @@ async function main(buildDir?: string) { const filesToSkip = [ '**/CodeResources', '**/Credits.rtf', + '**/policies/{*.mobileconfig,**/*.plist}', // TODO: Should we consider expanding this to other files in this area? '**/node_modules/@parcel/node-addon-api/nothing.target.mk' ]; diff --git a/build/darwin/sign.js b/build/darwin/sign.js index 2dbc5702..dff30fd0 100644 --- a/build/darwin/sign.js +++ b/build/darwin/sign.js @@ -54,11 +54,9 @@ async function main(buildDir) { ...defaultOpts, // TODO(deepak1556): Incorrectly declared type in electron-osx-sign ignore: (filePath) => { - const ext = path_1.default.extname(filePath); return filePath.includes(gpuHelperAppName) || filePath.includes(rendererHelperAppName) || - filePath.includes(pluginHelperAppName) || - ext === '.asar' || ext === '.dat' || ext === '.gif' || ext === '.icns' || ext === '.ico' || ext === '.json' || ext === '.mp3' || ext === '.nib' || ext === '.pak' || ext === '.png' || ext === '.scpt' || ext === '.ttf' || ext === '.wasm' || ext === '.woff' || ext === '.woff2'; + filePath.includes(pluginHelperAppName); } }; const gpuHelperOpts = { @@ -83,7 +81,7 @@ async function main(buildDir) { // universal will get its copy from the x64 build. if (arch !== 'universal') { await (0, cross_spawn_promise_1.spawn)('plutil', [ - '-replace', // Void changed this to replace + '-insert', 'NSAppleEventsUsageDescription', '-string', 'An application in Visual Studio Code wants to use AppleScript.', diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index cff07c75..ecf16274 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -58,11 +58,9 @@ async function main(buildDir?: string): Promise { ...defaultOpts, // TODO(deepak1556): Incorrectly declared type in electron-osx-sign ignore: (filePath: string) => { - const ext = path.extname(filePath); return filePath.includes(gpuHelperAppName) || filePath.includes(rendererHelperAppName) || - filePath.includes(pluginHelperAppName) || - ext === '.asar' || ext === '.dat' || ext === '.gif' || ext === '.icns' || ext === '.ico' || ext === '.json' || ext === '.mp3' || ext === '.nib' || ext === '.pak' || ext === '.png' || ext === '.scpt' || ext === '.ttf' || ext === '.wasm' || ext === '.woff' || ext === '.woff2'; + filePath.includes(pluginHelperAppName); } }; @@ -91,7 +89,7 @@ async function main(buildDir?: string): Promise { // universal will get its copy from the x64 build. if (arch !== 'universal') { await spawn('plutil', [ - '-replace', // Void changed this to replace + '-insert', 'NSAppleEventsUsageDescription', '-string', 'An application in Visual Studio Code wants to use AppleScript.', diff --git a/build/filters.js b/build/filters.js index 4d19dcb1..4f18bf66 100644 --- a/build/filters.js +++ b/build/filters.js @@ -57,6 +57,7 @@ module.exports.unicodeFilter = [ '!extensions/**/out/**', '!extensions/**/snippets/**', '!extensions/**/colorize-fixtures/**', + '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', '!src/vs/base/browser/dompurify/**', '!src/vs/workbench/services/keybinding/browser/keyboardLayouts/**', @@ -79,6 +80,7 @@ module.exports.indentationFilter = [ '!src/vs/base/node/terminateProcess.sh', '!src/vs/base/node/cpuUsage.sh', '!src/vs/editor/common/languages/highlights/*.scm', + '!src/vs/editor/common/languages/injections/*.scm', '!test/unit/assert.js', '!resources/linux/snap/electron-launch', '!build/ext.js', @@ -90,6 +92,7 @@ module.exports.indentationFilter = [ '!test/monaco/out/**', '!test/smoke/out/**', '!extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts', + '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', '!extensions/terminal-suggest/src/completions/upstream/**', '!extensions/typescript-language-features/test-workspace/**', '!extensions/typescript-language-features/resources/walkthroughs/**', @@ -137,9 +140,6 @@ module.exports.indentationFilter = [ '!extensions/ipynb/notebook-out/**', '!extensions/notebook-renderers/renderer-out/*.js', '!extensions/simple-browser/media/*.js', - - '!extensions/open-remote-ssh/out/*.js', // Void added this - '!extensions/open-remote-wsl/out/*.js', // Void added this ]; module.exports.copyrightFilter = [ @@ -198,6 +198,7 @@ module.exports.tsFormattingFilter = [ '!extensions/**/*.test.ts', '!extensions/html-language-features/server/lib/jquery.d.ts', '!extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts', + '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', ]; module.exports.eslintFilter = [ @@ -205,6 +206,7 @@ module.exports.eslintFilter = [ '**/*.cjs', '**/*.mjs', '**/*.ts', + '.eslint-plugin-local/**/*.ts', ...readFileSync(join(__dirname, '..', '.eslint-ignore')) .toString() .split(/\r\n|\n/) diff --git a/build/gulpfile.compile.js b/build/gulpfile.compile.js index e40b05f8..0c0a024c 100644 --- a/build/gulpfile.compile.js +++ b/build/gulpfile.compile.js @@ -24,12 +24,12 @@ function makeCompileBuildTask(disableMangle) { ); } -// Full compile, including nls and inline sources in sourcemaps, mangling, minification, for build -const compileBuildTask = task.define('compile-build', makeCompileBuildTask(false)); -gulp.task(compileBuildTask); -exports.compileBuildTask = compileBuildTask; +// Local/PR compile, including nls and inline sources in sourcemaps, minification, no mangling +const compileBuildWithoutManglingTask = task.define('compile-build-without-mangling', makeCompileBuildTask(true)); +gulp.task(compileBuildWithoutManglingTask); +exports.compileBuildWithoutManglingTask = compileBuildWithoutManglingTask; -// Full compile for PR ci, e.g no mangling -const compileBuildTaskPullRequest = task.define('compile-build-pr', makeCompileBuildTask(true)); -gulp.task(compileBuildTaskPullRequest); -exports.compileBuildTaskPullRequest = compileBuildTaskPullRequest; +// CI compile, including nls and inline sources in sourcemaps, mangling, minification, for build +const compileBuildWithManglingTask = task.define('compile-build-with-mangling', makeCompileBuildTask(false)); +gulp.task(compileBuildWithManglingTask); +exports.compileBuildWithManglingTask = compileBuildWithManglingTask; diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 9f7ea574..4787605d 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +//@ts-check + const gulp = require('gulp'); const path = require('path'); const util = require('./lib/util'); const { getVersion } = require('./lib/getVersion'); const task = require('./lib/task'); -const optimize = require('./lib/optimize'); const es = require('event-stream'); const File = require('vinyl'); const i18n = require('./lib/i18n'); @@ -17,39 +18,13 @@ const cp = require('child_process'); const compilation = require('./lib/compilation'); const monacoapi = require('./lib/monaco-api'); const fs = require('fs'); +const filter = require('gulp-filter'); const root = path.dirname(__dirname); const sha1 = getVersion(root); const semver = require('./monaco/package.json').version; const headerVersion = semver + '(' + sha1 + ')'; -// Build - -const editorEntryPoints = [ - { - name: 'vs/editor/editor.main', - include: [], - exclude: ['vs/css'], - prepend: [ - { path: 'out-editor-build/vs/css.js', amdModuleId: 'vs/css' } - ], - }, - { - name: 'vs/base/common/worker/simpleWorker', - include: ['vs/editor/common/services/editorSimpleWorker'], - exclude: [], - prepend: [ - { path: 'vs/loader.js' }, - { path: 'vs/base/worker/workerMain.js' } - ], - dest: 'vs/base/worker/workerMain.js' - } -]; - -const editorResources = [ - 'out-editor-build/vs/base/browser/ui/codicons/**/*.ttf' -]; - const BUNDLED_FILE_HEADER = [ '/*!-----------------------------------------------------------', ' * Copyright (c) Microsoft Corporation. All rights reserved.', @@ -60,8 +35,6 @@ const BUNDLED_FILE_HEADER = [ '' ].join('\n'); -const languages = i18n.defaultLanguages.concat([]); // i18n.defaultLanguages.concat(process.env.VSCODE_QUALITY !== 'stable' ? i18n.extraLanguages : []); - const extractEditorSrcTask = task.define('extract-editor-src', () => { const apiusages = monacoapi.execute().usageContent; const extrausages = fs.readFileSync(path.join(root, 'build', 'monaco', 'monaco.usage.recipe')).toString(); @@ -69,128 +42,43 @@ const extractEditorSrcTask = task.define('extract-editor-src', () => { sourcesRoot: path.join(root, 'src'), entryPoints: [ 'vs/editor/editor.main', - 'vs/editor/editor.worker', - 'vs/base/worker/workerMain', + 'vs/editor/editor.worker.start', + 'vs/editor/common/services/editorWebWorkerMain', ], inlineEntryPoints: [ apiusages, extrausages ], + typings: [], shakeLevel: 2, // 0-Files, 1-InnerFile, 2-ClassMembers importIgnorePattern: /\.css$/, destRoot: path.join(root, 'out-editor-src'), + tsOutDir: '../out-monaco-editor-core/esm/vs', redirects: { '@vscode/tree-sitter-wasm': '../node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter', } }); }); -// Disable mangling for the editor, as it complicates debugging & quite a few users rely on private/protected fields. -// Disable NLS task to remove english strings to preserve backwards compatibility when we removed the `vs/nls!` AMD plugin. -const compileEditorAMDTask = task.define('compile-editor-amd', compilation.compileTask('out-editor-src', 'out-editor-build', true, { disableMangle: true, preserveEnglish: true })); - -const bundleEditorAMDTask = task.define('bundle-editor-amd', optimize.bundleTask( - { - out: 'out-editor', - esm: { - src: 'out-editor-build', - entryPoints: editorEntryPoints, - resources: editorResources - } - } -)); - -const minifyEditorAMDTask = task.define('minify-editor-amd', optimize.minifyTask('out-editor')); - -const createESMSourcesAndResourcesTask = task.define('extract-editor-esm', () => { - standalone.createESMSourcesAndResources2({ - srcFolder: './out-editor-src', - outFolder: './out-editor-esm', - outResourcesFolder: './out-monaco-editor-core/esm', - ignores: [ - 'inlineEntryPoint:0.ts', - 'inlineEntryPoint:1.ts', - 'vs/loader.js', - 'vs/base/worker/workerMain.ts', - ], - renames: { - } - }); -}); - const compileEditorESMTask = task.define('compile-editor-esm', () => { - const KEEP_PREV_ANALYSIS = false; - const FAIL_ON_PURPOSE = false; - console.log(`Launching the TS compiler at ${path.join(__dirname, '../out-editor-esm')}...`); - let result; - if (process.platform === 'win32') { - result = cp.spawnSync(`..\\node_modules\\.bin\\tsc.cmd`, { - cwd: path.join(__dirname, '../out-editor-esm'), - shell: true - }); - } else { - result = cp.spawnSync(`node`, [`../node_modules/.bin/tsc`], { - cwd: path.join(__dirname, '../out-editor-esm') - }); - } - console.log(result.stdout.toString()); - console.log(result.stderr.toString()); + const src = 'out-editor-src'; + const out = 'out-monaco-editor-core/esm'; - if (FAIL_ON_PURPOSE || result.status !== 0) { - console.log(`The TS Compilation failed, preparing analysis folder...`); - const destPath = path.join(__dirname, '../../vscode-monaco-editor-esm-analysis'); - const keepPrevAnalysis = (KEEP_PREV_ANALYSIS && fs.existsSync(destPath)); - const cleanDestPath = (keepPrevAnalysis ? Promise.resolve() : util.rimraf(destPath)()); - return cleanDestPath.then(() => { - // build a list of files to copy - const files = util.rreddir(path.join(__dirname, '../out-editor-esm')); + const compile = compilation.createCompile(src, { build: true, emitError: true, transpileOnly: false, preserveEnglish: true }); + const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); - if (!keepPrevAnalysis) { - fs.mkdirSync(destPath); - - // initialize a new repository - cp.spawnSync(`git`, [`init`], { - cwd: destPath - }); - - // copy files from src - for (const file of files) { - const srcFilePath = path.join(__dirname, '../src', file); - const dstFilePath = path.join(destPath, file); - if (fs.existsSync(srcFilePath)) { - util.ensureDir(path.dirname(dstFilePath)); - const contents = fs.readFileSync(srcFilePath).toString().replace(/\r\n|\r|\n/g, '\n'); - fs.writeFileSync(dstFilePath, contents); - } - } - - // create an initial commit to diff against - cp.spawnSync(`git`, [`add`, `.`], { - cwd: destPath - }); - - // create the commit - cp.spawnSync(`git`, [`commit`, `-m`, `"original sources"`, `--no-gpg-sign`], { - cwd: destPath - }); - } - - // copy files from tree shaken src - for (const file of files) { - const srcFilePath = path.join(__dirname, '../out-editor-src', file); - const dstFilePath = path.join(destPath, file); - if (fs.existsSync(srcFilePath)) { - util.ensureDir(path.dirname(dstFilePath)); - const contents = fs.readFileSync(srcFilePath).toString().replace(/\r\n|\r|\n/g, '\n'); - fs.writeFileSync(dstFilePath, contents); - } - } - - console.log(`Open in VS Code the folder at '${destPath}' and you can analyze the compilation error`); - throw new Error('Standalone Editor compilation failed. If this is the build machine, simply launch `npm run gulp editor-distro` on your machine to further analyze the compilation problem.'); - }); - } + return ( + srcPipe + .pipe(compile()) + .pipe(i18n.processNlsFiles({ + out, + fileHeader: BUNDLED_FILE_HEADER, + languages: i18n.defaultLanguages, + })) + .pipe(filter(['**', '!**/inlineEntryPoint*', '!**/tsconfig.json', '!**/loader.js'])) + .pipe(gulp.dest(out)) + ); }); /** @@ -239,18 +127,6 @@ function toExternalDTS(contents) { return lines.join('\n').replace(/\n\n\n+/g, '\n\n'); } -/** - * @param {{ (path: string): boolean }} testFunc - */ -function filterStream(testFunc) { - return es.through(function (data) { - if (!testFunc(data.relative)) { - return; - } - this.emit('data', data); - }); -} - const finalEditorResourcesTask = task.define('final-editor-resources', () => { return es.merge( // other assets @@ -299,42 +175,6 @@ const finalEditorResourcesTask = task.define('final-editor-resources', () => { })); })) .pipe(gulp.dest('out-monaco-editor-core')), - - // dev folder - es.merge( - gulp.src('out-editor/**/*') - ).pipe(gulp.dest('out-monaco-editor-core/dev')), - - // min folder - es.merge( - gulp.src('out-editor-min/**/*') - ).pipe(filterStream(function (path) { - // no map files - return !/(\.js\.map$)|(nls\.metadata\.json$)|(bundleInfo\.json$)/.test(path); - })).pipe(es.through(function (data) { - // tweak the sourceMappingURL - if (!/\.js$/.test(data.path)) { - this.emit('data', data); - return; - } - - const relativePathToMap = path.relative(path.join(data.relative), path.join('min-maps', data.relative + '.map')); - - let strContents = data.contents.toString(); - const newStr = '//# sourceMappingURL=' + relativePathToMap.replace(/\\/g, '/'); - strContents = strContents.replace(/\/\/# sourceMappingURL=[^ ]+$/, newStr); - - data.contents = Buffer.from(strContents); - this.emit('data', data); - })).pipe(gulp.dest('out-monaco-editor-core/min')), - - // min-maps folder - es.merge( - gulp.src('out-editor-min/**/*') - ).pipe(filterStream(function (path) { - // no map files - return /\.js\.map$/.test(path); - })).pipe(gulp.dest('out-monaco-editor-core/min-maps')) ); }); @@ -349,38 +189,11 @@ gulp.task('editor-distro', task.series( task.parallel( util.rimraf('out-editor-src'), - util.rimraf('out-editor-build'), - util.rimraf('out-editor-esm'), - util.rimraf('out-monaco-editor-core'), - util.rimraf('out-editor'), - util.rimraf('out-editor-min') - ), - extractEditorSrcTask, - task.parallel( - task.series( - compileEditorAMDTask, - bundleEditorAMDTask, - minifyEditorAMDTask - ), - task.series( - createESMSourcesAndResourcesTask, - compileEditorESMTask, - ) - ), - finalEditorResourcesTask - ) -); - -gulp.task('editor-esm', - task.series( - task.parallel( - util.rimraf('out-editor-src'), - util.rimraf('out-editor-esm'), util.rimraf('out-monaco-editor-core'), ), extractEditorSrcTask, - createESMSourcesAndResourcesTask, compileEditorESMTask, + finalEditorResourcesTask ) ); @@ -393,6 +206,9 @@ gulp.task('monacodts', task.define('monacodts', () => { //#region monaco type checking +/** + * @param {boolean} watch + */ function createTscCompileTask(watch) { return () => { const createReporter = require('./lib/reporter').createReporter; @@ -421,6 +237,7 @@ function createTscCompileTask(watch) { report = reporter.end(false); } else if (str.indexOf('Compilation complete') >= 0) { + // @ts-ignore report.end(); } else if (str) { diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 0a553366..f05738fa 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -71,10 +71,6 @@ const compilations = [ '.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json', '.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json', - - 'extensions/open-remote-ssh/tsconfig.json', // Void added this - 'extensions/open-remote-wsl/tsconfig.json', // Void added this - ]; const getBaseUrl = out => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`; diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index b4b7f49e..c1d64c00 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -26,7 +26,7 @@ const gunzip = require('gulp-gunzip'); const File = require('vinyl'); const fs = require('fs'); const glob = require('glob'); -const { compileBuildTask } = require('./gulpfile.compile'); +const { compileBuildWithManglingTask } = require('./gulpfile.compile'); const { cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileExtensionMediaBuildTask } = require('./gulpfile.extensions'); const { vscodeWebResourceIncludes, createVSCodeWebFileContentMapper } = require('./gulpfile.vscode.web'); const cp = require('child_process'); @@ -154,20 +154,9 @@ function getNodeChecksum(expectedName) { } function extractAlpinefromDocker(nodeVersion, platform, arch) { - let imageName = 'node'; - let dockerPlatform = ''; - - if (arch === 'arm64') { - imageName = 'arm64v8/node'; - - const architecture = cp.execSync(`docker info --format '{{json .Architecture}}'`, { encoding: 'utf8' }).trim(); - if (architecture !== '"aarch64"') { - dockerPlatform = '--platform=linux/arm64'; - } - } - + const imageName = arch === 'arm64' ? 'arm64v8/node' : 'node'; log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from docker image ${imageName}`); - const contents = cp.execSync(`docker run --rm ${dockerPlatform} ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' }); + const contents = cp.execSync(`docker run --rm ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' }); return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } })]); } @@ -319,11 +308,10 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa } const name = product.nameShort; - const release = packageJson.release; let packageJsonContents; const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' }) - .pipe(json({ name, version, release, dependencies: undefined, optionalDependencies: undefined, type: 'module' })) + .pipe(json({ name, version, dependencies: undefined, optionalDependencies: undefined, type: 'module' })) .pipe(es.through(function (file) { packageJsonContents = file.contents.toString(); this.emit('data', file); @@ -331,7 +319,7 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa let productJsonContents; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(json({ commit, date: readISODate('out-build'), version, release })) + .pipe(json({ commit, date: readISODate('out-build'), version })) .pipe(es.through(function (file) { productJsonContents = file.contents.toString(); this.emit('data', file); @@ -414,13 +402,7 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa ); } - if (platform === 'linux' && process.env['VSCODE_NODE_GLIBC'] === '-glibc-2.17') { - result = es.merge(result, - gulp.src(`resources/server/bin/helpers/check-requirements-linux-legacy.sh`, { base: '.' }) - .pipe(rename(`bin/helpers/check-requirements.sh`)) - .pipe(util.setExecutableBit()) - ); - } else if (platform === 'linux' || platform === 'alpine') { + if (platform === 'linux' || platform === 'alpine') { result = es.merge(result, gulp.src(`resources/server/bin/helpers/check-requirements-linux.sh`, { base: '.' }) .pipe(rename(`bin/helpers/check-requirements.sh`)) @@ -491,7 +473,7 @@ function tweakProductForServerWeb(product) { gulp.task(serverTaskCI); const serverTask = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( - compileBuildTask, + compileBuildWithManglingTask, cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index f34954fa..7046ee00 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -30,7 +30,7 @@ const { getProductionDependencies } = require('./lib/dependencies'); const { config } = require('./lib/electron'); const createAsar = require('./lib/asar').createAsar; const minimist = require('minimist'); -const { compileBuildTask } = require('./gulpfile.compile'); +const { compileBuildWithoutManglingTask, compileBuildWithManglingTask } = require('./gulpfile.compile'); const { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } = require('./gulpfile.extensions'); const { promisify } = require('util'); const glob = promisify(require('glob')); @@ -101,6 +101,9 @@ const vscodeResourceIncludes = [ // Tree Sitter highlights 'out-build/vs/editor/common/languages/highlights/*.scm', + + // Tree Sitter injection queries + 'out-build/vs/editor/common/languages/injections/*.scm', ]; const vscodeResources = [ @@ -166,25 +169,25 @@ const minifyVSCodeTask = task.define('minify-vscode', task.series( )); gulp.task(minifyVSCodeTask); -const core = task.define('core-ci', task.series( - gulp.task('compile-build'), +const coreCI = task.define('core-ci', task.series( + gulp.task('compile-build-with-mangling'), task.parallel( gulp.task('minify-vscode'), gulp.task('minify-vscode-reh'), gulp.task('minify-vscode-reh-web'), ) )); -gulp.task(core); +gulp.task(coreCI); -const corePr = task.define('core-ci-pr', task.series( - gulp.task('compile-build-pr'), +const coreCIPR = task.define('core-ci-pr', task.series( + gulp.task('compile-build-without-mangling'), task.parallel( gulp.task('minify-vscode'), gulp.task('minify-vscode-reh'), gulp.task('minify-vscode-reh-web'), ) )); -gulp.task(corePr); +gulp.task(coreCIPR); /** * Compute checksums for some files. @@ -267,8 +270,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op } const name = product.nameShort; - const release = packageJson.release; - const packageJsonUpdates = { name, version, release }; + const packageJsonUpdates = { name, version }; if (platform === 'linux') { packageJsonUpdates.desktopName = `${product.applicationName}.desktop`; @@ -282,10 +284,9 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op this.emit('data', file); })); - // Void - this is important, creates the product.json in .app let productJsonContents; const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(json({ commit, date: readISODate('out-build'), checksums, version, release })) + .pipe(json({ commit, date: readISODate('out-build'), checksums, version })) .pipe(es.through(function (file) { productJsonContents = file.contents.toString(); this.emit('data', file); @@ -373,9 +374,10 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op } else if (platform === 'darwin') { const shortcut = gulp.src('resources/darwin/bin/code.sh') .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename('bin/' + product.applicationName)); - - all = es.merge(all, shortcut); + .pipe(rename('bin/code')); + const policyDest = gulp.src('.build/policies/darwin/**', { base: '.build/policies/darwin' }) + .pipe(rename(f => f.dirname = `policies/${f.dirname}`)); + all = es.merge(all, shortcut, policyDest); } let result = all @@ -502,7 +504,7 @@ BUILD_TARGETS.forEach(buildTarget => { gulp.task(vscodeTaskCI); const vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( - compileBuildTask, + minified ? compileBuildWithManglingTask : compileBuildWithoutManglingTask, cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask, @@ -540,7 +542,7 @@ const innoSetupConfig = { gulp.task(task.define( 'vscode-translations-export', task.series( - core, + coreCI, compileAllExtensionsBuildTask, function () { const pathToMetadata = './out-build/nls.metadata.json'; diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 620548ec..cd8610da 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -25,6 +25,7 @@ const exec = util.promisify(cp.exec); const root = path.dirname(__dirname); const commit = getVersion(root); +const linuxPackageRevision = Math.floor(new Date().getTime() / 1000); /** * @param {string} arch @@ -38,7 +39,9 @@ function prepareDebPackage(arch) { const debArch = getDebPackageArch(arch); const destination = '.build/linux/deb/' + debArch + '/' + product.applicationName + '-' + debArch; - return function () { + return async function () { + const dependencies = await dependenciesGenerator.getDependencies('deb', binaryDir, product.applicationName, debArch); + const desktop = gulp.src('resources/linux/code.desktop', { base: '.' }) .pipe(rename('usr/share/applications/' + product.applicationName + '.desktop')); @@ -81,12 +84,11 @@ function prepareDebPackage(arch) { let size = 0; const control = code.pipe(es.through( function (f) { size += f.isDirectory() ? 4096 : f.contents.length; }, - async function () { + function () { const that = this; - const dependencies = await dependenciesGenerator.getDependencies('deb', binaryDir, product.applicationName, debArch); gulp.src('resources/linux/debian/control.template', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`)) + .pipe(replace('@@VERSION@@', packageJson.version + '-' + linuxPackageRevision)) .pipe(replace('@@ARCHITECTURE@@', debArch)) .pipe(replace('@@DEPENDS@@', dependencies.join(', '))) .pipe(replace('@@RECOMMENDS@@', debianRecommendedDependencies.join(', '))) @@ -153,7 +155,9 @@ function prepareRpmPackage(arch) { const rpmArch = getRpmPackageArch(arch); const stripBinary = process.env['STRIP'] ?? '/usr/bin/strip'; - return function () { + return async function () { + const dependencies = await dependenciesGenerator.getDependencies('rpm', binaryDir, product.applicationName, rpmArch); + const desktop = gulp.src('resources/linux/code.desktop', { base: '.' }) .pipe(rename('BUILD/usr/share/applications/' + product.applicationName + '.desktop')); @@ -193,24 +197,19 @@ function prepareRpmPackage(arch) { const code = gulp.src(binaryDir + '/**/*', { base: binaryDir }) .pipe(rename(function (p) { p.dirname = 'BUILD/usr/share/' + product.applicationName + '/' + p.dirname; })); - const spec = code.pipe(es.through( - async function () { - const that = this; - const dependencies = await dependenciesGenerator.getDependencies('rpm', binaryDir, product.applicationName, rpmArch); - gulp.src('resources/linux/rpm/code.spec.template', { base: '.' }) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@NAME_LONG@@', product.nameLong)) - .pipe(replace('@@ICON@@', product.linuxIconName)) - .pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`)) - .pipe(replace('@@ARCHITECTURE@@', rpmArch)) - .pipe(replace('@@LICENSE@@', product.licenseName)) - .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) - .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) - .pipe(replace('@@DEPENDENCIES@@', dependencies.join(', '))) - .pipe(replace('@@STRIP@@', stripBinary)) - .pipe(rename('SPECS/' + product.applicationName + '.spec')) - .pipe(es.through(function (f) { that.emit('data', f); }, function () { that.emit('end'); })); - })); + const spec = gulp.src('resources/linux/rpm/code.spec.template', { base: '.' }) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@ICON@@', product.linuxIconName)) + .pipe(replace('@@VERSION@@', packageJson.version)) + .pipe(replace('@@RELEASE@@', linuxPackageRevision)) + .pipe(replace('@@ARCHITECTURE@@', rpmArch)) + .pipe(replace('@@LICENSE@@', product.licenseName)) + .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) + .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) + .pipe(replace('@@DEPENDENCIES@@', dependencies.join(', '))) + .pipe(replace('@@STRIP@@', stripBinary)) + .pipe(rename('SPECS/' + product.applicationName + '.spec')); const specIcon = gulp.src('resources/linux/rpm/code.xpm', { base: '.' }) .pipe(rename('SOURCES/' + product.applicationName + '.xpm')); @@ -277,7 +276,7 @@ function prepareSnapPackage(arch) { const snapcraft = gulp.src('resources/linux/snap/snapcraft.yaml', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`)) + .pipe(replace('@@VERSION@@', commit.substr(0, 8))) // Possible run-on values https://snapcraft.io/docs/architectures .pipe(replace('@@ARCHITECTURE@@', arch === 'x64' ? 'amd64' : arch)) .pipe(rename('snap/snapcraft.yaml')); diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 02b17022..2828f507 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -19,7 +19,7 @@ const filter = require('gulp-filter'); const { getProductionDependencies } = require('./lib/dependencies'); const vfs = require('vinyl-fs'); const packageJson = require('../package.json'); -const { compileBuildTask } = require('./gulpfile.compile'); +const { compileBuildWithManglingTask } = require('./gulpfile.compile'); const extensions = require('./lib/extensions'); const VinylFile = require('vinyl'); @@ -52,6 +52,9 @@ const vscodeWebResourceIncludes = [ // Tree Sitter highlights 'out-build/vs/editor/common/languages/highlights/*.scm', + // Tree Sitter injections + 'out-build/vs/editor/common/languages/injections/*.scm', + // Extension Host Worker 'out-build/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', ]; @@ -223,7 +226,7 @@ const dashed = (/** @type {string} */ str) => (str ? `-${str}` : ``); gulp.task(vscodeWebTaskCI); const vscodeWebTask = task.define(`vscode-web${dashed(minified)}`, task.series( - compileBuildTask, + compileBuildWithManglingTask, vscodeWebTaskCI )); gulp.task(vscodeWebTask); diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index afacbe34..311299c6 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -82,13 +82,14 @@ function buildWin32Setup(arch, target) { productJson['target'] = target; fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t')); + console.log('RawVersion!!!!!!!!!!!!!!', pkg.version.replace(/-\w+$/, '')) // Void const quality = product.quality || 'dev'; const definitions = { NameLong: product.nameLong, NameShort: product.nameShort, DirName: product.win32DirName, - Version: `${pkg.version}.${pkg.release}`, - RawVersion: `${pkg.version.replace(/-\w+$/, '')}.${pkg.release}`, + Version: pkg.version, + RawVersion: pkg.version.replace(/-\w+$/, ''), NameVersion: product.win32NameVersion + (target === 'user' ? ' (User)' : ''), ExeBasename: product.nameShort, RegValueName: product.win32RegValueName, diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index ba9c134a..249777c4 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -85,7 +85,10 @@ function getExtensionDownloadStream(extension) { if (extension.vsix) { input = ext.fromVsix(path_1.default.join(root, extension.vsix), extension); } - else { // Void - ext-from-gh.patch + else if (productjson.extensionsGallery?.serviceUrl) { + input = ext.fromMarketplace(productjson.extensionsGallery.serviceUrl, extension); + } + else { input = ext.fromGithub(extension); } return input.pipe((0, gulp_rename_1.default)(p => p.dirname = `${extension.name}/${p.dirname}`)); diff --git a/build/lib/builtInExtensions.ts b/build/lib/builtInExtensions.ts index 6d210d0f..e9a1180c 100644 --- a/build/lib/builtInExtensions.ts +++ b/build/lib/builtInExtensions.ts @@ -73,7 +73,9 @@ function getExtensionDownloadStream(extension: IExtensionDefinition) { if (extension.vsix) { input = ext.fromVsix(path.join(root, extension.vsix), extension); - } else { // Void - ext-from-gh.patch + } else if (productjson.extensionsGallery?.serviceUrl) { + input = ext.fromMarketplace(productjson.extensionsGallery.serviceUrl, extension); + } else { input = ext.fromGithub(extension); } diff --git a/build/lib/bundle.js b/build/lib/bundle.js index f1490f4a..382b648d 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -3,229 +3,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; Object.defineProperty(exports, "__esModule", { value: true }); -exports.bundle = bundle; exports.removeAllTSBoilerplate = removeAllTSBoilerplate; -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const vm_1 = __importDefault(require("vm")); -/** - * Bundle `entryPoints` given config `config`. - */ -function bundle(entryPoints, config, callback) { - const entryPointsMap = {}; - entryPoints.forEach((module) => { - if (entryPointsMap[module.name]) { - throw new Error(`Cannot have two entry points with the same name '${module.name}'`); - } - entryPointsMap[module.name] = module; - }); - const allMentionedModulesMap = {}; - entryPoints.forEach((module) => { - allMentionedModulesMap[module.name] = true; - module.include?.forEach(function (includedModule) { - allMentionedModulesMap[includedModule] = true; - }); - module.exclude?.forEach(function (excludedModule) { - allMentionedModulesMap[excludedModule] = true; - }); - }); - const code = require('fs').readFileSync(path_1.default.join(__dirname, '../../src/vs/loader.js')); - const r = vm_1.default.runInThisContext('(function(require, module, exports) { ' + code + '\n});'); - const loaderModule = { exports: {} }; - r.call({}, require, loaderModule, loaderModule.exports); - const loader = loaderModule.exports; - config.isBuild = true; - config.paths = config.paths || {}; - if (!config.paths['vs/css']) { - config.paths['vs/css'] = 'out-build/vs/css.build'; - } - config.buildForceInvokeFactory = config.buildForceInvokeFactory || {}; - config.buildForceInvokeFactory['vs/css'] = true; - loader.config(config); - loader(['require'], (localRequire) => { - const resolvePath = (entry) => { - let r = localRequire.toUrl(entry.path); - if (!r.endsWith('.js')) { - r += '.js'; - } - // avoid packaging the build version of plugins: - r = r.replace('vs/css.build.js', 'vs/css.js'); - return { path: r, amdModuleId: entry.amdModuleId }; - }; - for (const moduleId in entryPointsMap) { - const entryPoint = entryPointsMap[moduleId]; - if (entryPoint.prepend) { - entryPoint.prepend = entryPoint.prepend.map(resolvePath); - } - } - }); - loader(Object.keys(allMentionedModulesMap), () => { - const modules = loader.getBuildInfo(); - const partialResult = emitEntryPoints(modules, entryPointsMap); - const cssInlinedResources = loader('vs/css').getInlinedResources(); - callback(null, { - files: partialResult.files, - cssInlinedResources: cssInlinedResources, - bundleData: partialResult.bundleData - }); - }, (err) => callback(err, null)); -} -function emitEntryPoints(modules, entryPoints) { - const modulesMap = {}; - modules.forEach((m) => { - modulesMap[m.id] = m; - }); - const modulesGraph = {}; - modules.forEach((m) => { - modulesGraph[m.id] = m.dependencies; - }); - const sortedModules = topologicalSort(modulesGraph); - let result = []; - const usedPlugins = {}; - const bundleData = { - graph: modulesGraph, - bundles: {} - }; - Object.keys(entryPoints).forEach((moduleToBundle) => { - const info = entryPoints[moduleToBundle]; - const rootNodes = [moduleToBundle].concat(info.include || []); - const allDependencies = visit(rootNodes, modulesGraph); - const excludes = ['require', 'exports', 'module'].concat(info.exclude || []); - excludes.forEach((excludeRoot) => { - const allExcludes = visit([excludeRoot], modulesGraph); - Object.keys(allExcludes).forEach((exclude) => { - delete allDependencies[exclude]; - }); - }); - const includedModules = sortedModules.filter((module) => { - return allDependencies[module]; - }); - bundleData.bundles[moduleToBundle] = includedModules; - const res = emitEntryPoint(modulesMap, modulesGraph, moduleToBundle, includedModules, info.prepend || [], info.dest); - result = result.concat(res.files); - for (const pluginName in res.usedPlugins) { - usedPlugins[pluginName] = usedPlugins[pluginName] || res.usedPlugins[pluginName]; - } - }); - Object.keys(usedPlugins).forEach((pluginName) => { - const plugin = usedPlugins[pluginName]; - if (typeof plugin.finishBuild === 'function') { - const write = (filename, contents) => { - result.push({ - dest: filename, - sources: [{ - path: null, - contents: contents - }] - }); - }; - plugin.finishBuild(write); - } - }); - return { - // TODO@TS 2.1.2 - files: extractStrings(removeAllDuplicateTSBoilerplate(result)), - bundleData: bundleData - }; -} -function extractStrings(destFiles) { - const parseDefineCall = (moduleMatch, depsMatch) => { - const module = moduleMatch.replace(/^"|"$/g, ''); - let deps = depsMatch.split(','); - deps = deps.map((dep) => { - dep = dep.trim(); - dep = dep.replace(/^"|"$/g, ''); - dep = dep.replace(/^'|'$/g, ''); - let prefix = null; - let _path = null; - const pieces = dep.split('!'); - if (pieces.length > 1) { - prefix = pieces[0] + '!'; - _path = pieces[1]; - } - else { - prefix = ''; - _path = pieces[0]; - } - if (/^\.\//.test(_path) || /^\.\.\//.test(_path)) { - const res = path_1.default.join(path_1.default.dirname(module), _path).replace(/\\/g, '/'); - return prefix + res; - } - return prefix + _path; - }); - return { - module: module, - deps: deps - }; - }; - destFiles.forEach((destFile) => { - if (!/\.js$/.test(destFile.dest)) { - return; - } - if (/\.nls\.js$/.test(destFile.dest)) { - return; - } - // Do one pass to record the usage counts for each module id - const useCounts = {}; - destFile.sources.forEach((source) => { - const matches = source.contents.match(/define\(("[^"]+"),\s*\[(((, )?("|')[^"']+("|'))+)\]/); - if (!matches) { - return; - } - const defineCall = parseDefineCall(matches[1], matches[2]); - useCounts[defineCall.module] = (useCounts[defineCall.module] || 0) + 1; - defineCall.deps.forEach((dep) => { - useCounts[dep] = (useCounts[dep] || 0) + 1; - }); - }); - const sortedByUseModules = Object.keys(useCounts); - sortedByUseModules.sort((a, b) => { - return useCounts[b] - useCounts[a]; - }); - const replacementMap = {}; - sortedByUseModules.forEach((module, index) => { - replacementMap[module] = index; - }); - destFile.sources.forEach((source) => { - source.contents = source.contents.replace(/define\(("[^"]+"),\s*\[(((, )?("|')[^"']+("|'))+)\]/, (_, moduleMatch, depsMatch) => { - const defineCall = parseDefineCall(moduleMatch, depsMatch); - return `define(__m[${replacementMap[defineCall.module]}/*${defineCall.module}*/], __M([${defineCall.deps.map(dep => replacementMap[dep] + '/*' + dep + '*/').join(',')}])`; - }); - }); - destFile.sources.unshift({ - path: null, - contents: [ - '(function() {', - `var __m = ${JSON.stringify(sortedByUseModules)};`, - `var __M = function(deps) {`, - ` var result = [];`, - ` for (var i = 0, len = deps.length; i < len; i++) {`, - ` result[i] = __m[deps[i]];`, - ` }`, - ` return result;`, - `};` - ].join('\n') - }); - destFile.sources.push({ - path: null, - contents: '}).call(this);' - }); - }); - return destFiles; -} -function removeAllDuplicateTSBoilerplate(destFiles) { - destFiles.forEach((destFile) => { - const SEEN_BOILERPLATE = []; - destFile.sources.forEach((source) => { - source.contents = removeDuplicateTSBoilerplate(source.contents, SEEN_BOILERPLATE); - }); - }); - return destFiles; -} function removeAllTSBoilerplate(source) { const seen = new Array(BOILERPLATE.length).fill(true, 0, BOILERPLATE.length); return removeDuplicateTSBoilerplate(source, seen); @@ -280,213 +59,4 @@ function removeDuplicateTSBoilerplate(source, SEEN_BOILERPLATE = []) { } return newLines.join('\n'); } -function emitEntryPoint(modulesMap, deps, entryPoint, includedModules, prepend, dest) { - if (!dest) { - dest = entryPoint + '.js'; - } - const mainResult = { - sources: [], - dest: dest - }, results = [mainResult]; - const usedPlugins = {}; - const getLoaderPlugin = (pluginName) => { - if (!usedPlugins[pluginName]) { - usedPlugins[pluginName] = modulesMap[pluginName].exports; - } - return usedPlugins[pluginName]; - }; - includedModules.forEach((c) => { - const bangIndex = c.indexOf('!'); - if (bangIndex >= 0) { - const pluginName = c.substr(0, bangIndex); - const plugin = getLoaderPlugin(pluginName); - mainResult.sources.push(emitPlugin(entryPoint, plugin, pluginName, c.substr(bangIndex + 1))); - return; - } - const module = modulesMap[c]; - if (module.path === 'empty:') { - return; - } - const contents = readFileAndRemoveBOM(module.path); - if (module.shim) { - mainResult.sources.push(emitShimmedModule(c, deps[c], module.shim, module.path, contents)); - } - else if (module.defineLocation) { - mainResult.sources.push(emitNamedModule(c, module.defineLocation, module.path, contents)); - } - else { - const moduleCopy = { - id: module.id, - path: module.path, - defineLocation: module.defineLocation, - dependencies: module.dependencies - }; - throw new Error(`Cannot bundle module '${module.id}' for entry point '${entryPoint}' because it has no shim and it lacks a defineLocation: ${JSON.stringify(moduleCopy)}`); - } - }); - Object.keys(usedPlugins).forEach((pluginName) => { - const plugin = usedPlugins[pluginName]; - if (typeof plugin.writeFile === 'function') { - const req = (() => { - throw new Error('no-no!'); - }); - req.toUrl = something => something; - const write = (filename, contents) => { - results.push({ - dest: filename, - sources: [{ - path: null, - contents: contents - }] - }); - }; - plugin.writeFile(pluginName, entryPoint, req, write, {}); - } - }); - const toIFile = (entry) => { - let contents = readFileAndRemoveBOM(entry.path); - if (entry.amdModuleId) { - contents = contents.replace(/^define\(/m, `define("${entry.amdModuleId}",`); - } - return { - path: entry.path, - contents: contents - }; - }; - const toPrepend = (prepend || []).map(toIFile); - mainResult.sources = toPrepend.concat(mainResult.sources); - return { - files: results, - usedPlugins: usedPlugins - }; -} -function readFileAndRemoveBOM(path) { - const BOM_CHAR_CODE = 65279; - let contents = fs_1.default.readFileSync(path, 'utf8'); - // Remove BOM - if (contents.charCodeAt(0) === BOM_CHAR_CODE) { - contents = contents.substring(1); - } - return contents; -} -function emitPlugin(entryPoint, plugin, pluginName, moduleName) { - let result = ''; - if (typeof plugin.write === 'function') { - const write = ((what) => { - result += what; - }); - write.getEntryPoint = () => { - return entryPoint; - }; - write.asModule = (moduleId, code) => { - code = code.replace(/^define\(/, 'define("' + moduleId + '",'); - result += code; - }; - plugin.write(pluginName, moduleName, write); - } - return { - path: null, - contents: result - }; -} -function emitNamedModule(moduleId, defineCallPosition, path, contents) { - // `defineCallPosition` is the position in code: |define() - const defineCallOffset = positionToOffset(contents, defineCallPosition.line, defineCallPosition.col); - // `parensOffset` is the position in code: define|() - const parensOffset = contents.indexOf('(', defineCallOffset); - const insertStr = '"' + moduleId + '", '; - return { - path: path, - contents: contents.substr(0, parensOffset + 1) + insertStr + contents.substr(parensOffset + 1) - }; -} -function emitShimmedModule(moduleId, myDeps, factory, path, contents) { - const strDeps = (myDeps.length > 0 ? '"' + myDeps.join('", "') + '"' : ''); - const strDefine = 'define("' + moduleId + '", [' + strDeps + '], ' + factory + ');'; - return { - path: path, - contents: contents + '\n;\n' + strDefine - }; -} -/** - * Convert a position (line:col) to (offset) in string `str` - */ -function positionToOffset(str, desiredLine, desiredCol) { - if (desiredLine === 1) { - return desiredCol - 1; - } - let line = 1; - let lastNewLineOffset = -1; - do { - if (desiredLine === line) { - return lastNewLineOffset + 1 + desiredCol - 1; - } - lastNewLineOffset = str.indexOf('\n', lastNewLineOffset + 1); - line++; - } while (lastNewLineOffset >= 0); - return -1; -} -/** - * Return a set of reachable nodes in `graph` starting from `rootNodes` - */ -function visit(rootNodes, graph) { - const result = {}; - const queue = rootNodes; - rootNodes.forEach((node) => { - result[node] = true; - }); - while (queue.length > 0) { - const el = queue.shift(); - const myEdges = graph[el] || []; - myEdges.forEach((toNode) => { - if (!result[toNode]) { - result[toNode] = true; - queue.push(toNode); - } - }); - } - return result; -} -/** - * Perform a topological sort on `graph` - */ -function topologicalSort(graph) { - const allNodes = {}, outgoingEdgeCount = {}, inverseEdges = {}; - Object.keys(graph).forEach((fromNode) => { - allNodes[fromNode] = true; - outgoingEdgeCount[fromNode] = graph[fromNode].length; - graph[fromNode].forEach((toNode) => { - allNodes[toNode] = true; - outgoingEdgeCount[toNode] = outgoingEdgeCount[toNode] || 0; - inverseEdges[toNode] = inverseEdges[toNode] || []; - inverseEdges[toNode].push(fromNode); - }); - }); - // https://en.wikipedia.org/wiki/Topological_sorting - const S = [], L = []; - Object.keys(allNodes).forEach((node) => { - if (outgoingEdgeCount[node] === 0) { - delete outgoingEdgeCount[node]; - S.push(node); - } - }); - while (S.length > 0) { - // Ensure the exact same order all the time with the same inputs - S.sort(); - const n = S.shift(); - L.push(n); - const myInverseEdges = inverseEdges[n] || []; - myInverseEdges.forEach((m) => { - outgoingEdgeCount[m]--; - if (outgoingEdgeCount[m] === 0) { - delete outgoingEdgeCount[m]; - S.push(m); - } - }); - } - if (Object.keys(outgoingEdgeCount).length > 0) { - throw new Error('Cannot do topological sort on cyclic graph, remaining nodes: ' + Object.keys(outgoingEdgeCount)); - } - return L; -} //# sourceMappingURL=bundle.js.map \ No newline at end of file diff --git a/build/lib/bundle.ts b/build/lib/bundle.ts index 68182e6b..708bb5e9 100644 --- a/build/lib/bundle.ts +++ b/build/lib/bundle.ts @@ -3,360 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import fs from 'fs'; -import path from 'path'; -import vm from 'vm'; - -interface IPosition { - line: number; - col: number; -} - -interface IBuildModuleInfo { - id: string; - path: string; - defineLocation: IPosition | null; - dependencies: string[]; - shim: string; - exports: any; -} - -interface IBuildModuleInfoMap { - [moduleId: string]: IBuildModuleInfo; -} - -interface ILoaderPlugin { - write(pluginName: string, moduleName: string, write: ILoaderPluginWriteFunc): void; - writeFile(pluginName: string, entryPoint: string, req: ILoaderPluginReqFunc, write: (filename: string, contents: string) => void, config: any): void; - finishBuild(write: (filename: string, contents: string) => void): void; -} - -interface ILoaderPluginWriteFunc { - (something: string): void; - getEntryPoint(): string; - asModule(moduleId: string, code: string): void; -} - -interface ILoaderPluginReqFunc { - (something: string): void; - toUrl(something: string): string; -} - -export interface IExtraFile { - path: string; - amdModuleId?: string; -} - export interface IEntryPoint { name: string; include?: string[]; - exclude?: string[]; - /** @deprecated unsupported by ESM */ - prepend?: IExtraFile[]; dest?: string; } -interface IEntryPointMap { - [moduleId: string]: IEntryPoint; -} - -export interface IGraph { - [node: string]: string[]; -} - -interface INodeSet { - [node: string]: boolean; -} - -export interface IFile { - path: string | null; - contents: string; -} - -export interface IConcatFile { - dest: string; - sources: IFile[]; -} - -export interface IBundleData { - graph: IGraph; - bundles: { [moduleId: string]: string[] }; -} - -export interface IBundleResult { - files: IConcatFile[]; - cssInlinedResources: string[]; - bundleData: IBundleData; -} - -interface IPartialBundleResult { - files: IConcatFile[]; - bundleData: IBundleData; -} - -export interface ILoaderConfig { - isBuild?: boolean; - paths?: { [path: string]: any }; - /* - * Normally, during a build, no module factories are invoked. This can be used - * to forcefully execute a module's factory. - */ - buildForceInvokeFactory: { - [moduleId: string]: boolean; - }; -} - -/** - * Bundle `entryPoints` given config `config`. - */ -export function bundle(entryPoints: IEntryPoint[], config: ILoaderConfig, callback: (err: any, result: IBundleResult | null) => void): void { - const entryPointsMap: IEntryPointMap = {}; - entryPoints.forEach((module: IEntryPoint) => { - if (entryPointsMap[module.name]) { - throw new Error(`Cannot have two entry points with the same name '${module.name}'`); - } - entryPointsMap[module.name] = module; - }); - - const allMentionedModulesMap: { [modules: string]: boolean } = {}; - entryPoints.forEach((module: IEntryPoint) => { - allMentionedModulesMap[module.name] = true; - module.include?.forEach(function (includedModule) { - allMentionedModulesMap[includedModule] = true; - }); - module.exclude?.forEach(function (excludedModule) { - allMentionedModulesMap[excludedModule] = true; - }); - }); - - - const code = require('fs').readFileSync(path.join(__dirname, '../../src/vs/loader.js')); - const r: Function = vm.runInThisContext('(function(require, module, exports) { ' + code + '\n});'); - const loaderModule = { exports: {} }; - r.call({}, require, loaderModule, loaderModule.exports); - - const loader: any = loaderModule.exports; - config.isBuild = true; - config.paths = config.paths || {}; - if (!config.paths['vs/css']) { - config.paths['vs/css'] = 'out-build/vs/css.build'; - } - config.buildForceInvokeFactory = config.buildForceInvokeFactory || {}; - config.buildForceInvokeFactory['vs/css'] = true; - loader.config(config); - - loader(['require'], (localRequire: any) => { - const resolvePath = (entry: IExtraFile) => { - let r = localRequire.toUrl(entry.path); - if (!r.endsWith('.js')) { - r += '.js'; - } - // avoid packaging the build version of plugins: - r = r.replace('vs/css.build.js', 'vs/css.js'); - return { path: r, amdModuleId: entry.amdModuleId }; - }; - for (const moduleId in entryPointsMap) { - const entryPoint = entryPointsMap[moduleId]; - if (entryPoint.prepend) { - entryPoint.prepend = entryPoint.prepend.map(resolvePath); - } - } - }); - - loader(Object.keys(allMentionedModulesMap), () => { - const modules = loader.getBuildInfo(); - const partialResult = emitEntryPoints(modules, entryPointsMap); - const cssInlinedResources = loader('vs/css').getInlinedResources(); - callback(null, { - files: partialResult.files, - cssInlinedResources: cssInlinedResources, - bundleData: partialResult.bundleData - }); - }, (err: any) => callback(err, null)); -} - -function emitEntryPoints(modules: IBuildModuleInfo[], entryPoints: IEntryPointMap): IPartialBundleResult { - const modulesMap: IBuildModuleInfoMap = {}; - modules.forEach((m: IBuildModuleInfo) => { - modulesMap[m.id] = m; - }); - - const modulesGraph: IGraph = {}; - modules.forEach((m: IBuildModuleInfo) => { - modulesGraph[m.id] = m.dependencies; - }); - - const sortedModules = topologicalSort(modulesGraph); - - let result: IConcatFile[] = []; - const usedPlugins: IPluginMap = {}; - const bundleData: IBundleData = { - graph: modulesGraph, - bundles: {} - }; - - Object.keys(entryPoints).forEach((moduleToBundle: string) => { - const info = entryPoints[moduleToBundle]; - const rootNodes = [moduleToBundle].concat(info.include || []); - const allDependencies = visit(rootNodes, modulesGraph); - const excludes: string[] = ['require', 'exports', 'module'].concat(info.exclude || []); - - excludes.forEach((excludeRoot: string) => { - const allExcludes = visit([excludeRoot], modulesGraph); - Object.keys(allExcludes).forEach((exclude: string) => { - delete allDependencies[exclude]; - }); - }); - - const includedModules = sortedModules.filter((module: string) => { - return allDependencies[module]; - }); - - bundleData.bundles[moduleToBundle] = includedModules; - - const res = emitEntryPoint( - modulesMap, - modulesGraph, - moduleToBundle, - includedModules, - info.prepend || [], - info.dest - ); - - result = result.concat(res.files); - for (const pluginName in res.usedPlugins) { - usedPlugins[pluginName] = usedPlugins[pluginName] || res.usedPlugins[pluginName]; - } - }); - - Object.keys(usedPlugins).forEach((pluginName: string) => { - const plugin = usedPlugins[pluginName]; - if (typeof plugin.finishBuild === 'function') { - const write = (filename: string, contents: string) => { - result.push({ - dest: filename, - sources: [{ - path: null, - contents: contents - }] - }); - }; - plugin.finishBuild(write); - } - }); - - return { - // TODO@TS 2.1.2 - files: extractStrings(removeAllDuplicateTSBoilerplate(result)), - bundleData: bundleData - }; -} - -function extractStrings(destFiles: IConcatFile[]): IConcatFile[] { - const parseDefineCall = (moduleMatch: string, depsMatch: string) => { - const module = moduleMatch.replace(/^"|"$/g, ''); - let deps = depsMatch.split(','); - deps = deps.map((dep) => { - dep = dep.trim(); - dep = dep.replace(/^"|"$/g, ''); - dep = dep.replace(/^'|'$/g, ''); - let prefix: string | null = null; - let _path: string | null = null; - const pieces = dep.split('!'); - if (pieces.length > 1) { - prefix = pieces[0] + '!'; - _path = pieces[1]; - } else { - prefix = ''; - _path = pieces[0]; - } - - if (/^\.\//.test(_path) || /^\.\.\//.test(_path)) { - const res = path.join(path.dirname(module), _path).replace(/\\/g, '/'); - return prefix + res; - } - return prefix + _path; - }); - return { - module: module, - deps: deps - }; - }; - - destFiles.forEach((destFile) => { - if (!/\.js$/.test(destFile.dest)) { - return; - } - if (/\.nls\.js$/.test(destFile.dest)) { - return; - } - - // Do one pass to record the usage counts for each module id - const useCounts: { [moduleId: string]: number } = {}; - destFile.sources.forEach((source) => { - const matches = source.contents.match(/define\(("[^"]+"),\s*\[(((, )?("|')[^"']+("|'))+)\]/); - if (!matches) { - return; - } - - const defineCall = parseDefineCall(matches[1], matches[2]); - useCounts[defineCall.module] = (useCounts[defineCall.module] || 0) + 1; - defineCall.deps.forEach((dep) => { - useCounts[dep] = (useCounts[dep] || 0) + 1; - }); - }); - - const sortedByUseModules = Object.keys(useCounts); - sortedByUseModules.sort((a, b) => { - return useCounts[b] - useCounts[a]; - }); - - const replacementMap: { [moduleId: string]: number } = {}; - sortedByUseModules.forEach((module, index) => { - replacementMap[module] = index; - }); - - destFile.sources.forEach((source) => { - source.contents = source.contents.replace(/define\(("[^"]+"),\s*\[(((, )?("|')[^"']+("|'))+)\]/, (_, moduleMatch, depsMatch) => { - const defineCall = parseDefineCall(moduleMatch, depsMatch); - return `define(__m[${replacementMap[defineCall.module]}/*${defineCall.module}*/], __M([${defineCall.deps.map(dep => replacementMap[dep] + '/*' + dep + '*/').join(',')}])`; - }); - }); - - destFile.sources.unshift({ - path: null, - contents: [ - '(function() {', - `var __m = ${JSON.stringify(sortedByUseModules)};`, - `var __M = function(deps) {`, - ` var result = [];`, - ` for (var i = 0, len = deps.length; i < len; i++) {`, - ` result[i] = __m[deps[i]];`, - ` }`, - ` return result;`, - `};` - ].join('\n') - }); - - destFile.sources.push({ - path: null, - contents: '}).call(this);' - }); - }); - return destFiles; -} - -function removeAllDuplicateTSBoilerplate(destFiles: IConcatFile[]): IConcatFile[] { - destFiles.forEach((destFile) => { - const SEEN_BOILERPLATE: boolean[] = []; - destFile.sources.forEach((source) => { - source.contents = removeDuplicateTSBoilerplate(source.contents, SEEN_BOILERPLATE); - }); - }); - - return destFiles; -} - export function removeAllTSBoilerplate(source: string) { const seen = new Array(BOILERPLATE.length).fill(true, 0, BOILERPLATE.length); return removeDuplicateTSBoilerplate(source, seen); @@ -411,273 +63,3 @@ function removeDuplicateTSBoilerplate(source: string, SEEN_BOILERPLATE: boolean[ } return newLines.join('\n'); } - -interface IPluginMap { - [moduleId: string]: ILoaderPlugin; -} - -interface IEmitEntryPointResult { - files: IConcatFile[]; - usedPlugins: IPluginMap; -} - -function emitEntryPoint( - modulesMap: IBuildModuleInfoMap, - deps: IGraph, - entryPoint: string, - includedModules: string[], - prepend: IExtraFile[], - dest: string | undefined -): IEmitEntryPointResult { - if (!dest) { - dest = entryPoint + '.js'; - } - const mainResult: IConcatFile = { - sources: [], - dest: dest - }, - results: IConcatFile[] = [mainResult]; - - const usedPlugins: IPluginMap = {}; - const getLoaderPlugin = (pluginName: string): ILoaderPlugin => { - if (!usedPlugins[pluginName]) { - usedPlugins[pluginName] = modulesMap[pluginName].exports; - } - return usedPlugins[pluginName]; - }; - - includedModules.forEach((c: string) => { - const bangIndex = c.indexOf('!'); - - if (bangIndex >= 0) { - const pluginName = c.substr(0, bangIndex); - const plugin = getLoaderPlugin(pluginName); - mainResult.sources.push(emitPlugin(entryPoint, plugin, pluginName, c.substr(bangIndex + 1))); - return; - } - - const module = modulesMap[c]; - - if (module.path === 'empty:') { - return; - } - - const contents = readFileAndRemoveBOM(module.path); - - if (module.shim) { - mainResult.sources.push(emitShimmedModule(c, deps[c], module.shim, module.path, contents)); - } else if (module.defineLocation) { - mainResult.sources.push(emitNamedModule(c, module.defineLocation, module.path, contents)); - } else { - const moduleCopy = { - id: module.id, - path: module.path, - defineLocation: module.defineLocation, - dependencies: module.dependencies - }; - throw new Error(`Cannot bundle module '${module.id}' for entry point '${entryPoint}' because it has no shim and it lacks a defineLocation: ${JSON.stringify(moduleCopy)}`); - } - }); - - Object.keys(usedPlugins).forEach((pluginName: string) => { - const plugin = usedPlugins[pluginName]; - if (typeof plugin.writeFile === 'function') { - const req: ILoaderPluginReqFunc = (() => { - throw new Error('no-no!'); - }); - req.toUrl = something => something; - - const write = (filename: string, contents: string) => { - results.push({ - dest: filename, - sources: [{ - path: null, - contents: contents - }] - }); - }; - plugin.writeFile(pluginName, entryPoint, req, write, {}); - } - }); - - const toIFile = (entry: IExtraFile): IFile => { - let contents = readFileAndRemoveBOM(entry.path); - if (entry.amdModuleId) { - contents = contents.replace(/^define\(/m, `define("${entry.amdModuleId}",`); - } - return { - path: entry.path, - contents: contents - }; - }; - - const toPrepend = (prepend || []).map(toIFile); - - mainResult.sources = toPrepend.concat(mainResult.sources); - - return { - files: results, - usedPlugins: usedPlugins - }; -} - -function readFileAndRemoveBOM(path: string): string { - const BOM_CHAR_CODE = 65279; - let contents = fs.readFileSync(path, 'utf8'); - // Remove BOM - if (contents.charCodeAt(0) === BOM_CHAR_CODE) { - contents = contents.substring(1); - } - return contents; -} - -function emitPlugin(entryPoint: string, plugin: ILoaderPlugin, pluginName: string, moduleName: string): IFile { - let result = ''; - if (typeof plugin.write === 'function') { - const write: ILoaderPluginWriteFunc = ((what: string) => { - result += what; - }); - write.getEntryPoint = () => { - return entryPoint; - }; - write.asModule = (moduleId: string, code: string) => { - code = code.replace(/^define\(/, 'define("' + moduleId + '",'); - result += code; - }; - plugin.write(pluginName, moduleName, write); - } - return { - path: null, - contents: result - }; -} - -function emitNamedModule(moduleId: string, defineCallPosition: IPosition, path: string, contents: string): IFile { - - // `defineCallPosition` is the position in code: |define() - const defineCallOffset = positionToOffset(contents, defineCallPosition.line, defineCallPosition.col); - - // `parensOffset` is the position in code: define|() - const parensOffset = contents.indexOf('(', defineCallOffset); - - const insertStr = '"' + moduleId + '", '; - - return { - path: path, - contents: contents.substr(0, parensOffset + 1) + insertStr + contents.substr(parensOffset + 1) - }; -} - -function emitShimmedModule(moduleId: string, myDeps: string[], factory: string, path: string, contents: string): IFile { - const strDeps = (myDeps.length > 0 ? '"' + myDeps.join('", "') + '"' : ''); - const strDefine = 'define("' + moduleId + '", [' + strDeps + '], ' + factory + ');'; - return { - path: path, - contents: contents + '\n;\n' + strDefine - }; -} - -/** - * Convert a position (line:col) to (offset) in string `str` - */ -function positionToOffset(str: string, desiredLine: number, desiredCol: number): number { - if (desiredLine === 1) { - return desiredCol - 1; - } - - let line = 1; - let lastNewLineOffset = -1; - - do { - if (desiredLine === line) { - return lastNewLineOffset + 1 + desiredCol - 1; - } - lastNewLineOffset = str.indexOf('\n', lastNewLineOffset + 1); - line++; - } while (lastNewLineOffset >= 0); - - return -1; -} - - -/** - * Return a set of reachable nodes in `graph` starting from `rootNodes` - */ -function visit(rootNodes: string[], graph: IGraph): INodeSet { - const result: INodeSet = {}; - const queue = rootNodes; - - rootNodes.forEach((node) => { - result[node] = true; - }); - - while (queue.length > 0) { - const el = queue.shift(); - const myEdges = graph[el!] || []; - myEdges.forEach((toNode) => { - if (!result[toNode]) { - result[toNode] = true; - queue.push(toNode); - } - }); - } - - return result; -} - -/** - * Perform a topological sort on `graph` - */ -function topologicalSort(graph: IGraph): string[] { - - const allNodes: INodeSet = {}, - outgoingEdgeCount: { [node: string]: number } = {}, - inverseEdges: IGraph = {}; - - Object.keys(graph).forEach((fromNode: string) => { - allNodes[fromNode] = true; - outgoingEdgeCount[fromNode] = graph[fromNode].length; - - graph[fromNode].forEach((toNode) => { - allNodes[toNode] = true; - outgoingEdgeCount[toNode] = outgoingEdgeCount[toNode] || 0; - - inverseEdges[toNode] = inverseEdges[toNode] || []; - inverseEdges[toNode].push(fromNode); - }); - }); - - // https://en.wikipedia.org/wiki/Topological_sorting - const S: string[] = [], - L: string[] = []; - - Object.keys(allNodes).forEach((node: string) => { - if (outgoingEdgeCount[node] === 0) { - delete outgoingEdgeCount[node]; - S.push(node); - } - }); - - while (S.length > 0) { - // Ensure the exact same order all the time with the same inputs - S.sort(); - - const n: string = S.shift()!; - L.push(n); - - const myInverseEdges = inverseEdges[n] || []; - myInverseEdges.forEach((m: string) => { - outgoingEdgeCount[m]--; - if (outgoingEdgeCount[m] === 0) { - delete outgoingEdgeCount[m]; - S.push(m); - } - }); - } - - if (Object.keys(outgoingEdgeCount).length > 0) { - throw new Error('Cannot do topological sort on cyclic graph, remaining nodes: ' + Object.keys(outgoingEdgeCount)); - } - - return L; -} diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 841dbe13..55fb1480 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -41,6 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.watchApiProposalNamesTask = exports.compileApiProposalNamesTask = void 0; +exports.createCompile = createCompile; exports.transpileTask = transpileTask; exports.compileTask = compileTask; exports.watchTask = watchTask; diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index 6e1fcab5..625fc430 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -49,7 +49,7 @@ interface ICompileTaskOptions { readonly preserveEnglish: boolean; } -function createCompile(src: string, { build, emitError, transpileOnly, preserveEnglish }: ICompileTaskOptions) { +export function createCompile(src: string, { build, emitError, transpileOnly, preserveEnglish }: ICompileTaskOptions) { const tsb = require('./tsb') as typeof import('./tsb'); const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); diff --git a/build/lib/electron.js b/build/lib/electron.js index f7814d5d..56992d8a 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -90,7 +90,7 @@ function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) { role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions, - iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns + iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', utis }; } @@ -215,7 +215,7 @@ exports.config = { darwinForceDarkModeSupport: true, darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined, linuxExecutableName: product.applicationName, - winIcon: 'resources/win32/code.ico', // <-- Void icon + winIcon: 'resources/win32/code.ico', token: process.env['GITHUB_TOKEN'], repo: product.electronRepository || undefined, validateChecksum: true, diff --git a/build/lib/electron.ts b/build/lib/electron.ts index 9cdf58dd..3bb047df 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -68,7 +68,7 @@ function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuff role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions, - iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns + iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', utis }; } @@ -196,7 +196,7 @@ export const config = { darwinForceDarkModeSupport: true, darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined, linuxExecutableName: product.applicationName, - winIcon: 'resources/win32/code.ico', // <-- Void icon + winIcon: 'resources/win32/code.ico', token: process.env['GITHUB_TOKEN'], repo: product.electronRepository || undefined, validateChecksum: true, diff --git a/build/lib/i18n.js b/build/lib/i18n.js index 9483d319..1d3bfb90 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -319,9 +319,10 @@ globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), function processNlsFiles(opts) { return (0, event_stream_1.through)(function (file) { const fileName = path_1.default.basename(file.path); - if (fileName === 'bundleInfo.json') { // pick a root level file to put the core bundles (TODO@esm this file is not created anymore, pick another) + if (fileName === 'nls.keys.json') { try { - const json = JSON.parse(fs_1.default.readFileSync(path_1.default.join(REPO_ROOT_PATH, opts.out, 'nls.keys.json')).toString()); + const contents = file.contents.toString('utf8'); + const json = JSON.parse(contents); if (NLSKeysFormat.is(json)) { processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this); } diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index d2904ccf..96468033 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -387,9 +387,10 @@ globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), export function processNlsFiles(opts: { out: string; fileHeader: string; languages: Language[] }): ThroughStream { return through(function (this: ThroughStream, file: File) { const fileName = path.basename(file.path); - if (fileName === 'bundleInfo.json') { // pick a root level file to put the core bundles (TODO@esm this file is not created anymore, pick another) + if (fileName === 'nls.keys.json') { try { - const json = JSON.parse(fs.readFileSync(path.join(REPO_ROOT_PATH, opts.out, 'nls.keys.json')).toString()); + const contents = file.contents.toString('utf8'); + const json = JSON.parse(contents); if (NLSKeysFormat.is(json)) { processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this); } diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 5cf5c584..9785bb8d 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -79,9 +79,12 @@ const CORE_TYPES = [ 'PerformanceMark', 'PerformanceObserver', 'ImportMeta', + 'structuredClone', // webcrypto has been available since Node.js 19, but still live in dom.d.ts 'Crypto', - 'SubtleCrypto' + 'SubtleCrypto', + 'JsonWebKey', + 'MessageEvent', ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 63377328..f8aa401a 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -77,10 +77,13 @@ const CORE_TYPES = [ 'PerformanceMark', 'PerformanceObserver', 'ImportMeta', + 'structuredClone', // webcrypto has been available since Node.js 19, but still live in dom.d.ts 'Crypto', - 'SubtleCrypto' + 'SubtleCrypto', + 'JsonWebKey', + 'MessageEvent', ]; // Types that are defined in a common layer but are known to be only diff --git a/build/lib/optimize.js b/build/lib/optimize.js index d75a2978..105dc02a 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -70,12 +70,6 @@ function bundleESMTask(opts) { } return entryPoint; }); - const allMentionedModules = new Set(); - for (const entryPoint of entryPoints) { - allMentionedModules.add(entryPoint.name); - entryPoint.include?.forEach(allMentionedModules.add, allMentionedModules); - entryPoint.exclude?.forEach(allMentionedModules.add, allMentionedModules); - } const bundleAsync = async () => { const files = []; const tasks = []; @@ -127,7 +121,6 @@ function bundleESMTask(opts) { }; const task = esbuild_1.default.build({ bundle: true, - external: entryPoint.exclude, packages: 'external', // "external all the things", see https://esbuild.github.io/api/#packages platform: 'neutral', // makes esm format: 'esm', diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index e12c85c3..88a2a25d 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -62,13 +62,6 @@ function bundleESMTask(opts: IBundleESMTaskOpts): NodeJS.ReadWriteStream { return entryPoint; }); - const allMentionedModules = new Set(); - for (const entryPoint of entryPoints) { - allMentionedModules.add(entryPoint.name); - entryPoint.include?.forEach(allMentionedModules.add, allMentionedModules); - entryPoint.exclude?.forEach(allMentionedModules.add, allMentionedModules); - } - const bundleAsync = async () => { const files: VinylFile[] = []; const tasks: Promise[] = []; @@ -129,7 +122,6 @@ function bundleESMTask(opts: IBundleESMTaskOpts): NodeJS.ReadWriteStream { const task = esbuild.build({ bundle: true, - external: entryPoint.exclude, packages: 'external', // "external all the things", see https://esbuild.github.io/api/#packages platform: 'neutral', // makes esm format: 'esm', diff --git a/build/lib/policies.js b/build/lib/policies.js index b76d9ffe..ac697629 100644 --- a/build/lib/policies.js +++ b/build/lib/policies.js @@ -27,7 +27,11 @@ function isNlsStringArray(value) { } var PolicyType; (function (PolicyType) { - PolicyType[PolicyType["StringEnum"] = 0] = "StringEnum"; + PolicyType["Boolean"] = "boolean"; + PolicyType["Number"] = "number"; + PolicyType["Object"] = "object"; + PolicyType["String"] = "string"; + PolicyType["StringEnum"] = "stringEnum"; })(PolicyType || (PolicyType = {})); function renderADMLString(prefix, moduleName, nlsString, translations) { let value; @@ -42,15 +46,28 @@ function renderADMLString(prefix, moduleName, nlsString, translations) { } return `${value}`; } +function renderProfileString(_prefix, moduleName, nlsString, translations) { + let value; + if (translations) { + const moduleTranslations = translations[moduleName]; + if (moduleTranslations) { + value = moduleTranslations[nlsString.nlsKey]; + } + } + if (!value) { + value = nlsString.value; + } + return value; +} class BasePolicy { - policyType; + type; name; category; minimumVersion; description; moduleName; - constructor(policyType, name, category, minimumVersion, description, moduleName) { - this.policyType = policyType; + constructor(type, name, category, minimumVersion, description, moduleName) { + this.type = type; this.name = name; this.category = category; this.minimumVersion = minimumVersion; @@ -80,17 +97,25 @@ class BasePolicy { renderADMLPresentation() { return `${this.renderADMLPresentationContents()}`; } + renderProfile() { + return [`${this.name}`, this.renderProfileValue()]; + } + renderProfileManifest(translations) { + return ` +${this.renderProfileManifestValue(translations)} +`; + } } class BooleanPolicy extends BasePolicy { static from(name, category, minimumVersion, description, moduleName, settingNode) { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'boolean') { return undefined; } return new BooleanPolicy(name, category, minimumVersion, description, moduleName); } constructor(name, category, minimumVersion, description, moduleName) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.Boolean, name, category, minimumVersion, description, moduleName); } renderADMXElements() { return [ @@ -102,19 +127,39 @@ class BooleanPolicy extends BasePolicy { renderADMLPresentationContents() { return `${this.name}`; } + renderProfileValue() { + return ``; + } + renderProfileManifestValue(translations) { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +boolean`; + } } -class IntPolicy extends BasePolicy { +class ParseError extends Error { + constructor(message, moduleName, node) { + super(`${message}. ${moduleName}.ts:${node.startPosition.row + 1}`); + } +} +class NumberPolicy extends BasePolicy { defaultValue; static from(name, category, minimumVersion, description, moduleName, settingNode) { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'number') { return undefined; } - const defaultValue = getIntProperty(settingNode, 'default'); + const defaultValue = getNumberProperty(moduleName, settingNode, 'default'); if (typeof defaultValue === 'undefined') { - throw new Error(`Missing required 'default' property.`); + throw new ParseError(`Missing required 'default' property.`, moduleName, settingNode); } - return new IntPolicy(name, category, minimumVersion, description, moduleName, defaultValue); + return new NumberPolicy(name, category, minimumVersion, description, moduleName, defaultValue); } constructor(name, category, minimumVersion, description, moduleName, defaultValue) { super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); @@ -129,17 +174,32 @@ class IntPolicy extends BasePolicy { renderADMLPresentationContents() { return `${this.name}`; } + renderProfileValue() { + return `${this.defaultValue}`; + } + renderProfileManifestValue(translations) { + return `pfm_default +${this.defaultValue} +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +integer`; + } } class StringPolicy extends BasePolicy { static from(name, category, minimumVersion, description, moduleName, settingNode) { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'string') { return undefined; } return new StringPolicy(name, category, minimumVersion, description, moduleName); } constructor(name, category, minimumVersion, description, moduleName) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.String, name, category, minimumVersion, description, moduleName); } renderADMXElements() { return [``]; @@ -147,17 +207,32 @@ class StringPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + renderProfileValue() { + return ``; + } + renderProfileManifestValue(translations) { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string`; + } } class ObjectPolicy extends BasePolicy { static from(name, category, minimumVersion, description, moduleName, settingNode) { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'object' && type !== 'array') { return undefined; } return new ObjectPolicy(name, category, minimumVersion, description, moduleName); } constructor(name, category, minimumVersion, description, moduleName) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.Object, name, category, minimumVersion, description, moduleName); } renderADMXElements() { return [``]; @@ -165,28 +240,44 @@ class ObjectPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + renderProfileValue() { + return ``; + } + renderProfileManifestValue(translations) { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string +`; + } } class StringEnumPolicy extends BasePolicy { enum_; enumDescriptions; static from(name, category, minimumVersion, description, moduleName, settingNode) { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'string') { return undefined; } - const enum_ = getStringArrayProperty(settingNode, 'enum'); + const enum_ = getStringArrayProperty(moduleName, settingNode, 'enum'); if (!enum_) { return undefined; } if (!isStringArray(enum_)) { - throw new Error(`Property 'enum' should not be localized.`); + throw new ParseError(`Property 'enum' should not be localized.`, moduleName, settingNode); } - const enumDescriptions = getStringArrayProperty(settingNode, 'enumDescriptions'); + const enumDescriptions = getStringArrayProperty(moduleName, settingNode, 'enumDescriptions'); if (!enumDescriptions) { - throw new Error(`Missing required 'enumDescriptions' property.`); + throw new ParseError(`Missing required 'enumDescriptions' property.`, moduleName, settingNode); } else if (!isNlsStringArray(enumDescriptions)) { - throw new Error(`Property 'enumDescriptions' should be localized.`); + throw new ParseError(`Property 'enumDescriptions' should be localized.`, moduleName, settingNode); } return new StringEnumPolicy(name, category, minimumVersion, description, moduleName, enum_, enumDescriptions); } @@ -211,8 +302,27 @@ class StringEnumPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + renderProfileValue() { + return `${this.enum_[0]}`; + } + renderProfileManifestValue(translations) { + return `pfm_default +${this.enum_[0]} +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string +pfm_range_list + + ${this.enum_.map(e => `${e}`).join('\n ')} +`; + } } -const IntQ = { +const NumberQ = { Q: `(number) @value`, value(matches) { const match = matches[0]; @@ -229,7 +339,16 @@ const IntQ = { const StringQ = { Q: `[ (string (string_fragment) @value) - (call_expression function: (identifier) @localizeFn arguments: (arguments (string (string_fragment) @nlsKey) (string (string_fragment) @value)) (#eq? @localizeFn localize)) + (call_expression + function: [ + (identifier) @localizeFn (#eq? @localizeFn localize) + (member_expression + object: (identifier) @nlsObj (#eq? @nlsObj nls) + property: (property_identifier) @localizeFn (#eq? @localizeFn localize) + ) + ] + arguments: (arguments (string (string_fragment) @nlsKey) (string (string_fragment) @value)) + ) ]`, value(matches) { const match = matches[0]; @@ -260,47 +379,53 @@ const StringArrayQ = { }); } }; -function getProperty(qtype, node, key) { +function getProperty(qtype, moduleName, node, key) { const query = new tree_sitter_1.default.Query(typescript, `( (pair key: [(property_identifier)(string)] @key value: ${qtype.Q} ) - (#eq? @key ${key}) + (#any-of? @key "${key}" "'${key}'") )`); - return qtype.value(query.matches(node)); + try { + const matches = query.matches(node).filter(m => m.captures[0].node.parent?.parent === node); + return qtype.value(matches); + } + catch (e) { + throw new ParseError(e.message, moduleName, node); + } } -function getIntProperty(node, key) { - return getProperty(IntQ, node, key); +function getNumberProperty(moduleName, node, key) { + return getProperty(NumberQ, moduleName, node, key); } -function getStringProperty(node, key) { - return getProperty(StringQ, node, key); +function getStringProperty(moduleName, node, key) { + return getProperty(StringQ, moduleName, node, key); } -function getStringArrayProperty(node, key) { - return getProperty(StringArrayQ, node, key); +function getStringArrayProperty(moduleName, node, key) { + return getProperty(StringArrayQ, moduleName, node, key); } // TODO: add more policy types const PolicyTypes = [ BooleanPolicy, - IntPolicy, + NumberPolicy, StringEnumPolicy, StringPolicy, ObjectPolicy ]; function getPolicy(moduleName, configurationNode, settingNode, policyNode, categories) { - const name = getStringProperty(policyNode, 'name'); + const name = getStringProperty(moduleName, policyNode, 'name'); if (!name) { - throw new Error(`Missing required 'name' property.`); + throw new ParseError(`Missing required 'name' property`, moduleName, policyNode); } else if (isNlsString(name)) { - throw new Error(`Property 'name' should be a literal string.`); + throw new ParseError(`Property 'name' should be a literal string`, moduleName, policyNode); } - const categoryName = getStringProperty(configurationNode, 'title'); + const categoryName = getStringProperty(moduleName, configurationNode, 'title'); if (!categoryName) { - throw new Error(`Missing required 'title' property.`); + throw new ParseError(`Missing required 'title' property`, moduleName, configurationNode); } else if (!isNlsString(categoryName)) { - throw new Error(`Property 'title' should be localized.`); + throw new ParseError(`Property 'title' should be localized`, moduleName, configurationNode); } const categoryKey = `${categoryName.nlsKey}:${categoryName.value}`; let category = categories.get(categoryKey); @@ -308,19 +433,19 @@ function getPolicy(moduleName, configurationNode, settingNode, policyNode, categ category = { moduleName, name: categoryName }; categories.set(categoryKey, category); } - const minimumVersion = getStringProperty(policyNode, 'minimumVersion'); + const minimumVersion = getStringProperty(moduleName, policyNode, 'minimumVersion'); if (!minimumVersion) { - throw new Error(`Missing required 'minimumVersion' property.`); + throw new ParseError(`Missing required 'minimumVersion' property.`, moduleName, policyNode); } else if (isNlsString(minimumVersion)) { - throw new Error(`Property 'minimumVersion' should be a literal string.`); + throw new ParseError(`Property 'minimumVersion' should be a literal string.`, moduleName, policyNode); } - const description = getStringProperty(settingNode, 'description'); + const description = getStringProperty(moduleName, policyNode, 'description') ?? getStringProperty(moduleName, settingNode, 'description'); if (!description) { - throw new Error(`Missing required 'description' property.`); + throw new ParseError(`Missing required 'description' property.`, moduleName, settingNode); } if (!isNlsString(description)) { - throw new Error(`Property 'description' should be localized.`); + throw new ParseError(`Property 'description' should be localized.`, moduleName, settingNode); } let result; for (const policyType of PolicyTypes) { @@ -329,7 +454,7 @@ function getPolicy(moduleName, configurationNode, settingNode, policyNode, categ } } if (!result) { - throw new Error(`Failed to parse policy '${name}'.`); + throw new ParseError(`Failed to parse policy '${name}'.`, moduleName, settingNode); } return result; } @@ -339,11 +464,11 @@ function getPolicies(moduleName, node) { (call_expression function: (member_expression property: (property_identifier) @registerConfigurationFn) (#eq? @registerConfigurationFn registerConfiguration) arguments: (arguments (object (pair - key: [(property_identifier)(string)] @propertiesKey (#eq? @propertiesKey properties) + key: [(property_identifier)(string)] @propertiesKey (#any-of? @propertiesKey "properties" "'properties'") value: (object (pair key: [(property_identifier)(string)(computed_property_name)] value: (object (pair - key: [(property_identifier)(string)] @policyKey (#eq? @policyKey policy) + key: [(property_identifier)(string)] @policyKey (#any-of? @policyKey "policy" "'policy'") value: (object) @policy )) @setting )) @@ -400,8 +525,8 @@ function renderADML(appName, versions, categories, policies, translations) { ${appName} - ${versions.map(v => `${appName} >= ${v}`)} - ${categories.map(c => renderADMLString('Category', c.moduleName, c.name, translations))} + ${versions.map(v => `${appName} >= ${v}`).join(`\n `)} + ${categories.map(c => renderADMLString('Category', c.moduleName, c.name, translations)).join(`\n `)} ${policies.map(p => p.renderADMLStrings(translations)).flat().join(`\n `)} @@ -411,6 +536,186 @@ function renderADML(appName, versions, categories, policies, translations) { `; } +function renderProfileManifest(appName, bundleIdentifier, _versions, _categories, policies, translations) { + const requiredPayloadFields = ` + + pfm_default + Configure ${appName} + pfm_name + PayloadDescription + pfm_title + Payload Description + pfm_type + string + + + pfm_default + ${appName} + pfm_name + PayloadDisplayName + pfm_require + always + pfm_title + Payload Display Name + pfm_type + string + + + pfm_default + ${bundleIdentifier} + pfm_name + PayloadIdentifier + pfm_require + always + pfm_title + Payload Identifier + pfm_type + string + + + pfm_default + ${bundleIdentifier} + pfm_name + PayloadType + pfm_require + always + pfm_title + Payload Type + pfm_type + string + + + pfm_default + + pfm_name + PayloadUUID + pfm_require + always + pfm_title + Payload UUID + pfm_type + string + + + pfm_default + 1 + pfm_name + PayloadVersion + pfm_range_list + + 1 + + pfm_require + always + pfm_title + Payload Version + pfm_type + integer + + + pfm_default + Microsoft + pfm_name + PayloadOrganization + pfm_title + Payload Organization + pfm_type + string + `; + const profileManifestSubkeys = policies.map(policy => { + return policy.renderProfileManifest(translations); + }).join(''); + return ` + + + + pfm_app_url + https://code.visualstudio.com/ + pfm_description + ${appName} Managed Settings + pfm_documentation_url + https://code.visualstudio.com/docs/setup/enterprise + pfm_domain + ${bundleIdentifier} + pfm_format_version + 1 + pfm_interaction + combined + pfm_last_modified + ${new Date().toISOString().replace(/\.\d+Z$/, 'Z')} + pfm_platforms + + macOS + + pfm_subkeys + + ${requiredPayloadFields} + ${profileManifestSubkeys} + + pfm_title + ${appName} + pfm_unique + + pfm_version + 1 + +`; +} +function renderMacOSPolicy(policies, translations) { + const appName = product.nameLong; + const bundleIdentifier = product.darwinBundleIdentifier; + const payloadUUID = product.darwinProfilePayloadUUID; + const UUID = product.darwinProfileUUID; + const versions = [...new Set(policies.map(p => p.minimumVersion)).values()].sort(); + const categories = [...new Set(policies.map(p => p.category))]; + const policyEntries = policies.map(policy => policy.renderProfile()) + .flat() + .map(entry => `\t\t\t\t${entry}`) + .join('\n'); + return { + profile: ` + + + + PayloadContent + + + PayloadDisplayName + ${appName} + PayloadIdentifier + ${bundleIdentifier}.${UUID} + PayloadType + ${bundleIdentifier} + PayloadUUID + ${UUID} + PayloadVersion + 1 +${policyEntries} + + + PayloadDescription + This profile manages ${appName}. For more information see https://code.visualstudio.com/docs/setup/enterprise + PayloadDisplayName + ${appName} + PayloadIdentifier + ${bundleIdentifier} + PayloadOrganization + Microsoft + PayloadType + Configuration + PayloadUUID + ${payloadUUID} + PayloadVersion + 1 + TargetDeviceType + 5 + +`, + manifests: [{ languageId: 'en-us', contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies) }, + ...translations.map(({ languageId, languageTranslations }) => ({ languageId, contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies, languageTranslations) })) + ] + }; +} function renderGP(policies, translations) { const appName = product.nameLong; const regKey = product.win32RegValueName; @@ -526,10 +831,9 @@ async function getTranslations() { return await Promise.all(languageIds.map(languageId => getNLS(extensionGalleryServiceUrl, resourceUrlTemplate, languageId, version) .then(languageTranslations => ({ languageId, languageTranslations })))); } -async function main() { - const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]); - const { admx, adml } = await renderGP(policies, translations); +async function windowsMain(policies, translations) { const root = '.build/policies/win32'; + const { admx, adml } = await renderGP(policies, translations); await fs_1.promises.rm(root, { recursive: true, force: true }); await fs_1.promises.mkdir(root, { recursive: true }); await fs_1.promises.writeFile(path_1.default.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n')); @@ -539,9 +843,44 @@ async function main() { await fs_1.promises.writeFile(path_1.default.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n')); } } +async function darwinMain(policies, translations) { + const bundleIdentifier = product.darwinBundleIdentifier; + if (!bundleIdentifier || !product.darwinProfilePayloadUUID || !product.darwinProfileUUID) { + throw new Error(`Missing required product information.`); + } + const root = '.build/policies/darwin'; + const { profile, manifests } = await renderMacOSPolicy(policies, translations); + await fs_1.promises.rm(root, { recursive: true, force: true }); + await fs_1.promises.mkdir(root, { recursive: true }); + await fs_1.promises.writeFile(path_1.default.join(root, `${bundleIdentifier}.mobileconfig`), profile.replace(/\r?\n/g, '\n')); + for (const { languageId, contents } of manifests) { + const languagePath = path_1.default.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId]); + await fs_1.promises.mkdir(languagePath, { recursive: true }); + await fs_1.promises.writeFile(path_1.default.join(languagePath, `${bundleIdentifier}.plist`), contents.replace(/\r?\n/g, '\n')); + } +} +async function main() { + const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]); + const platform = process.argv[2]; + if (platform === 'darwin') { + await darwinMain(policies, translations); + } + else if (platform === 'win32') { + await windowsMain(policies, translations); + } + else { + console.error(`Usage: node build/lib/policies `); + process.exit(1); + } +} if (require.main === module) { main().catch(err => { - console.error(err); + if (err instanceof ParseError) { + console.error(`Parse Error:`, err.message); + } + else { + console.error(err); + } process.exit(1); }); } diff --git a/build/lib/policies.ts b/build/lib/policies.ts index 2488920c..34d20e93 100644 --- a/build/lib/policies.ts +++ b/build/lib/policies.ts @@ -33,15 +33,24 @@ interface Category { } enum PolicyType { - StringEnum + Boolean = 'boolean', + Number = 'number', + Object = 'object', + String = 'string', + StringEnum = 'stringEnum', } interface Policy { + readonly name: string; + readonly type: PolicyType; readonly category: Category; readonly minimumVersion: string; renderADMX(regKey: string): string[]; renderADMLStrings(translations?: LanguageTranslations): string[]; renderADMLPresentation(): string; + renderProfile(): string[]; + // https://github.com/ProfileManifests/ProfileManifests/wiki/Manifest-Format + renderProfileManifest(translations?: LanguageTranslations): string; } function renderADMLString(prefix: string, moduleName: string, nlsString: NlsString, translations?: LanguageTranslations): string { @@ -62,10 +71,28 @@ function renderADMLString(prefix: string, moduleName: string, nlsString: NlsStri return `${value}`; } +function renderProfileString(_prefix: string, moduleName: string, nlsString: NlsString, translations?: LanguageTranslations): string { + let value: string | undefined; + + if (translations) { + const moduleTranslations = translations[moduleName]; + + if (moduleTranslations) { + value = moduleTranslations[nlsString.nlsKey]; + } + } + + if (!value) { + value = nlsString.value; + } + + return value; +} + abstract class BasePolicy implements Policy { constructor( - protected policyType: PolicyType, - protected name: string, + readonly type: PolicyType, + readonly name: string, readonly category: Category, readonly minimumVersion: string, protected description: NlsString, @@ -102,6 +129,19 @@ abstract class BasePolicy implements Policy { } protected abstract renderADMLPresentationContents(): string; + + renderProfile() { + return [`${this.name}`, this.renderProfileValue()]; + } + + renderProfileManifest(translations?: LanguageTranslations): string { + return ` +${this.renderProfileManifestValue(translations)} +`; + } + + abstract renderProfileValue(): string; + abstract renderProfileManifestValue(translations?: LanguageTranslations): string; } class BooleanPolicy extends BasePolicy { @@ -114,7 +154,7 @@ class BooleanPolicy extends BasePolicy { moduleName: string, settingNode: Parser.SyntaxNode ): BooleanPolicy | undefined { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'boolean') { return undefined; @@ -130,7 +170,7 @@ class BooleanPolicy extends BasePolicy { description: NlsString, moduleName: string, ) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.Boolean, name, category, minimumVersion, description, moduleName); } protected renderADMXElements(): string[] { @@ -144,9 +184,32 @@ class BooleanPolicy extends BasePolicy { renderADMLPresentationContents() { return `${this.name}`; } + + renderProfileValue(): string { + return ``; + } + + renderProfileManifestValue(translations?: LanguageTranslations): string { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +boolean`; + } } -class IntPolicy extends BasePolicy { +class ParseError extends Error { + constructor(message: string, moduleName: string, node: Parser.SyntaxNode) { + super(`${message}. ${moduleName}.ts:${node.startPosition.row + 1}`); + } +} + +class NumberPolicy extends BasePolicy { static from( name: string, @@ -155,20 +218,20 @@ class IntPolicy extends BasePolicy { description: NlsString, moduleName: string, settingNode: Parser.SyntaxNode - ): IntPolicy | undefined { - const type = getStringProperty(settingNode, 'type'); + ): NumberPolicy | undefined { + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'number') { return undefined; } - const defaultValue = getIntProperty(settingNode, 'default'); + const defaultValue = getNumberProperty(moduleName, settingNode, 'default'); if (typeof defaultValue === 'undefined') { - throw new Error(`Missing required 'default' property.`); + throw new ParseError(`Missing required 'default' property.`, moduleName, settingNode); } - return new IntPolicy(name, category, minimumVersion, description, moduleName, defaultValue); + return new NumberPolicy(name, category, minimumVersion, description, moduleName, defaultValue); } private constructor( @@ -192,6 +255,23 @@ class IntPolicy extends BasePolicy { renderADMLPresentationContents() { return `${this.name}`; } + + renderProfileValue() { + return `${this.defaultValue}`; + } + + renderProfileManifestValue(translations?: LanguageTranslations) { + return `pfm_default +${this.defaultValue} +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +integer`; + } } class StringPolicy extends BasePolicy { @@ -204,7 +284,7 @@ class StringPolicy extends BasePolicy { moduleName: string, settingNode: Parser.SyntaxNode ): StringPolicy | undefined { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'string') { return undefined; @@ -220,7 +300,7 @@ class StringPolicy extends BasePolicy { description: NlsString, moduleName: string, ) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.String, name, category, minimumVersion, description, moduleName); } protected renderADMXElements(): string[] { @@ -230,6 +310,23 @@ class StringPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + + renderProfileValue(): string { + return ``; + } + + renderProfileManifestValue(translations?: LanguageTranslations): string { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string`; + } } class ObjectPolicy extends BasePolicy { @@ -242,7 +339,7 @@ class ObjectPolicy extends BasePolicy { moduleName: string, settingNode: Parser.SyntaxNode ): ObjectPolicy | undefined { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'object' && type !== 'array') { return undefined; @@ -258,7 +355,7 @@ class ObjectPolicy extends BasePolicy { description: NlsString, moduleName: string, ) { - super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + super(PolicyType.Object, name, category, minimumVersion, description, moduleName); } protected renderADMXElements(): string[] { @@ -268,6 +365,24 @@ class ObjectPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + + renderProfileValue(): string { + return ``; + } + + renderProfileManifestValue(translations?: LanguageTranslations): string { + return `pfm_default + +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string +`; + } } class StringEnumPolicy extends BasePolicy { @@ -280,28 +395,28 @@ class StringEnumPolicy extends BasePolicy { moduleName: string, settingNode: Parser.SyntaxNode ): StringEnumPolicy | undefined { - const type = getStringProperty(settingNode, 'type'); + const type = getStringProperty(moduleName, settingNode, 'type'); if (type !== 'string') { return undefined; } - const enum_ = getStringArrayProperty(settingNode, 'enum'); + const enum_ = getStringArrayProperty(moduleName, settingNode, 'enum'); if (!enum_) { return undefined; } if (!isStringArray(enum_)) { - throw new Error(`Property 'enum' should not be localized.`); + throw new ParseError(`Property 'enum' should not be localized.`, moduleName, settingNode); } - const enumDescriptions = getStringArrayProperty(settingNode, 'enumDescriptions'); + const enumDescriptions = getStringArrayProperty(moduleName, settingNode, 'enumDescriptions'); if (!enumDescriptions) { - throw new Error(`Missing required 'enumDescriptions' property.`); + throw new ParseError(`Missing required 'enumDescriptions' property.`, moduleName, settingNode); } else if (!isNlsStringArray(enumDescriptions)) { - throw new Error(`Property 'enumDescriptions' should be localized.`); + throw new ParseError(`Property 'enumDescriptions' should be localized.`, moduleName, settingNode); } return new StringEnumPolicy(name, category, minimumVersion, description, moduleName, enum_, enumDescriptions); @@ -337,6 +452,27 @@ class StringEnumPolicy extends BasePolicy { renderADMLPresentationContents() { return ``; } + + renderProfileValue() { + return `${this.enum_[0]}`; + } + + renderProfileManifestValue(translations?: LanguageTranslations): string { + return `pfm_default +${this.enum_[0]} +pfm_description +${renderProfileString(this.name, this.moduleName, this.description, translations)} +pfm_name +${this.name} +pfm_title +${this.name} +pfm_type +string +pfm_range_list + + ${this.enum_.map(e => `${e}`).join('\n ')} +`; + } } interface QType { @@ -344,7 +480,7 @@ interface QType { value(matches: Parser.QueryMatch[]): T | undefined; } -const IntQ: QType = { +const NumberQ: QType = { Q: `(number) @value`, value(matches: Parser.QueryMatch[]): number | undefined { @@ -367,7 +503,16 @@ const IntQ: QType = { const StringQ: QType = { Q: `[ (string (string_fragment) @value) - (call_expression function: (identifier) @localizeFn arguments: (arguments (string (string_fragment) @nlsKey) (string (string_fragment) @value)) (#eq? @localizeFn localize)) + (call_expression + function: [ + (identifier) @localizeFn (#eq? @localizeFn localize) + (member_expression + object: (identifier) @nlsObj (#eq? @nlsObj nls) + property: (property_identifier) @localizeFn (#eq? @localizeFn localize) + ) + ] + arguments: (arguments (string (string_fragment) @nlsKey) (string (string_fragment) @value)) + ) ]`, value(matches: Parser.QueryMatch[]): string | NlsString | undefined { @@ -407,7 +552,7 @@ const StringArrayQ: QType<(string | NlsString)[]> = { } }; -function getProperty(qtype: QType, node: Parser.SyntaxNode, key: string): T | undefined { +function getProperty(qtype: QType, moduleName: string, node: Parser.SyntaxNode, key: string): T | undefined { const query = new Parser.Query( typescript, `( @@ -415,29 +560,34 @@ function getProperty(qtype: QType, node: Parser.SyntaxNode, key: string): key: [(property_identifier)(string)] @key value: ${qtype.Q} ) - (#eq? @key ${key}) + (#any-of? @key "${key}" "'${key}'") )` ); - return qtype.value(query.matches(node)); + try { + const matches = query.matches(node).filter(m => m.captures[0].node.parent?.parent === node); + return qtype.value(matches); + } catch (e) { + throw new ParseError(e.message, moduleName, node); + } } -function getIntProperty(node: Parser.SyntaxNode, key: string): number | undefined { - return getProperty(IntQ, node, key); +function getNumberProperty(moduleName: string, node: Parser.SyntaxNode, key: string): number | undefined { + return getProperty(NumberQ, moduleName, node, key); } -function getStringProperty(node: Parser.SyntaxNode, key: string): string | NlsString | undefined { - return getProperty(StringQ, node, key); +function getStringProperty(moduleName: string, node: Parser.SyntaxNode, key: string): string | NlsString | undefined { + return getProperty(StringQ, moduleName, node, key); } -function getStringArrayProperty(node: Parser.SyntaxNode, key: string): (string | NlsString)[] | undefined { - return getProperty(StringArrayQ, node, key); +function getStringArrayProperty(moduleName: string, node: Parser.SyntaxNode, key: string): (string | NlsString)[] | undefined { + return getProperty(StringArrayQ, moduleName, node, key); } // TODO: add more policy types const PolicyTypes = [ BooleanPolicy, - IntPolicy, + NumberPolicy, StringEnumPolicy, StringPolicy, ObjectPolicy @@ -450,20 +600,20 @@ function getPolicy( policyNode: Parser.SyntaxNode, categories: Map ): Policy { - const name = getStringProperty(policyNode, 'name'); + const name = getStringProperty(moduleName, policyNode, 'name'); if (!name) { - throw new Error(`Missing required 'name' property.`); + throw new ParseError(`Missing required 'name' property`, moduleName, policyNode); } else if (isNlsString(name)) { - throw new Error(`Property 'name' should be a literal string.`); + throw new ParseError(`Property 'name' should be a literal string`, moduleName, policyNode); } - const categoryName = getStringProperty(configurationNode, 'title'); + const categoryName = getStringProperty(moduleName, configurationNode, 'title'); if (!categoryName) { - throw new Error(`Missing required 'title' property.`); + throw new ParseError(`Missing required 'title' property`, moduleName, configurationNode); } else if (!isNlsString(categoryName)) { - throw new Error(`Property 'title' should be localized.`); + throw new ParseError(`Property 'title' should be localized`, moduleName, configurationNode); } const categoryKey = `${categoryName.nlsKey}:${categoryName.value}`; @@ -474,20 +624,20 @@ function getPolicy( categories.set(categoryKey, category); } - const minimumVersion = getStringProperty(policyNode, 'minimumVersion'); + const minimumVersion = getStringProperty(moduleName, policyNode, 'minimumVersion'); if (!minimumVersion) { - throw new Error(`Missing required 'minimumVersion' property.`); + throw new ParseError(`Missing required 'minimumVersion' property.`, moduleName, policyNode); } else if (isNlsString(minimumVersion)) { - throw new Error(`Property 'minimumVersion' should be a literal string.`); + throw new ParseError(`Property 'minimumVersion' should be a literal string.`, moduleName, policyNode); } - const description = getStringProperty(settingNode, 'description'); + const description = getStringProperty(moduleName, policyNode, 'description') ?? getStringProperty(moduleName, settingNode, 'description'); if (!description) { - throw new Error(`Missing required 'description' property.`); + throw new ParseError(`Missing required 'description' property.`, moduleName, settingNode); } if (!isNlsString(description)) { - throw new Error(`Property 'description' should be localized.`); + throw new ParseError(`Property 'description' should be localized.`, moduleName, settingNode); } let result: Policy | undefined; @@ -499,7 +649,7 @@ function getPolicy( } if (!result) { - throw new Error(`Failed to parse policy '${name}'.`); + throw new ParseError(`Failed to parse policy '${name}'.`, moduleName, settingNode); } return result; @@ -511,11 +661,11 @@ function getPolicies(moduleName: string, node: Parser.SyntaxNode): Policy[] { (call_expression function: (member_expression property: (property_identifier) @registerConfigurationFn) (#eq? @registerConfigurationFn registerConfiguration) arguments: (arguments (object (pair - key: [(property_identifier)(string)] @propertiesKey (#eq? @propertiesKey properties) + key: [(property_identifier)(string)] @propertiesKey (#any-of? @propertiesKey "properties" "'properties'") value: (object (pair key: [(property_identifier)(string)(computed_property_name)] value: (object (pair - key: [(property_identifier)(string)] @policyKey (#eq? @policyKey policy) + key: [(property_identifier)(string)] @policyKey (#any-of? @policyKey "policy" "'policy'") value: (object) @policy )) @setting )) @@ -578,8 +728,8 @@ function renderADML(appName: string, versions: string[], categories: Category[], ${appName} - ${versions.map(v => `${appName} >= ${v}`)} - ${categories.map(c => renderADMLString('Category', c.moduleName, c.name, translations))} + ${versions.map(v => `${appName} >= ${v}`).join(`\n `)} + ${categories.map(c => renderADMLString('Category', c.moduleName, c.name, translations)).join(`\n `)} ${policies.map(p => p.renderADMLStrings(translations)).flat().join(`\n `)} @@ -590,6 +740,197 @@ function renderADML(appName: string, versions: string[], categories: Category[], `; } +function renderProfileManifest(appName: string, bundleIdentifier: string, _versions: string[], _categories: Category[], policies: Policy[], translations?: LanguageTranslations) { + + const requiredPayloadFields = ` + + pfm_default + Configure ${appName} + pfm_name + PayloadDescription + pfm_title + Payload Description + pfm_type + string + + + pfm_default + ${appName} + pfm_name + PayloadDisplayName + pfm_require + always + pfm_title + Payload Display Name + pfm_type + string + + + pfm_default + ${bundleIdentifier} + pfm_name + PayloadIdentifier + pfm_require + always + pfm_title + Payload Identifier + pfm_type + string + + + pfm_default + ${bundleIdentifier} + pfm_name + PayloadType + pfm_require + always + pfm_title + Payload Type + pfm_type + string + + + pfm_default + + pfm_name + PayloadUUID + pfm_require + always + pfm_title + Payload UUID + pfm_type + string + + + pfm_default + 1 + pfm_name + PayloadVersion + pfm_range_list + + 1 + + pfm_require + always + pfm_title + Payload Version + pfm_type + integer + + + pfm_default + Microsoft + pfm_name + PayloadOrganization + pfm_title + Payload Organization + pfm_type + string + `; + + const profileManifestSubkeys = policies.map(policy => { + return policy.renderProfileManifest(translations); + }).join(''); + + return ` + + + + pfm_app_url + https://code.visualstudio.com/ + pfm_description + ${appName} Managed Settings + pfm_documentation_url + https://code.visualstudio.com/docs/setup/enterprise + pfm_domain + ${bundleIdentifier} + pfm_format_version + 1 + pfm_interaction + combined + pfm_last_modified + ${new Date().toISOString().replace(/\.\d+Z$/, 'Z')} + pfm_platforms + + macOS + + pfm_subkeys + + ${requiredPayloadFields} + ${profileManifestSubkeys} + + pfm_title + ${appName} + pfm_unique + + pfm_version + 1 + +`; +} + +function renderMacOSPolicy(policies: Policy[], translations: Translations) { + const appName = product.nameLong; + const bundleIdentifier = product.darwinBundleIdentifier; + const payloadUUID = product.darwinProfilePayloadUUID; + const UUID = product.darwinProfileUUID; + + const versions = [...new Set(policies.map(p => p.minimumVersion)).values()].sort(); + const categories = [...new Set(policies.map(p => p.category))]; + + const policyEntries = + policies.map(policy => policy.renderProfile()) + .flat() + .map(entry => `\t\t\t\t${entry}`) + .join('\n'); + + + return { + profile: ` + + + + PayloadContent + + + PayloadDisplayName + ${appName} + PayloadIdentifier + ${bundleIdentifier}.${UUID} + PayloadType + ${bundleIdentifier} + PayloadUUID + ${UUID} + PayloadVersion + 1 +${policyEntries} + + + PayloadDescription + This profile manages ${appName}. For more information see https://code.visualstudio.com/docs/setup/enterprise + PayloadDisplayName + ${appName} + PayloadIdentifier + ${bundleIdentifier} + PayloadOrganization + Microsoft + PayloadType + Configuration + PayloadUUID + ${payloadUUID} + PayloadVersion + 1 + TargetDeviceType + 5 + +`, + manifests: [{ languageId: 'en-us', contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies) }, + ...translations.map(({ languageId, languageTranslations }) => + ({ languageId, contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies, languageTranslations) })) + ] + }; +} + function renderGP(policies: Policy[], translations: Translations) { const appName = product.nameLong; const regKey = product.win32RegValueName; @@ -735,11 +1076,10 @@ async function getTranslations(): Promise { )); } -async function main() { - const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]); +async function windowsMain(policies: Policy[], translations: Translations) { + const root = '.build/policies/win32'; const { admx, adml } = await renderGP(policies, translations); - const root = '.build/policies/win32'; await fs.rm(root, { recursive: true, force: true }); await fs.mkdir(root, { recursive: true }); @@ -752,9 +1092,46 @@ async function main() { } } +async function darwinMain(policies: Policy[], translations: Translations) { + const bundleIdentifier = product.darwinBundleIdentifier; + if (!bundleIdentifier || !product.darwinProfilePayloadUUID || !product.darwinProfileUUID) { + throw new Error(`Missing required product information.`); + } + const root = '.build/policies/darwin'; + const { profile, manifests } = await renderMacOSPolicy(policies, translations); + + await fs.rm(root, { recursive: true, force: true }); + await fs.mkdir(root, { recursive: true }); + await fs.writeFile(path.join(root, `${bundleIdentifier}.mobileconfig`), profile.replace(/\r?\n/g, '\n')); + + for (const { languageId, contents } of manifests) { + const languagePath = path.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId as keyof typeof Languages]); + await fs.mkdir(languagePath, { recursive: true }); + await fs.writeFile(path.join(languagePath, `${bundleIdentifier}.plist`), contents.replace(/\r?\n/g, '\n')); + } +} + +async function main() { + const [policies, translations] = await Promise.all([parsePolicies(), getTranslations()]); + const platform = process.argv[2]; + + if (platform === 'darwin') { + await darwinMain(policies, translations); + } else if (platform === 'win32') { + await windowsMain(policies, translations); + } else { + console.error(`Usage: node build/lib/policies `); + process.exit(1); + } +} + if (require.main === module) { main().catch(err => { - console.error(err); + if (err instanceof ParseError) { + console.error(`Parse Error:`, err.message); + } else { + console.error(err); + } process.exit(1); }); } diff --git a/build/lib/propertyInitOrderChecker.js b/build/lib/propertyInitOrderChecker.js new file mode 100644 index 00000000..dbca887b --- /dev/null +++ b/build/lib/propertyInitOrderChecker.js @@ -0,0 +1,367 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.EntryKind = void 0; +const ts = __importStar(require("typescript")); +const path = __importStar(require("path")); +const fs = __importStar(require("fs")); +const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); +// +// ############################################################################################# +// +// A custom typescript checker that ensure constructor properties are NOT used to initialize +// defined properties. This is needed for the times when `useDefineForClassFields` is gone. +// +// see https://github.com/microsoft/vscode/issues/243049, https://github.com/microsoft/vscode/issues/186726, +// https://github.com/microsoft/vscode/pull/241544 +// +// ############################################################################################# +// +const ignored = new Set([ + 'vs/base/common/arrays.ts', + 'vs/platform/extensionManagement/common/extensionsScannerService.ts', + 'vs/platform/configuration/common/configurations.ts', + 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts', + 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts', + 'vs/editor/common/model/textModelTokens.ts', + 'vs/editor/common/model/tokenizationTextModelPart.ts', + 'vs/editor/common/core/textEdit.ts', + 'vs/workbench/contrib/debug/common/debugStorage.ts', + 'vs/workbench/contrib/debug/common/debugModel.ts', + 'vs/workbench/api/common/extHostCommands.ts', + 'vs/editor/browser/view/viewLayer.ts', + 'vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts', + 'vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts', + 'vs/editor/browser/widget/diffEditor/utils.ts', + 'vs/editor/browser/observableCodeEditor.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts', + 'vs/editor/browser/widget/diffEditor/diffEditorOptions.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts', + 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts', + 'vs/editor/browser/widget/diffEditor/utils/editorGutter.ts', + 'vs/editor/browser/widget/diffEditor/features/gutterFeature.ts', + 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts', + 'vs/editor/browser/widget/diffEditor/diffEditorWidget.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts', + 'vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts', + 'vs/editor/contrib/inlayHints/browser/inlayHintsController.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts', + 'vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts', + 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts', + 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts', + 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts', + 'vs/workbench/contrib/files/browser/views/openEditorsView.ts', + 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts', + 'vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts', + 'vs/workbench/contrib/chat/browser/chatInputPart.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts', + 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts', + 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts', + 'vs/platform/terminal/common/capabilities/commandDetectionCapability.ts', + 'vs/workbench/contrib/testing/common/testExclusions.ts', + 'vs/workbench/contrib/testing/common/testResultStorage.ts', + 'vs/workbench/services/userDataProfile/browser/snippetsResource.ts', + 'vs/platform/quickinput/browser/quickInputController.ts', + 'vs/platform/userDataSync/common/abstractSynchronizer.ts', + 'vs/workbench/services/authentication/browser/authenticationExtensionsService.ts', + 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts', + 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', + 'vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts', + 'vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts', + 'vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts', + 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts', + 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts', + 'vs/workbench/contrib/search/common/cacheState.ts', + 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts', + 'vs/workbench/contrib/search/browser/anythingQuickAccess.ts', + 'vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts', + 'vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts', + 'vs/workbench/contrib/testing/common/testExplorerFilterState.ts', + 'vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts', + 'vs/workbench/contrib/testing/browser/testingOutputPeek.ts', + 'vs/workbench/contrib/testing/browser/explorerProjections/index.ts', + 'vs/workbench/contrib/testing/browser/testingExplorerFilter.ts', + 'vs/workbench/contrib/testing/browser/testingExplorerView.ts', + 'vs/workbench/contrib/testing/common/testServiceImpl.ts', + 'vs/platform/quickinput/browser/commandsQuickAccess.ts', + 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts', + 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts', + 'vs/workbench/contrib/debug/browser/debugMemory.ts', + 'vs/workbench/contrib/markers/browser/markersViewActions.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts', + 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts', + 'vs/workbench/contrib/output/browser/outputServices.ts', + 'vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts', + 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts', + 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts', + 'vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts', + 'vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts', + 'vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts', + 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts', + 'vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts', + 'vs/platform/terminal/node/ptyService.ts', + 'vs/workbench/api/common/extHostLanguageFeatures.ts', + 'vs/workbench/api/common/extHostSearch.ts', + 'vs/workbench/contrib/testing/test/common/testStubs.ts' +]); +const cancellationToken = { + isCancellationRequested: () => false, + throwIfCancellationRequested: () => { }, +}; +const seenFiles = new Set(); +let errorCount = 0; +function createProgram(tsconfigPath) { + const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + const configHostParser = { fileExists: fs.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => fs.readFileSync(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, path.resolve(path.dirname(tsconfigPath)), { noEmit: true }); + const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); + return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); +} +const program = createProgram(TS_CONFIG_PATH); +program.getTypeChecker(); +for (const file of program.getSourceFiles()) { + if (!file || file.isDeclarationFile) { + continue; + } + const relativePath = path.relative(path.dirname(TS_CONFIG_PATH), file.fileName).replace(/\\/g, '/'); + if (ignored.has(relativePath)) { + continue; + } + visit(file); +} +if (seenFiles.size) { + console.log(); + console.log(`Found ${errorCount} error${errorCount === 1 ? '' : 's'} in ${seenFiles.size} file${seenFiles.size === 1 ? '' : 's'}.`); + process.exit(errorCount); +} +function visit(node) { + if (ts.isParameter(node) && ts.isParameterPropertyDeclaration(node, node.parent)) { + checkParameterPropertyDeclaration(node); + } + ts.forEachChild(node, visit); +} +function checkParameterPropertyDeclaration(param) { + const uses = [...collectReferences(param.name, [])]; + if (!uses.length) { + return; + } + const sourceFile = param.getSourceFile(); + if (!seenFiles.has(sourceFile)) { + if (seenFiles.size) { + console.log(``); + } + console.log(`${formatFileName(param)}:`); + seenFiles.add(sourceFile); + } + else { + console.log(``); + } + console.log(` Parameter property '${param.name.getText()}' is used before its declaration.`); + for (const { stack, container } of uses) { + const use = stack[stack.length - 1]; + console.log(` at ${formatLocation(use)}: ${formatMember(container)} -> ${formatStack(stack)}`); + errorCount++; + } +} +function* collectReferences(node, stack, requiresInvocationDepth = 0, seen = new Set()) { + for (const use of findAllReferencesInClass(node)) { + const container = findContainer(use); + if (!container || seen.has(container) || ts.isConstructorDeclaration(container)) { + continue; + } + seen.add(container); + const nextStack = [...stack, use]; + let nextRequiresInvocationDepth = requiresInvocationDepth; + if (isInvocation(use) && nextRequiresInvocationDepth > 0) { + nextRequiresInvocationDepth--; + } + if (ts.isPropertyDeclaration(container) && nextRequiresInvocationDepth === 0) { + yield { stack: nextStack, container }; + } + else if (requiresInvocation(container)) { + nextRequiresInvocationDepth++; + } + yield* collectReferences(container.name ?? container, nextStack, nextRequiresInvocationDepth, seen); + } +} +function requiresInvocation(definition) { + return ts.isMethodDeclaration(definition) || ts.isFunctionDeclaration(definition) || ts.isFunctionExpression(definition) || ts.isArrowFunction(definition); +} +function isInvocation(use) { + let location = use; + if (ts.isPropertyAccessExpression(location.parent) && location.parent.name === location) { + location = location.parent; + } + else if (ts.isElementAccessExpression(location.parent) && location.parent.argumentExpression === location) { + location = location.parent; + } + return ts.isCallExpression(location.parent) && location.parent.expression === location + || ts.isTaggedTemplateExpression(location.parent) && location.parent.tag === location; +} +function formatFileName(node) { + const sourceFile = node.getSourceFile(); + return path.resolve(sourceFile.fileName); +} +function formatLocation(node) { + const sourceFile = node.getSourceFile(); + const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.pos); + return `${formatFileName(sourceFile)}(${line + 1},${character + 1})`; +} +function formatStack(stack) { + return stack.slice().reverse().map((use) => formatUse(use)).join(' -> '); +} +function formatMember(container) { + const name = container.name?.getText(); + if (name) { + const className = findClass(container)?.name?.getText(); + if (className) { + return `${className}.${name}`; + } + return name; + } + return ''; +} +function formatUse(use) { + let text = use.getText(); + if (use.parent && ts.isPropertyAccessExpression(use.parent) && use.parent.name === use) { + if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) { + text = `this.${text}`; + } + use = use.parent; + } + else if (use.parent && ts.isElementAccessExpression(use.parent) && use.parent.argumentExpression === use) { + if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) { + text = `this['${text}']`; + } + use = use.parent; + } + if (ts.isCallExpression(use.parent)) { + text = `${text}(...)`; + } + return text; +} +function findContainer(node) { + return ts.findAncestor(node, ancestor => { + switch (ancestor.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ClassStaticBlockDeclaration: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.Parameter: + return true; + } + return false; + }); +} +function findClass(node) { + return ts.findAncestor(node, ts.isClassLike); +} +function* findAllReferencesInClass(node) { + const classDecl = findClass(node); + if (!classDecl) { + return []; + } + for (const ref of findAllReferences(node)) { + for (const entry of ref.references) { + if (entry.kind !== 1 /* EntryKind.Node */ || entry.node === node) { + continue; + } + if (findClass(entry.node) === classDecl) { + yield entry.node; + } + } + } +} +// NOTE: The following uses TypeScript internals and are subject to change from version to version. +function findAllReferences(node) { + const sourceFile = node.getSourceFile(); + const position = node.getStart(); + const name = ts.getTouchingPropertyName(sourceFile, position); + const options = { use: ts.FindAllReferences.FindReferencesUse.References }; + return ts.FindAllReferences.Core.getReferencedSymbolsForNode(position, name, program, [sourceFile], cancellationToken, options) ?? []; +} +var DefinitionKind; +(function (DefinitionKind) { + DefinitionKind[DefinitionKind["Symbol"] = 0] = "Symbol"; + DefinitionKind[DefinitionKind["Label"] = 1] = "Label"; + DefinitionKind[DefinitionKind["Keyword"] = 2] = "Keyword"; + DefinitionKind[DefinitionKind["This"] = 3] = "This"; + DefinitionKind[DefinitionKind["String"] = 4] = "String"; + DefinitionKind[DefinitionKind["TripleSlashReference"] = 5] = "TripleSlashReference"; +})(DefinitionKind || (DefinitionKind = {})); +/** @internal */ +var EntryKind; +(function (EntryKind) { + EntryKind[EntryKind["Span"] = 0] = "Span"; + EntryKind[EntryKind["Node"] = 1] = "Node"; + EntryKind[EntryKind["StringLiteral"] = 2] = "StringLiteral"; + EntryKind[EntryKind["SearchedLocalFoundProperty"] = 3] = "SearchedLocalFoundProperty"; + EntryKind[EntryKind["SearchedPropertyFoundLocal"] = 4] = "SearchedPropertyFoundLocal"; +})(EntryKind || (exports.EntryKind = EntryKind = {})); +//# sourceMappingURL=propertyInitOrderChecker.js.map \ No newline at end of file diff --git a/build/lib/propertyInitOrderChecker.ts b/build/lib/propertyInitOrderChecker.ts new file mode 100644 index 00000000..dc182135 --- /dev/null +++ b/build/lib/propertyInitOrderChecker.ts @@ -0,0 +1,417 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import * as ts from 'typescript'; +import * as path from 'path'; +import * as fs from 'fs'; + +const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); + +// +// ############################################################################################# +// +// A custom typescript checker that ensure constructor properties are NOT used to initialize +// defined properties. This is needed for the times when `useDefineForClassFields` is gone. +// +// see https://github.com/microsoft/vscode/issues/243049, https://github.com/microsoft/vscode/issues/186726, +// https://github.com/microsoft/vscode/pull/241544 +// +// ############################################################################################# +// + +const ignored = new Set([ + 'vs/base/common/arrays.ts', + 'vs/platform/extensionManagement/common/extensionsScannerService.ts', + 'vs/platform/configuration/common/configurations.ts', + 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts', + 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts', + 'vs/editor/common/model/textModelTokens.ts', + 'vs/editor/common/model/tokenizationTextModelPart.ts', + 'vs/editor/common/core/textEdit.ts', + 'vs/workbench/contrib/debug/common/debugStorage.ts', + 'vs/workbench/contrib/debug/common/debugModel.ts', + 'vs/workbench/api/common/extHostCommands.ts', + 'vs/editor/browser/view/viewLayer.ts', + 'vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts', + 'vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts', + 'vs/editor/browser/widget/diffEditor/utils.ts', + 'vs/editor/browser/observableCodeEditor.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts', + 'vs/editor/browser/widget/diffEditor/diffEditorOptions.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts', + 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts', + 'vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts', + 'vs/editor/browser/widget/diffEditor/utils/editorGutter.ts', + 'vs/editor/browser/widget/diffEditor/features/gutterFeature.ts', + 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature.ts', + 'vs/editor/browser/widget/diffEditor/diffEditorWidget.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdapter.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts', + 'vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts', + 'vs/editor/contrib/inlayHints/browser/inlayHintsController.ts', + 'vs/editor/contrib/inlineCompletions/browser/model/changeRecorder.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts', + 'vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts', + 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsAccessibleView.ts', + 'vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts', + 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts', + 'vs/workbench/contrib/files/browser/views/openEditorsView.ts', + 'vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts', + 'vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts', + 'vs/workbench/contrib/chat/browser/chatInputPart.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts', + 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editorGutter.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts', + 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts', + 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts', + 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts', + 'vs/platform/terminal/common/capabilities/commandDetectionCapability.ts', + 'vs/workbench/contrib/testing/common/testExclusions.ts', + 'vs/workbench/contrib/testing/common/testResultStorage.ts', + 'vs/workbench/services/userDataProfile/browser/snippetsResource.ts', + 'vs/platform/quickinput/browser/quickInputController.ts', + 'vs/platform/userDataSync/common/abstractSynchronizer.ts', + 'vs/workbench/services/authentication/browser/authenticationExtensionsService.ts', + 'vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts', + 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', + 'vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts', + 'vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts', + 'vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts', + 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts', + 'vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts', + 'vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts', + 'vs/workbench/contrib/search/common/cacheState.ts', + 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts', + 'vs/workbench/contrib/search/browser/anythingQuickAccess.ts', + 'vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts', + 'vs/workbench/contrib/testing/browser/testResultsView/testResultsOutput.ts', + 'vs/workbench/contrib/testing/common/testExplorerFilterState.ts', + 'vs/workbench/contrib/testing/browser/testResultsView/testResultsTree.ts', + 'vs/workbench/contrib/testing/browser/testingOutputPeek.ts', + 'vs/workbench/contrib/testing/browser/explorerProjections/index.ts', + 'vs/workbench/contrib/testing/browser/testingExplorerFilter.ts', + 'vs/workbench/contrib/testing/browser/testingExplorerView.ts', + 'vs/workbench/contrib/testing/common/testServiceImpl.ts', + 'vs/platform/quickinput/browser/commandsQuickAccess.ts', + 'vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts', + 'vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts', + 'vs/workbench/contrib/debug/browser/debugMemory.ts', + 'vs/workbench/contrib/markers/browser/markersViewActions.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/viewZones.ts', + 'vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts', + 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts', + 'vs/workbench/contrib/output/browser/outputServices.ts', + 'vs/workbench/contrib/terminalContrib/typeAhead/browser/terminalTypeAheadAddon.ts', + 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts', + 'vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts', + 'vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts', + 'vs/workbench/contrib/inlineCompletions/browser/inlineCompletionLanguageStatusBarContribution.ts', + 'vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts', + 'vs/workbench/contrib/welcomeDialog/browser/welcomeWidget.ts', + 'vs/editor/standalone/browser/quickInput/standaloneQuickInputService.ts', + 'vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts', + 'vs/platform/terminal/node/ptyService.ts', + 'vs/workbench/api/common/extHostLanguageFeatures.ts', + 'vs/workbench/api/common/extHostSearch.ts', + 'vs/workbench/contrib/testing/test/common/testStubs.ts' +]); + + +const cancellationToken: ts.CancellationToken = { + isCancellationRequested: () => false, + throwIfCancellationRequested: () => { }, +}; + +const seenFiles = new Set(); +let errorCount = 0; + + + +function createProgram(tsconfigPath: string): ts.Program { + const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + + const configHostParser: ts.ParseConfigHost = { fileExists: fs.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => fs.readFileSync(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, path.resolve(path.dirname(tsconfigPath)), { noEmit: true }); + + const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); + + return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); +} + +const program = createProgram(TS_CONFIG_PATH); + +program.getTypeChecker(); + +for (const file of program.getSourceFiles()) { + if (!file || file.isDeclarationFile) { + continue; + } + + const relativePath = path.relative(path.dirname(TS_CONFIG_PATH), file.fileName).replace(/\\/g, '/'); + if (ignored.has(relativePath)) { + continue; + } + + visit(file); +} + +if (seenFiles.size) { + console.log(); + console.log(`Found ${errorCount} error${errorCount === 1 ? '' : 's'} in ${seenFiles.size} file${seenFiles.size === 1 ? '' : 's'}.`); + process.exit(errorCount); +} + +function visit(node: ts.Node) { + if (ts.isParameter(node) && ts.isParameterPropertyDeclaration(node, node.parent)) { + checkParameterPropertyDeclaration(node); + } + + ts.forEachChild(node, visit); +} + +function checkParameterPropertyDeclaration(param: ts.ParameterPropertyDeclaration) { + const uses = [...collectReferences(param.name, [])]; + if (!uses.length) { + return; + } + + const sourceFile = param.getSourceFile(); + if (!seenFiles.has(sourceFile)) { + if (seenFiles.size) { + console.log(``); + } + console.log(`${formatFileName(param)}:`); + seenFiles.add(sourceFile); + } else { + console.log(``); + } + console.log(` Parameter property '${param.name.getText()}' is used before its declaration.`); + for (const { stack, container } of uses) { + const use = stack[stack.length - 1]; + console.log(` at ${formatLocation(use)}: ${formatMember(container)} -> ${formatStack(stack)}`); + errorCount++; + } +} + +interface InvalidUse { + stack: ts.Node[]; + container: ReferenceContainer; +} + +function* collectReferences(node: ts.Node, stack: ts.Node[], requiresInvocationDepth: number = 0, seen = new Set()): Generator { + for (const use of findAllReferencesInClass(node)) { + const container = findContainer(use); + if (!container || seen.has(container) || ts.isConstructorDeclaration(container)) { + continue; + } + seen.add(container); + + const nextStack = [...stack, use]; + + let nextRequiresInvocationDepth = requiresInvocationDepth; + if (isInvocation(use) && nextRequiresInvocationDepth > 0) { + nextRequiresInvocationDepth--; + } + + if (ts.isPropertyDeclaration(container) && nextRequiresInvocationDepth === 0) { + yield { stack: nextStack, container }; + } + else if (requiresInvocation(container)) { + nextRequiresInvocationDepth++; + } + + yield* collectReferences(container.name ?? container, nextStack, nextRequiresInvocationDepth, seen); + } +} + +function requiresInvocation(definition: ReferenceContainer): boolean { + return ts.isMethodDeclaration(definition) || ts.isFunctionDeclaration(definition) || ts.isFunctionExpression(definition) || ts.isArrowFunction(definition); +} + +function isInvocation(use: ts.Node): boolean { + let location = use; + if (ts.isPropertyAccessExpression(location.parent) && location.parent.name === location) { + location = location.parent; + } + else if (ts.isElementAccessExpression(location.parent) && location.parent.argumentExpression === location) { + location = location.parent; + } + return ts.isCallExpression(location.parent) && location.parent.expression === location + || ts.isTaggedTemplateExpression(location.parent) && location.parent.tag === location; +} + +function formatFileName(node: ts.Node): string { + const sourceFile = node.getSourceFile(); + return path.resolve(sourceFile.fileName); +} + +function formatLocation(node: ts.Node): string { + const sourceFile = node.getSourceFile(); + const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.pos); + return `${formatFileName(sourceFile)}(${line + 1},${character + 1})`; +} + +function formatStack(stack: ts.Node[]): string { + return stack.slice().reverse().map((use) => formatUse(use)).join(' -> '); +} + +function formatMember(container: ReferenceContainer): string { + const name = container.name?.getText(); + if (name) { + const className = findClass(container)?.name?.getText(); + if (className) { + return `${className}.${name}`; + } + return name; + } + return ''; +} + +function formatUse(use: ts.Node): string { + let text = use.getText(); + if (use.parent && ts.isPropertyAccessExpression(use.parent) && use.parent.name === use) { + if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) { + text = `this.${text}`; + } + use = use.parent; + } + else if (use.parent && ts.isElementAccessExpression(use.parent) && use.parent.argumentExpression === use) { + if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) { + text = `this['${text}']`; + } + use = use.parent; + } + if (ts.isCallExpression(use.parent)) { + text = `${text}(...)`; + } + return text; +} + +type ReferenceContainer = + | ts.PropertyDeclaration + | ts.MethodDeclaration + | ts.GetAccessorDeclaration + | ts.SetAccessorDeclaration + | ts.ConstructorDeclaration + | ts.ClassStaticBlockDeclaration + | ts.ArrowFunction + | ts.FunctionExpression + | ts.FunctionDeclaration + | ts.ParameterDeclaration; + +function findContainer(node: ts.Node): ReferenceContainer | undefined { + return ts.findAncestor(node, ancestor => { + switch (ancestor.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.MethodDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.Constructor: + case ts.SyntaxKind.ClassStaticBlockDeclaration: + case ts.SyntaxKind.ArrowFunction: + case ts.SyntaxKind.FunctionExpression: + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.Parameter: + return true; + } + return false; + }) as ReferenceContainer | undefined; +} + +function findClass(node: ts.Node): ts.ClassLikeDeclaration | undefined { + return ts.findAncestor(node, ts.isClassLike); +} + +function* findAllReferencesInClass(node: ts.Node): Generator { + const classDecl = findClass(node); + if (!classDecl) { + return []; + } + for (const ref of findAllReferences(node)) { + for (const entry of ref.references) { + if (entry.kind !== EntryKind.Node || entry.node === node) { + continue; + } + if (findClass(entry.node) === classDecl) { + yield entry.node; + } + } + } +} + +// NOTE: The following uses TypeScript internals and are subject to change from version to version. + +function findAllReferences(node: ts.Node): readonly SymbolAndEntries[] { + const sourceFile = node.getSourceFile(); + const position = node.getStart(); + const name: ts.Node = (ts as any).getTouchingPropertyName(sourceFile, position); + const options = { use: (ts as any).FindAllReferences.FindReferencesUse.References }; + return (ts as any).FindAllReferences.Core.getReferencedSymbolsForNode(position, name, program, [sourceFile], cancellationToken, options) ?? []; +} + +interface SymbolAndEntries { + readonly definition: Definition | undefined; + readonly references: readonly Entry[]; +} + +const enum DefinitionKind { + Symbol, + Label, + Keyword, + This, + String, + TripleSlashReference, +} + +type Definition = + | { readonly type: DefinitionKind.Symbol; readonly symbol: ts.Symbol } + | { readonly type: DefinitionKind.Label; readonly node: ts.Identifier } + | { readonly type: DefinitionKind.Keyword; readonly node: ts.Node } + | { readonly type: DefinitionKind.This; readonly node: ts.Node } + | { readonly type: DefinitionKind.String; readonly node: ts.StringLiteralLike } + | { readonly type: DefinitionKind.TripleSlashReference; readonly reference: ts.FileReference; readonly file: ts.SourceFile }; + +/** @internal */ +export const enum EntryKind { + Span, + Node, + StringLiteral, + SearchedLocalFoundProperty, + SearchedPropertyFoundLocal, +} +type NodeEntryKind = EntryKind.Node | EntryKind.StringLiteral | EntryKind.SearchedLocalFoundProperty | EntryKind.SearchedPropertyFoundLocal; +type Entry = NodeEntry | SpanEntry; +interface ContextWithStartAndEndNode { + start: ts.Node; + end: ts.Node; +} +type ContextNode = ts.Node | ContextWithStartAndEndNode; +interface NodeEntry { + readonly kind: NodeEntryKind; + readonly node: ts.Node; + readonly context?: ContextNode; +} +interface SpanEntry { + readonly kind: EntryKind.Span; + readonly fileName: string; + readonly textSpan: ts.TextSpan; +} diff --git a/build/lib/standalone.js b/build/lib/standalone.js index 0e7a9ecc..fb6c4fcf 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -41,7 +41,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.extractEditor = extractEditor; -exports.createESMSourcesAndResources2 = createESMSourcesAndResources2; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const tss = __importStar(require("./treeshaking")); @@ -75,6 +74,9 @@ function extractEditor(options) { compilerOptions = tsConfig.compilerOptions; } tsConfig.compilerOptions = compilerOptions; + tsConfig.compilerOptions.sourceMap = true; + tsConfig.compilerOptions.module = 'es2022'; + tsConfig.compilerOptions.outDir = options.tsOutDir; compilerOptions.noEmit = false; compilerOptions.noUnusedLocals = false; compilerOptions.preserveConstEnums = false; @@ -142,102 +144,6 @@ function extractEditor(options) { 'vs/loader.js' ].forEach(copyFile); } -function createESMSourcesAndResources2(options) { - const SRC_FOLDER = path_1.default.join(REPO_ROOT, options.srcFolder); - const OUT_FOLDER = path_1.default.join(REPO_ROOT, options.outFolder); - const OUT_RESOURCES_FOLDER = path_1.default.join(REPO_ROOT, options.outResourcesFolder); - const getDestAbsoluteFilePath = (file) => { - const dest = options.renames[file.replace(/\\/g, '/')] || file; - if (dest === 'tsconfig.json') { - return path_1.default.join(OUT_FOLDER, `tsconfig.json`); - } - if (/\.ts$/.test(dest)) { - return path_1.default.join(OUT_FOLDER, dest); - } - return path_1.default.join(OUT_RESOURCES_FOLDER, dest); - }; - const allFiles = walkDirRecursive(SRC_FOLDER); - for (const file of allFiles) { - if (options.ignores.indexOf(file.replace(/\\/g, '/')) >= 0) { - continue; - } - if (file === 'tsconfig.json') { - const tsConfig = JSON.parse(fs_1.default.readFileSync(path_1.default.join(SRC_FOLDER, file)).toString()); - tsConfig.compilerOptions.module = 'es2022'; - tsConfig.compilerOptions.outDir = path_1.default.join(path_1.default.relative(OUT_FOLDER, OUT_RESOURCES_FOLDER), 'vs').replace(/\\/g, '/'); - write(getDestAbsoluteFilePath(file), JSON.stringify(tsConfig, null, '\t')); - continue; - } - if (/\.ts$/.test(file) || /\.d\.ts$/.test(file) || /\.css$/.test(file) || /\.js$/.test(file) || /\.ttf$/.test(file)) { - // Transport the files directly - write(getDestAbsoluteFilePath(file), fs_1.default.readFileSync(path_1.default.join(SRC_FOLDER, file))); - continue; - } - console.log(`UNKNOWN FILE: ${file}`); - } - function walkDirRecursive(dir) { - if (dir.charAt(dir.length - 1) !== '/' || dir.charAt(dir.length - 1) !== '\\') { - dir += '/'; - } - const result = []; - _walkDirRecursive(dir, result, dir.length); - return result; - } - function _walkDirRecursive(dir, result, trimPos) { - const files = fs_1.default.readdirSync(dir); - for (let i = 0; i < files.length; i++) { - const file = path_1.default.join(dir, files[i]); - if (fs_1.default.statSync(file).isDirectory()) { - _walkDirRecursive(file, result, trimPos); - } - else { - result.push(file.substr(trimPos)); - } - } - } - function write(absoluteFilePath, contents) { - if (/(\.ts$)|(\.js$)/.test(absoluteFilePath)) { - contents = toggleComments(contents.toString()); - } - writeFile(absoluteFilePath, contents); - function toggleComments(fileContents) { - const lines = fileContents.split(/\r\n|\r|\n/); - let mode = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (mode === 0) { - if (/\/\/ ESM-comment-begin/.test(line)) { - mode = 1; - continue; - } - if (/\/\/ ESM-uncomment-begin/.test(line)) { - mode = 2; - continue; - } - continue; - } - if (mode === 1) { - if (/\/\/ ESM-comment-end/.test(line)) { - mode = 0; - continue; - } - lines[i] = '// ' + line; - continue; - } - if (mode === 2) { - if (/\/\/ ESM-uncomment-end/.test(line)) { - mode = 0; - continue; - } - lines[i] = line.replace(/^(\s*)\/\/ ?/, function (_, indent) { - return indent; - }); - } - } - return lines.join('\n'); - } - } -} function transportCSS(module, enqueue, write) { if (!/\.css/.test(module)) { return false; diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index b2ae02f1..be7d07f7 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -29,7 +29,7 @@ function writeFile(filePath: string, contents: Buffer | string): void { fs.writeFileSync(filePath, contents); } -export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: string }): void { +export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: string; tsOutDir: string }): void { const ts = require('typescript') as typeof import('typescript'); const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); @@ -41,6 +41,9 @@ export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: str compilerOptions = tsConfig.compilerOptions; } tsConfig.compilerOptions = compilerOptions; + tsConfig.compilerOptions.sourceMap = true; + tsConfig.compilerOptions.module = 'es2022'; + tsConfig.compilerOptions.outDir = options.tsOutDir; compilerOptions.noEmit = false; compilerOptions.noUnusedLocals = false; @@ -119,125 +122,6 @@ export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: str ].forEach(copyFile); } -export interface IOptions2 { - srcFolder: string; - outFolder: string; - outResourcesFolder: string; - ignores: string[]; - renames: { [filename: string]: string }; -} - -export function createESMSourcesAndResources2(options: IOptions2): void { - - const SRC_FOLDER = path.join(REPO_ROOT, options.srcFolder); - const OUT_FOLDER = path.join(REPO_ROOT, options.outFolder); - const OUT_RESOURCES_FOLDER = path.join(REPO_ROOT, options.outResourcesFolder); - - const getDestAbsoluteFilePath = (file: string): string => { - const dest = options.renames[file.replace(/\\/g, '/')] || file; - if (dest === 'tsconfig.json') { - return path.join(OUT_FOLDER, `tsconfig.json`); - } - if (/\.ts$/.test(dest)) { - return path.join(OUT_FOLDER, dest); - } - return path.join(OUT_RESOURCES_FOLDER, dest); - }; - - const allFiles = walkDirRecursive(SRC_FOLDER); - for (const file of allFiles) { - - if (options.ignores.indexOf(file.replace(/\\/g, '/')) >= 0) { - continue; - } - - if (file === 'tsconfig.json') { - const tsConfig = JSON.parse(fs.readFileSync(path.join(SRC_FOLDER, file)).toString()); - tsConfig.compilerOptions.module = 'es2022'; - tsConfig.compilerOptions.outDir = path.join(path.relative(OUT_FOLDER, OUT_RESOURCES_FOLDER), 'vs').replace(/\\/g, '/'); - write(getDestAbsoluteFilePath(file), JSON.stringify(tsConfig, null, '\t')); - continue; - } - - if (/\.ts$/.test(file) || /\.d\.ts$/.test(file) || /\.css$/.test(file) || /\.js$/.test(file) || /\.ttf$/.test(file)) { - // Transport the files directly - write(getDestAbsoluteFilePath(file), fs.readFileSync(path.join(SRC_FOLDER, file))); - continue; - } - - console.log(`UNKNOWN FILE: ${file}`); - } - - - function walkDirRecursive(dir: string): string[] { - if (dir.charAt(dir.length - 1) !== '/' || dir.charAt(dir.length - 1) !== '\\') { - dir += '/'; - } - const result: string[] = []; - _walkDirRecursive(dir, result, dir.length); - return result; - } - - function _walkDirRecursive(dir: string, result: string[], trimPos: number): void { - const files = fs.readdirSync(dir); - for (let i = 0; i < files.length; i++) { - const file = path.join(dir, files[i]); - if (fs.statSync(file).isDirectory()) { - _walkDirRecursive(file, result, trimPos); - } else { - result.push(file.substr(trimPos)); - } - } - } - - function write(absoluteFilePath: string, contents: string | Buffer): void { - if (/(\.ts$)|(\.js$)/.test(absoluteFilePath)) { - contents = toggleComments(contents.toString()); - } - writeFile(absoluteFilePath, contents); - - function toggleComments(fileContents: string): string { - const lines = fileContents.split(/\r\n|\r|\n/); - let mode = 0; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (mode === 0) { - if (/\/\/ ESM-comment-begin/.test(line)) { - mode = 1; - continue; - } - if (/\/\/ ESM-uncomment-begin/.test(line)) { - mode = 2; - continue; - } - continue; - } - - if (mode === 1) { - if (/\/\/ ESM-comment-end/.test(line)) { - mode = 0; - continue; - } - lines[i] = '// ' + line; - continue; - } - - if (mode === 2) { - if (/\/\/ ESM-uncomment-end/.test(line)) { - mode = 0; - continue; - } - lines[i] = line.replace(/^(\s*)\/\/ ?/, function (_, indent) { - return indent; - }); - } - } - - return lines.join('\n'); - } - } -} - function transportCSS(module: string, enqueue: (module: string) => void, write: (path: string, contents: string | Buffer) => void): boolean { if (!/\.css/.test(module)) { diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 9a5b0802..352b5361 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -234,6 +234,8 @@ "--vscode-editorGutter-commentUnresolvedGlyphForeground", "--vscode-editorGutter-deletedBackground", "--vscode-editorGutter-foldingControlForeground", + "--vscode-editorGutter-itemBackground", + "--vscode-editorGutter-itemGlyphForeground", "--vscode-editorGutter-modifiedBackground", "--vscode-editorHint-border", "--vscode-editorHint-foreground", @@ -345,6 +347,7 @@ "--vscode-extensionButton-prominentHoverBackground", "--vscode-extensionButton-separator", "--vscode-extensionIcon-preReleaseForeground", + "--vscode-extensionIcon-privateForeground", "--vscode-extensionIcon-sponsorForeground", "--vscode-extensionIcon-starForeground", "--vscode-extensionIcon-verifiedForeground", @@ -370,14 +373,14 @@ "--vscode-inlineChatInput-placeholderForeground", "--vscode-inlineEdit-gutterIndicator-background", "--vscode-inlineEdit-gutterIndicator-primaryBackground", + "--vscode-inlineEdit-gutterIndicator-primaryBorder", "--vscode-inlineEdit-gutterIndicator-primaryForeground", "--vscode-inlineEdit-gutterIndicator-secondaryBackground", + "--vscode-inlineEdit-gutterIndicator-secondaryBorder", "--vscode-inlineEdit-gutterIndicator-secondaryForeground", "--vscode-inlineEdit-gutterIndicator-successfulBackground", + "--vscode-inlineEdit-gutterIndicator-successfulBorder", "--vscode-inlineEdit-gutterIndicator-successfulForeground", - "--vscode-inlineEdit-indicator-background", - "--vscode-inlineEdit-indicator-border", - "--vscode-inlineEdit-indicator-foreground", "--vscode-inlineEdit-modifiedBackground", "--vscode-inlineEdit-modifiedBorder", "--vscode-inlineEdit-modifiedChangedLineBackground", @@ -386,8 +389,8 @@ "--vscode-inlineEdit-originalBorder", "--vscode-inlineEdit-originalChangedLineBackground", "--vscode-inlineEdit-originalChangedTextBackground", - "--vscode-inlineEdit-tabWillAcceptBorder", - "--vscode-inlineEdit-wordReplacementView-background", + "--vscode-inlineEdit-tabWillAcceptModifiedBorder", + "--vscode-inlineEdit-tabWillAcceptOriginalBorder", "--vscode-input-background", "--vscode-input-border", "--vscode-input-foreground", @@ -783,7 +786,14 @@ "--vscode-terminalStickyScroll-border", "--vscode-terminalStickyScrollHover-background", "--vscode-terminalSymbolIcon-aliasForeground", + "--vscode-terminalSymbolIcon-argumentForeground", + "--vscode-terminalSymbolIcon-fileForeground", "--vscode-terminalSymbolIcon-flagForeground", + "--vscode-terminalSymbolIcon-folderForeground", + "--vscode-terminalSymbolIcon-inlineSuggestionForeground", + "--vscode-terminalSymbolIcon-methodForeground", + "--vscode-terminalSymbolIcon-optionForeground", + "--vscode-terminalSymbolIcon-optionValueForeground", "--vscode-testing-coverCountBadgeBackground", "--vscode-testing-coverCountBadgeForeground", "--vscode-testing-coveredBackground", diff --git a/build/linux/dependencies-generator.js b/build/linux/dependencies-generator.js index 39e2b4e3..448ab38c 100644 --- a/build/linux/dependencies-generator.js +++ b/build/linux/dependencies-generator.js @@ -26,7 +26,7 @@ const product = require("../../product.json"); // The reference dependencies, which one has to update when the new dependencies // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.196:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index 83d2ec78..6c1f7b75 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -25,7 +25,7 @@ import product = require('../../product.json'); // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.196:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/132.0.6834.210:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index 13f32376..f45b6f34 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -46,6 +46,7 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', 'libc.so.6(GLIBC_2.25)(64bit)', + 'libc.so.6(GLIBC_2.27)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', 'libc.so.6(GLIBC_2.3.2)(64bit)', @@ -140,6 +141,7 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)', 'libc.so.6(GLIBC_2.18)', 'libc.so.6(GLIBC_2.25)', + 'libc.so.6(GLIBC_2.27)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', 'libc.so.6(GLIBC_2.6)', @@ -240,6 +242,7 @@ exports.referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.25)(64bit)', + 'libc.so.6(GLIBC_2.27)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 63b47522..d277ca7e 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -45,6 +45,7 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.2.5)(64bit)', 'libc.so.6(GLIBC_2.25)(64bit)', + 'libc.so.6(GLIBC_2.27)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libc.so.6(GLIBC_2.3)(64bit)', 'libc.so.6(GLIBC_2.3.2)(64bit)', @@ -139,6 +140,7 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)', 'libc.so.6(GLIBC_2.18)', 'libc.so.6(GLIBC_2.25)', + 'libc.so.6(GLIBC_2.27)', 'libc.so.6(GLIBC_2.28)', 'libc.so.6(GLIBC_2.4)', 'libc.so.6(GLIBC_2.6)', @@ -239,6 +241,7 @@ export const referenceGeneratedDepsByArch = { 'libc.so.6(GLIBC_2.17)(64bit)', 'libc.so.6(GLIBC_2.18)(64bit)', 'libc.so.6(GLIBC_2.25)(64bit)', + 'libc.so.6(GLIBC_2.27)(64bit)', 'libc.so.6(GLIBC_2.28)(64bit)', 'libcairo.so.2()(64bit)', 'libcurl.so.4()(64bit)', diff --git a/build/monaco/monaco.d.ts.recipe b/build/monaco/monaco.d.ts.recipe index 6192ad5b..cb4c9082 100644 --- a/build/monaco/monaco.d.ts.recipe +++ b/build/monaco/monaco.d.ts.recipe @@ -92,7 +92,7 @@ declare namespace monaco.editor { #includeAll(vs/editor/standalone/browser/standaloneEditor;languages.Token=>Token): #include(vs/editor/standalone/common/standaloneTheme): BuiltinTheme, IStandaloneThemeData, IColors #include(vs/editor/common/languages/supports/tokenization): ITokenThemeRule -#include(vs/editor/standalone/browser/standaloneWebWorker): MonacoWebWorker, IWebWorkerOptions +#include(vs/editor/standalone/browser/standaloneWebWorker): MonacoWebWorker, IInternalWebWorkerOptions #include(vs/editor/standalone/browser/standaloneCodeEditor): IActionDescriptor, IGlobalEditorOptions, IStandaloneEditorConstructionOptions, IStandaloneDiffEditorConstructionOptions, IStandaloneCodeEditor, IStandaloneDiffEditor export interface ICommandHandler { (...args: any[]): void; @@ -145,7 +145,7 @@ declare namespace monaco.languages { declare namespace monaco.worker { #include(vs/editor/common/model/mirrorTextModel): IMirrorTextModel -#includeAll(vs/editor/common/services/editorSimpleWorker;): +#includeAll(vs/editor/common/services/editorWebWorker;): } diff --git a/build/monaco/monaco.usage.recipe b/build/monaco/monaco.usage.recipe index a3369eb2..9e96a685 100644 --- a/build/monaco/monaco.usage.recipe +++ b/build/monaco/monaco.usage.recipe @@ -4,8 +4,7 @@ import { IObservable } from './vs/base/common/observable'; import { ServiceIdentifier } from './vs/platform/instantiation/common/instantiation'; -import { create as create1 } from './vs/base/common/worker/simpleWorker'; -import { create as create2 } from './vs/editor/common/services/editorSimpleWorker'; +import { start } from './vs/editor/editor.worker.start'; import { SyncDescriptor0 } from './vs/platform/instantiation/common/descriptors'; import * as editorAPI from './vs/editor/editor.api'; @@ -13,8 +12,7 @@ import * as editorAPI from './vs/editor/editor.api'; var a: any; var b: any; a = (>b).type; - a = create1; - a = create2; + a = start; // injection madness a = (>b).ctor; diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index c1f22aa5..458847af 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -85,7 +85,7 @@ function setNpmrcConfig(dir, env) { // the correct clang variable. So keep the version check // in preinstall sync with this logic. // Change was first introduced in https://github.com/nodejs/node/commit/6e0a2bb54c5bbeff0e9e33e1a0c683ed980a8a0f - if (dir === 'remote' && process.platform === 'darwin') { + if ((dir === 'remote' || dir === 'build') && process.platform === 'darwin') { env['npm_config_force_process_config'] = 'true'; } else { delete env['npm_config_force_process_config']; diff --git a/build/package-lock.json b/build/package-lock.json index aa939e40..445e842c 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -57,13 +57,13 @@ "source-map": "0.6.1", "ternary-stream": "^3.0.0", "through2": "^4.0.2", - "tree-sitter": "^0.20.5", + "tree-sitter": "^0.22.4", "vscode-universal-bundler": "^0.1.3", "workerpool": "^6.4.0", "yauzl": "^2.10.0" }, "optionalDependencies": { - "tree-sitter-typescript": "^0.20.5", + "tree-sitter-typescript": "^0.23.2", "vscode-gulp-watch": "^5.0.3" } }, @@ -1551,7 +1551,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -1580,7 +1580,8 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -1632,7 +1633,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -1647,6 +1648,7 @@ "url": "https://feross.org/support" } ], + "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -1821,7 +1823,8 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/clone": { "version": "2.1.2", @@ -1991,7 +1994,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "devOptional": true, + "dev": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -2006,7 +2009,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "devOptional": true, + "dev": true, "engines": { "node": ">=10" }, @@ -2018,7 +2021,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "devOptional": true, + "dev": true, + "optional": true, "engines": { "node": ">=4.0.0" } @@ -2075,7 +2079,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "devOptional": true, + "dev": true, + "optional": true, "engines": { "node": ">=8" } @@ -2214,7 +2219,7 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "devOptional": true, + "dev": true, "dependencies": { "once": "^1.4.0" } @@ -2331,7 +2336,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "devOptional": true, + "dev": true, + "optional": true, "engines": { "node": ">=6" } @@ -2471,7 +2477,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/fs-extra": { "version": "8.1.0", @@ -2553,7 +2560,8 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/glob": { "version": "7.2.3", @@ -2867,7 +2875,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -2881,7 +2889,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true }, "node_modules/inflight": { "version": "1.0.6", @@ -2904,7 +2913,8 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -3222,7 +3232,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "devOptional": true, + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -3330,13 +3340,14 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "devOptional": true + "dev": true }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/ms": { "version": "2.1.2", @@ -3350,23 +3361,19 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", - "devOptional": true - }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "devOptional": true + "dev": true, + "optional": true }, "node_modules/node-abi": { "version": "3.30.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.30.0.tgz", "integrity": "sha512-qWO5l3SCqbwQavymOmtTVuCWZE23++S+rxyoHjXqUmPyzRcaoI4lA2gO55/drddGnedAyjA7sk76SfQ5lfUMnw==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "semver": "^7.3.5" }, @@ -3378,7 +3385,8 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -3402,6 +3410,18 @@ "dev": true, "optional": true }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3470,7 +3490,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, + "dev": true, "dependencies": { "wrappy": "1" } @@ -3646,7 +3666,8 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -3700,7 +3721,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "devOptional": true, + "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -3737,7 +3758,8 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -3950,27 +3972,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -3985,6 +3987,28 @@ "url": "https://feross.org/support" } ], + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -4079,7 +4103,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo= sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "devOptional": true, + "dev": true, + "optional": true, "engines": { "node": ">=0.10.0" } @@ -4119,7 +4144,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -4131,7 +4157,8 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -4211,25 +4238,86 @@ } }, "node_modules/tree-sitter": { - "version": "0.20.6", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.20.6.tgz", - "integrity": "sha512-GxJodajVpfgb3UREzzIbtA1hyRnTxVbWVXrbC6sk4xTMH5ERMBJk9HJNq4c8jOJeUaIOmLcwg+t6mez/PDvGqg==", - "devOptional": true, + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", + "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", + "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { - "nan": "^2.18.0", - "prebuild-install": "^7.1.1" + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + } + }, + "node_modules/tree-sitter-javascript": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.23.1.tgz", + "integrity": "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-javascript/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" } }, "node_modules/tree-sitter-typescript": { - "version": "0.20.5", - "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.20.5.tgz", - "integrity": "sha512-RzK/Pc6k4GiXvInIBlo8ZggekP6rODfW2P6KHFCTSUHENsw6ynh+iacFhfkJRa4MT8EIN2WHygFJ7076/+eHKg==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.23.2.tgz", + "integrity": "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA==", "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { - "nan": "^2.18.0", - "tree-sitter": "^0.20.6" + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2", + "tree-sitter-javascript": "^0.23.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-typescript/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/tree-sitter/node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" } }, "node_modules/tslib": { @@ -4251,7 +4339,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "devOptional": true, + "dev": true, + "optional": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -4500,7 +4589,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true + "dev": true }, "node_modules/xml2js": { "version": "0.5.0", @@ -4546,7 +4635,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true + "dev": true }, "node_modules/yauzl": { "version": "2.10.0", diff --git a/build/package.json b/build/package.json index 3a435a83..73d4f42e 100644 --- a/build/package.json +++ b/build/package.json @@ -51,7 +51,7 @@ "source-map": "0.6.1", "ternary-stream": "^3.0.0", "through2": "^4.0.2", - "tree-sitter": "^0.20.5", + "tree-sitter": "^0.22.4", "vscode-universal-bundler": "^0.1.3", "workerpool": "^6.4.0", "yauzl": "^2.10.0" @@ -63,7 +63,7 @@ "npmCheckJs": "../node_modules/.bin/tsc --noEmit" }, "optionalDependencies": { - "tree-sitter-typescript": "^0.20.5", + "tree-sitter-typescript": "^0.23.2", "vscode-gulp-watch": "^5.0.3" } } diff --git a/build/win32/Cargo.lock b/build/win32/Cargo.lock index 4c169ba0..0bbdba1b 100644 --- a/build/win32/Cargo.lock +++ b/build/win32/Cargo.lock @@ -95,7 +95,7 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "inno_updater" -version = "0.12.1" +version = "0.14.2" dependencies = [ "byteorder", "crc", diff --git a/build/win32/Cargo.toml b/build/win32/Cargo.toml index e2130dd2..02f8d0d7 100644 --- a/build/win32/Cargo.toml +++ b/build/win32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "inno_updater" -version = "0.12.1" +version = "0.14.2" authors = ["Microsoft "] build = "build.rs" diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index ef7cb934eb637a39ef4246012216e64dc6d5cc68..e70d5b64ee349919c67c73dfe7c6e333ce7107ad 100644 GIT binary patch literal 455168 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%0DLPq@531B`>mDp}Csi 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

nxbT&mQ*IF>eyvV^Idw8Q2*%!CY!neP@Soup zEH%_-JjNY1bm0Efzrm?$wu7|aLEwM@ynoO8bZo=181VPcTH0rM9;V38I9)>EPM}jqe~zF zSj8~v6h!V~DEZuC@?xq4i}1@huel1psZC6p)zpk5n%jX!IgD7@7JbQeZrFTZE7*eA z=zu`1p52Ak^KAGk1EV08@@yUa_8_ zdW&~ZjRA!wNSGd~GfR6A)p?Y-1m*9yfe9~Ngp3ROR-#Q{DJcl{MfU_$S8qWvB-`Y) ztet3n(LKG1tJ9j0{ydlsKcS7U&M)3@_oW4VzU(#J7GrBmU!y+s;DMhm zyj#uNAL7?g-KU5D536v>Pz_INkjD4i*a#pZ9|EZ*R^zt@9hFE{z;~+u@SsVcjvKdf z90(UUPU63$K@)51%*zf*_(C0058%$g?@Y8i6OdzcGzR9uM+dk@8jS1>FOhnfpR@r~ zC(*w%{1U!t_|JrO{7h~?ip1f9w%Pq_m6M>QT##1PAdd5)*9acEqzLuC|AL|6lEDr~jgs^0yD>SQ)L z8A+fKFKYm>jAvt!m|x%tp%&HdEj~a{M76~-2GP{`>F`hT11($K@Fbe5hu@=qN2(oe z#?<>@zSCFrQ>Q>Lh+gZA(JwHy6;SiVUgNmum$B<{HEo5yYF65D&mVzA}z=!C|K5#i4Ba-M5^b%H*MS)d6HLOz{>{L| z13}`tZNHo*%4b%IPp83F2+w~#@;J(ty{dm*qFZr`9Gd?@?Ne_auMQi@#G(Z!e+_@N-ga($b zM3LxEAQ?Coe_|hpeJ$-c?APKobeG@MBiEu%{QTxm&{9^1qWwpd`&zY&j*loX0c-ki=u5ZZmGjH! zLd5urPPkM%ysK=d&@KYz=gt|&CJH(bVQk4JLPSGYP(&5~+J8RU&5w>ri~~avZAuw8Ql{I>bI*ED8Ns~`=ns9LcCFSlwa1D90A`c+ov!48o$nCjPtBb9Bf2rpJLW~Dv;vy>-&O&1Mi%Ic z0%E%?a#Tfoi=c#flZ~*egIec{I!cu4cqHa ze4XUhveIAWx6!0rHzMC|NEII@IREPl;eBQ>I7^>lM+SBZAD2GXwYpSn1>P`4J8~dJ_-;7_3-mw zK&n_zO!WHf?&*;!0CVh4!Wx}QJ+9*8N%+HgvS#{SDBd_p)}+Ml0Nw`^*>lQ4zFiX_ zA3^T$v3(#DxLyueRip~vj`92>9*$zU-=xaLdU9?qWi-hXdppVsX${NhmLU?5*c14J zaP)15ck98Jn&L?qZ;+4YYY0^x9*d^;vP*FNx zo_XB}1erMj_DM-IeiKL3u0ZzfP9W1GR{)jJ6K^v2eCi`fMzsc<$ziU(U-791A`Rqg zVC&0|Cva3peB&UfivBsK%_?1FJLEk z;W0`k@7J>!KLwd+tNm_)I>z?+|7Gu8z@w_p#qXIg!URTlkf1@M1WgqxRig0{jT#^c zs04!C)Pm5~aw1yGWd`tq4(u6WyBVdeR@>T2Pi>F=pPm*ga*0-w5EDSu0G6v2RH}O% z)BqO3#mxWrt~IwLYJ2+he9!rw@AHw&-h18Gde?hj?~*jZ2ufV8IXRK?Po=&o@cenI zxWDiLYE6`!&F3Zt{`Y4fkC|6oh3bSNBsG&|H7-du1g|BL&kFTO^Z1-FcckJUv@Sdl zlo4#hTMZ{GE~RGN>g-vJ-W!wZn9dqGLCUWZXpBM3Z_&osnVATr4AqOAd>jE&@uuC4 zm<6ni&#K=NwKkdW^*4|A51cE;u3hMM_E>GkADR>X6e;={nVI#RJ8%Jg$Txo9YJaoL z+P&qYk}dvuMeEg+=bI$Rd2+1moW2>3K`cAsbs%8U}GqaUIP zvN>${hKW z2ns^fG~65Ljkw2&t7w69@S?8uEs>fmRjV909L%=nxS0UBJ_iHqc%BCE;B!dZCfOHws%jpux`H5t;8U*~_x{0+XdcWSIM!g>~eZ z?K>YY{%+4pZX)IJY_$R~{kWAP)QAuS$gCBRQ3S^r_NMWNjR2>eq-Teh-2lAIO~FIS z`oLrW(`sU|bL^pI_79{>+)Tgy==K3kC2#m^WFfz>QX_P!RFeu`r0A`#|BO`qx-^ja z3G*(HDY`2KnHnB7{C>d4=iT83BRmkvmr>VnZ-xNoFv?G3kQy+bS)>3{9j3PGSutPH zp`u;cTLPz|BpCR-8=!O${7cq9?>?J}F&6FStpmt)0ZQRN)TSo{TB3*voT~sINu7LBky^Qb> znWQ>)?r&MOhm7!*yn_{elGRu>oDf1r_-ej!ErzHTcc2HePt`lUuiELpM}lkBDeXYN z&I!|-7fOm-ID#Au!bR~&Zkc_9HJrtBP5Ymx0SBf%>uT~e9n z-@(0B%4y^FoWN_Gzl5RTu-O-Qss?&xoYDnya_rlQIAc7#Lq;_9j>=nnVZ`VX@r zo*yuuGi~*QEcyd)WYiCQqyv5u4QKw%5;t$Y^VnV`@riTr?LH&4lg$NkoR2#NfyB{* zcC_d|FU>GkTx{IpP;rlbA0kU0;5m42HjKzOMS(ZVbHwdde&2lgchZ`MdfidKB+-M2 zX9u;HbyH};j0LS-5WR97Wc@qfz;-x4V#)cESF|zu8B=E-KUZi>;1c@gvx99+n=FMWpr*e+hIVYrY zPVSgk-mP=)KpmrJz~3>aKb6z3bN*K< z=K~#cK9I`!fX-X2bSxrYQbpuN7da9LkE)qMQ9DZY$BJw3wL|$}}HK`(-yA+Wx zsUq^Ci-c1}-s)0BzNCuCi!O3=s>s`2ipZB#5qZ%?`lpKQ?@~m*q>9LkE^>e%F?#h6 zs3Jz#5n)^?f?(UAd`%UT7hUW{ibVzK3%Qg?gg8Kws3@EqSjly}I}$lLPLc2-4T}p? z(wl91vbD*y1~I{3%G$vlM4Pg}@Z(Q-tmA=CpF@vSPJ`z0d}FcrJPD5*J3Yyt4mFRR zYlMoD6_sxgf+7CCRJg%Id`xN+dNKY@Pv9^~Z$^){b>+3vLWg+Scjm;p2pvW!8|e@p zub`9AA)-h*mK3z6^NiKw3j>hKaypZvNG37=rDK?OtCx%_lzaJdr$a}_3d1(h$=b{y z8$F#H?;xT0drDl0Q=AXb(Ze+;tG%_!c(y*T{v#Eh?g9E`JiEzT|1J)9b=e!JUQDaG z(j&QYFwa<3pLD8BX*PT*-BxrrL435s>sDMghcJKIWqf9eu8RduoyK-=8e!IFrC5jX zs}vOq|2hm$UU~RwmmR+KBShnbMY}9W4ort2oC{6Q5<45Z%hsepSp_ zx$x9AqtbphzuXO@B}aHf0&#{PL-Qi1`3q!TWl9;hT8+dS$ZZ27`8P`Df0FZ~O=`NM zXS3j#*u3IGXHii{HcQNza`;_Ngr)UsisKVrsxG=y+HPhPU)gq-^`GSQ(e1a!4I^9x zk+zO0k7MUQ{{k<6AJj@&LM76Gs1RbS8KT^Oq)!dPj3 z>h0&<(bK5Z`TR(ha=8@0QRNgG-~r_X;YYt&;d1H;%AE+fMa{x=hHd75g&W07UpZZl zBPjiQh3?=;H**sE{*OF6ze!X(OfOcBisW^}4po^zWY%dpn zgU`g7Qm=Z3q2_PYWR(_js%CLl5v35%@mh-49k=87!vO*lJ9!GbF4q3 zXCugP$fPo*X1ks7x5gulyc^-$nHao|zgI?xCnA`;A`8ljw!TD?%Qo zt|+r3f>eYa>G(_&h|i?Ih3f--BatAfT-ou?M39xrilDUsry^NYl3Yi(4rqz>ru={% zI=$PUWMuY>!WG+Q{?+@yZDWgzXkdZwf=IAUB{<6n-X;%jlIE@VxdR* z22^`&?B~aO%*iKI+6C4Y;}4stU+GU!LMF#!HKi~$_D@}pa;1rx_$p&?#xUr@i+SaC z6%DDPOz_^POb&O4fk7(l1>Vf-kj4LcwEvQ{Kk(h-#NW*J6^ff|B0^Q@4>Gy>W`pQO zJHbZ_u#fcN0AJ@VWCt)|anIj^+}M}+`}$P<<^tuP$`v!6;t|+^LMkY*7o_4#b@CV9 ze6#Y0TcG??0|i~n57FgQ@ufPIUz#Yd{8Ix7|I{=N(DhA-t^`1Ts78$JBW{HmB4bu99|f4+jKJ?|MtI0LX{!)&lVhS5#3J_jD;T#br76_sY0U zKIIxdnThyH*YGI`*YJ#R%E}+VgMawsu`=K3PHX@0Xa67b4}Y8M<^DPUaD5l4OlItc zbpP!|qWrKv7iuCFQTvbTeTQE73>JU|LC5?lYiNDG-74Aj5ljjV6o_~qh=^t$zf}8O;9*;8zYAc<7@~9F z12S4k;)9I()yQ-Uj4phhFb=fx7ljeqI`C`TQbd}=Ai`x9LL&-bT600#&>WXIJz|qq z)JdGba%M1+fiNi6yJKrs^c{xBW=#}KQyuU7nXoOMLKJWkw>9uBp)<}Cud{4ZsnnAQ zix;>#6%sFVf|B?*9Ul*U-J{D{80XtS9&nxrlgD;A==@L@bqkNlp<%4J$9fxK50(#cK)pHMx0R77HF@+?ACM8q z4L*qV^)!`sfVAjq^ozXinw}7Wn%)<>TsK=ljw3TLQCud=;&XGHW51wljUHyToSE}g z&yX{wJIRCUmFs~Dl&WM7f?h;SFKKPEKJWoVpPgU8XKW@?;&&O4MgN3y~VD~HoNg7N+tjk7PUaV`)MvmCOP)HjRA?f-rgSXyJle|R3FvR#~1h*jfg(-BA7<2F>?q!&>id9|i*7 z64TZC{|Ktc!Eh^i#bt3;II0lazM8^*yXhlC??ZP{tIF2ZnUb zS3*9?F^U{2f3q@7+*=;FmTP!r$s*OF@EBWWgcb_>Nu$b&YaZ`z3~PX!2t^?+*m9Hj ze^^ukrbEBG@<;SX%3JNxTC&J^xK3n`nxHSqE=IQN!B=!7*YfObHAn6BxH+x&G-DJ2 za}xrT?8nISLB6mde;~gc_oGbarhJ-r>_n@6u#n`?R(!7cz^kf<*P+6AvT60nKNd4r zCPmiapWmxTB0mU81=bS&%3@(HiuFws5crjPUnTY;|AG#CwSLWv5Tt4AtbjW2O1@%hK; zYV*RU_$&YE%PUGB4HPAh%X2H4+?f1Lf%+ZwpLUCVyTf>zxbqCpDpH9?HBsBR#^wYr z>>6V<&_~A=Js}DV*=xxCdqscrlJlDp5XzPK!;H~aGuz#zzn|KtIZGwwgU(8*w0`VA zXt;A~7yBpftRRn0c2DL)ftYA$(98e|5|#WGeh2bh;u1(0fg#p$)DFeQYMcb7lXkiI z0-npKE1d!BkVwUP+LUk^Q2h{}fexn9q>iishdYaVWV;#Rb|BF?Pn21Ur=*54nH;3{Nem#6Mfk3{FJHrZ+GQuiLOWw zq&uN=z;03bxnTo1`${0tT!WAw7f| zFQ+zZ^wG^L=(;njT&^uu6ZL>_F{QGkAYHVjdBV?+uxFQ9TkIbu)ALD30LZb| zisXM|(VhW^F8WC$kG5w=NmGCrUTPTT&R6vuCZ8Kk(T zpV2Fj4Ya*a+7+$tC6YnfIh&*|{utf)C>fN_Qn_NK z7-HwdG?oi|K=E>vS9vm7%2%VbXm`;ud^N;&JTm8^|8;g|xGOc#T zM`>)em`UlJ+C`i1zcZmVz6C(YZ|>+uB$!HiMwkleE~?3V=N7!CI@j7kO>czcekkRw z@fr`&dj7B*17(2!&)&nQ51$Nby9I|w&>Z!vxJn(`_zbfEQ$< zqZi1Gi=x}Jqf#~-UB}Z2Q-ARe0mp^)%Ne@!3$S^LWebl5Wy$uheSrNq;{s=fwN)-NQXePDUZ^$-UITlL;E-6K;#*Ve^oR~FyPbsbccjfIP(Zf=3zv|IgN+3 zl6i6+o5wHt=$PcYi9MG*LVZ2YNe#5-M#HnwBd^^o>SBqX?_5nuDRsTpHT#sV`BKRP z)^XJ;R_r$7MPvjuC=)Nk2AW-4EL!fihP0Yeg*V7!$OA0|BkSF2XT}e-IEJ$*lY>w{ z<8)`D{wB0%Fp!!P2?cYIHmb*=0EqW}OAAi8b2KG2C~2ou(H|$jrquXf27soP({-)?IjytTqL`)aU<=PPgcjV=P;bh6l}vg%6WXg8XX z>Sq6IE>Ui$Q~HM8|HJtHg%xj+ioZdf|I$m8e=W5RrdIU+N~fRF^ymllJ!0LH9U@Lm zKD#n+M(>)DfKhyIZoFC!@#v_kARG@wGZ~k+sk|`TRWtK@!(7fWPVb0*gLomUIHecf z=)Zb}`$&U3a5;+pz?IZ2*LG$T`T0m?z9;Z?X~JFOo0i!`{R($zW55$T)oLkpo2Zu8 zOEI1Eq?wU3k(wGsr~Op+Ps9J?8vdCJv1imJ?H6lgDr$Q(f%?XF(g_ij$2}JtWR7O$ z#>dJWC9iWAdx%Q+)x7*==~917s(z`a7S%|i{!~SY5q|OdI3T=#cztgD^Z%ap*+<9! zDSTH*NPWR~iS^l+nm?I}jP*(C_u9+@x7coLn?2Sek!t{L?oNo*2AOC?+DGIUzy0xo z$F75`&0y%mH}H64jmZ=-Sb{3 zld|1)juLa1bU1Of!xhp`w|zrdQ9ZX~&*>!{vdhYzbNhYT&R>V-;ieop^$FvZ3U(G4 zl+Xv;!!fK9KQp%;pG{ zIcK671Z|ho;W7p`lEDs9r=*LS)vJnHSq+peMJ-&QHdn+)2o%=VuWv={x@eXvR79?Z+SCC{szf@LAFXd!-vBVvH88O>jsHJYth|Sf? zvGj*eK{!@7sN1Da$=!TUMarMbxSMbEi(0n_1dO&+6X7F)^V2|}0RR+=a{Fv9Iwl`S zwU77-9G;QXQBDjT8|n25Rg((r=cKCy=wy3(A!4er znl`$hkoPKS0tLJ%O{AV^Egeg6smxkfZXjDbUPk+KGCEHs#wuK@M5|2lw)17Y_h%&D zN4aF89Q8{^qP+UWETVEL4Bz?wr0klj82Nwq{v@Av%V*=yDr0MUBDck@y#o#pXdZN8 zp3gAPWYxRTa&i2sx0vn)(yLJ@YOXQ;+6j?Rm&&snE8zq}NR%qXu2(pw8fUJ{= z;#Pc<5qeunGM{P#mo#(J@Ldk%?_!7BD&C&cpQi(s=L;f+>q=}~7f9Wjc@2d5z{Q_& zZ2%?PzPM!h#h8!0fm7{K7ZZixVy|=B<$_)>CZ_wvIrrl_-3DHT<# zf5LRyL)*_Y?uQ-B!D+Eq3N`rq!Lcq(1jjN`_P);XvEIH+*S9QLpQsnR_Um_i1w~Nb zb3A3nlj&^#PSp2ovOdY*wZ1i}`c{5*eWi)|RwV0_{9Wq%qWE2!J#~Z?N^y6ak(a#I z20I(OxrKYryF#&R-(rV|*)2ZP>*UW&%X(5QzNI4vrR5npBb;wukDz9cP#}b+*W>K- zvC1Rzp>c-TxF*?S3Y-HgnBNKZyn>{i@`jY1Qu}jvzP~GFpKM^IGwhSSQB4`Esf?xy zoWH#Q>^+2}*vFnXu=8|8=H-(4yq`gF2`XMEcSU)@&oQ;U-sC?Y-xND=((XZJ-6yIWAk2qw(Rj~%h_ket?YSpo&B9-s4?<+n@831TPRiW z>ycli9Ivn+J~TKU2#634iS$t*iPg>}<784KhK(3ua-8o|N9y9Gj-APU()vHr6YHna z6T9!g(;+n-x=W5l(?~l~@Ai+iM-oQgf2&=c8ynMcqI{27KT#E}RTZ78D#|o|nsr2? zc*I&Lb*Uwx!oVNa?tchA8QnJLS8swMR}N-a0~wN-_bcTp$k9CEfhN~KGD!IAD05ww6E)K z?teG0b8#<6rP``MA(;Fs1%(%9%Y>`v^_qYNk8$M>POfs3hkHHR9{V)a;tp#t>oxq{d}&%JgOkA^g19Wza;Kgr z((EOwhBfMM)tX4v8s}?22KiO2VQLiS>BBc>#+8^E=Yl6mu3CvOC$fz!U{C{?=~B}WR^*fU6yG-vjcB|?m92AJz-~LSQ};7EZ0=5@PFMN)@I*$(weZ- zs(7oKyF-;!&%yY&)vY-#=6rAVoR!oSd(ob-g3ehJUT&&*Px?oTE@%3`OZO8XU^Y%x zXfjgK(p<5o#mmyO=86rHySYMWEGz%J%+-%nJr$Yo93zTUtZA-jlpNL0FW}<= z0n!9coc{CNY5bz~^@;mGJ599Yj;lpPH!)>>dP0^Z?+aCP-CMA{FEQ6n{S$8K5Dii! z=}(l%A6)Rh%TLbbjN0vd2z1Lsr^>}1lB$0vx1L%RFRMFbPGchNs<-6Qq0;ZS$7R8b zfjybQPE{oP7hLe3D{zWRWZj)rPs=>Wt)>(0-jP|b1VtMs#%Fq*sy&)4r8calM{>Pw zbw_&it^W_uqh~N8WYVL(91;27Pmf9}vcwTuQKZ3&+WccQN%9f{!MT-DDw-5z&-~BO zq)Lt*cBM(BiYEOuOD0`CLz5<28;E|DYv)8}dKHBJr})RhL?s|p!)LX`qMPkKlI2EP zk>kNb>|t_dVDKZ~&RudxVV3_CdzdG7+Aj}m(-#qr2Y5%4?m28ccuMtK6|U*VgLTu5 znROF*Xh?5w&TrHY*kT?-(vMJtOLP8gu6LV%#sj$3Xg*a+;-A=hbmy#L>F68Ld=($ZJ^Shsks8eCtR%JN(kxBGF<(xeIh`x>FeK5bH;E~F! zo$uf>3oOzFbt7~ZqQAHhVi58Vf`clfQDL)+6TeppMdH0w-izWmngz~?oyW2k!+Lq0 z@BYxOzlx_tcV%`o9=(boriUTykn^V-^f3C9)M*%Askyn#CqLG!ibp-lK++YIk>EU# zsNiFgbcmBtg?GT*d4N!Q%U{fq0A886-xC?9MlR5H^G{vfR3_ZhNCDrW!Z(!xqB%p! z(YvhJzM;SxDM35(YVLsJtac7vFXWwLI~YOD6Q001%wImNq}+HfM|XVIY1Tv!SHoGA zzSwAEmA~+8122hwBFfb7^VWZmmyKzhAk{r^@BTIR!E6bRA`CqhDG@^aQGCwvc)Msq za*4E}?t@}xXt!FED7sVINOzo-??AzLlPHC=daiM~3S3b!a7ermem~G$iUmx>3&lBxCSaMO|yF<+r zJtNI`iC;!{*Oe0Ap(t?$|d>=RFh<7$r4d@u6yqiOyJ2L5zKsh|Y zv}iKmBHuvrl_e5ykwj)j9Ob8>w8^!NL_FDK&=>PWFT02F!+K-YM)zxYzvLHUN?Nyp zVqEb3qs{zYWQqtdX26@G@w)41v5-!C{G~%8v)m=y1GkR2=A}B)t-Z5*4%pCbvE(IU z>jrE)XWIFNM(8DWt9e34dQtwh#<174{%ZT-Wy7|8f^P6{90pKV1*h2u{7tRBC7S{_ zN+_ZcW%iTOBkD^9dzqE2Q@Qa{C+BSsFDu*j5oQlnAIPmk#1|<|8Dp`B1XU=rwQ|+# zUC9~_NB5(5&CF2dl9(?~qqYzd!>k$MzAS>R*Wy~4Z|+`SSty)}{WLcpDec5e3tgF? zQwNWgD%*n}k~x!`DK-@yvf7K@$==SHoszw0qbFWVuuK=4Yui^yq@wvsmECV6di{zG z;<&u%;j=qQ*diMXWf1v2EbjlE1TTAq@E6TN#bxj$9)%z93xrGq=j29~s!ldnDvZ-y zsjw4ZGLvQY1C#UFU^59V^x_#EU5;?+6~9NR)>2J~m0k2DmphwKV{^`AFw;D*pwkq~ zd1xy=zp?JxO=~#YdK#0G3sWX+r&ryHARbE(IN#(pH99eks7GR=q&tZ|s~#Ro_R;A> z4~zB?R3`98G?f)}BKBM7+<&m}_P&Y2+#?dY8qiPy@m$PeZ=)EDqpC^z@V1}D+KV<-`AdJ< zg)V^s9j{;Ma9^dm-({+OW%7QP(Rp8eU!_fx$@>kI0T7V(e;8Yxu!ncHzf6|w9|3S2 ztT5F*1+G9(^OdI~mbNb=&!sR>|p?~Y1 zsI-n(_H{AU(v~hq+llN$3slR}lr>aTzaqoX72RZSk!^TePI7cTe`Kc?ZEP+p;MoE0jC+xcV>b}3wE7{l!Kr&4aFQhGfs|#b) zckk+%Ho_C3InhO@Gq@Z6C7ggB5!iED?5rdb*53rDCGjw<2tkqezv{jd?H_mop;pk75ZPC!W+!!(-lvrd4ncu?hK$` zz#E((TLRi4x6{~?!ozf85A;@pz&%TDZUT^9O3vl-cF6fUl3Zp>U%)CDp=CG|EC1xh zJUCA(@*BOMXSEP_@t`?}j{HLO7RezYH-1T5PT5aj4VJ2{sknn_^hG+6=ykG#hdOrA zvCdBrdn24peQ*nB(th-FK?WZ~4E~8KuSixY_mHimqUb;%#rYH=HTp1HTcVx48Th*J z&SC^LLOx+3y(-38D!`eYTWAgQSUr=>hBben1w)b8l_LjANH+18vC9SvR7dXh07Q4y z6-WGTm=kv~$9BtLyb3Y_6jQef+fg}$t!i!qTmwY#>^{#?(<}US2Yze^b~yW7Ft=cP zwjp@6vM@3deNuvfAwZgZ>%hZQcHm(u(>zRNf`M~Y{NHB=m$unet5iazJ7SR@G1T9Y~k``(sTL&`lug53L{co9q&f4K~uoT6UjpJ_znne z;KzEpu#q1?Kr+)8UAQ?^IE44dgIIDBqhh&uEaR*E2WHFXB=6mLOAm}_T1&^rGOXS= z$gsGVM%TxF;UPg&A+nn2^rpeL`EIOQTG%S-A%z9_`j&oE;tg=A-@N>QGo+_WmMYhA zW7U`e6q=?JDtAO8Mb`Nm$MgRmtdh6ZFEHqu&I}&;^l@sqJ_C zj2ga@tGKVbIk+8#7}8nZhT%&d|uwTcy+GXSRiqS28tW~APlqm)FquOfUbvh zby5#eQf7jTAE4SGuTl{M3G;{$J(C~#6$7*U&X@O^1LT<$S*cLp#Q4g>m3*X0n8;kz zgaf-9;X{0(yZh-p_h0C#Cyvc5VznFLZ;%bVdv{c3K*`5CzElBt zDr3)ll-x7SX2<;!6^zExk7P{e8LKCLV9$MTx;4pTUCH5jIb<}zX2Q&?;Jx+=N70PF zQ^D0Tx(n1L7%O~&DwF2En5XDS0x(hgK&!!6)mpUCe2?(i zpUX8C(|9o6gg?WEn!<=-% zSF|S+R@yzV#HPDIzF!)LTI-a&1^qi}@K65$AGtA;$J9-%JrBlOBi ziiZkoc<|pkaU4DK#wV4UoK^rpJb{V8(!lCj%GYDCjEwiI_>tcs30NzZ_i+;=Xbr35 zlM0bgD4XjE6gEktu`9vf@GJV|>xuJHz&ndD7&jj*IXvaD9M5i$_*`lSa8SiHaa?>- zS+XNWxSCH9Jv!-=LMzme*(Y{cuCaSP#xbP-F&&Q zg#>n-*hJ{RsG|msTKe}#TTSOw4Kh$8Ec<36?G7HtP`vT#!Rc=Qge0l8;J-1488tsF zH79BX1PoO;}2=$|L~SITNevG1oh0ecV`7!k!1ba-R1H3`TFEPLjp;RRCNU z#`>t;_Ir*k%;@FZ#XdG0y9(H}#vRDtJN zolodkR6{WdqfK^t{-Ll?h)Cx`<9E7+w-kW9MWHLa+^Z zQ)Z5R{1!hcI8IKWGL>JES302faj5X5Ushl0mFf$4&Tz`HXQoDcu|a6azliOz}=Fq_)bGnvf(;N#?6 zhcDrUeO(}0Cz*YJaWr4jL#jhejC9XfwUw4LSni`4(P};~ETFP6CUNCbzBfB0AZ}Ebc@*QrjuMYE^4MZ)HFm!V%8{w4Y)z`z$g9`AVEeyUHDOBVwF_87u4chxLe zh^?te8FLwCPC~=T+~C4?`D=vcL)GmOgCSW1`2d|7p;eFti8sx@ z=Ox}W`<|0{gJ;hfBRu?4WnWMGqcb>sfxP+z9Rd*(d_PYhHKD);*q`T(@h|5mqS21A z>gmF*yc(+?S7?RQXC+lgTSzf-tccvwC@dii;tGDITfLXung#ltnP8(QIyX&`P1`k0 zrjP6?Rd^yy=DP066GXzIh*C^6I?6UbHV+e-(w`+&Tsgv*!!0bQ79P5))ov)TZ$Lm{ zRqCW6dixp2%?yw3M2w2p(I^$%una&9hxqZQVt>{*;7{*P6#U zeygS()S0dm{>)D+Giw@0QFn2bsM#)~Zkb26j2I>gg}U*sNmC!HS-TmY+}ZpvdrdS1 zBEUH3T6CyM00fYb;ee)RQgUWQ!vv4gkeChUt8Q?OB1<1+W+pn{`lecG6{r4h2=R>U z6U;{HIJvQE#1K)T2W5&n?Y>q)5$wKWOyR$7|2dc5hDxCm@d2Hv)lY2YrG>JK8LF(l zwNfu&suQ6?z5hE1g0f+$LT~mif?#VL3)eB&3w8S4F__NIoAL{^oKxZ0onHwo=&4U{ zA7*4ZbDNy~e_#hrbavk=Q_q%@g$O+-$`$$J{1ClsbeF)-<)Y$1f;ba?IF5JEh=j63 zD9lvRzM$~nNz3OnejAd5fqt5ZXTa<^Ob zeljG`p7K3PsuRM`e@f?+Qu$f#U*%GY-8D;U0LtA9Q4t@)79~AuWpovX_gt8yi81bUV<1iC{sy|Fvh!Y)o# z6(|J~&e3Wwm4qajOx-4S(!kjQ5AS>-JiGyR@!4a&_AH-$gM@_ffNJs+y_TtTHlNGF z9)`maw0)I3}EPWbsD#uN1=+tF|{CZ?;E zoXlO0)zYtRjW+#mD5@)&T$ulodV8YI7`8sNk%B&>qQ2B^l+%Jye%!t(&pPDZWRJ_Y zPW0ZUk{8i0SE(C7$}!537u|34-qCG%fl@)U>pr8U+2>lV1KP|s*oT8m^+NsCb;WlZ z%_g_GgH_SW`o*5tWszj6ls++`LQJG;!8{|6(kJdw)7eFzxJmkE-{iH&`7T3X!N>=z zif%*%7ACgjrW}1)4GgXHkSiN4py_^}byH5DC;f?gURQtuC$Bq&I|3||t@(0}Yvxl@ zX3_Xz9d91qWcD$od$CLOdOX1)d3qZW5YvTfQQEUV5Wq?b#en;WE4(7L&qo zigpzpmHo%*6dZ2+kJ%U&-UP;W^{byfz>@RHUh5FOh%H{bbTR(?B8k;B^5iTGQZX~d zA)%oRA#jBnUpeCwA?9)Y+W}Yby@P7P`TYXW!o54W)$3U(wPR4@+LiKj3Qx-lXzJ?- zP39Rc*9!yqM+~Ev&g*cTYP0t5@>#DtWOm)kd5vzq@U**zx#I7dZ6X!H;6>$+y#NC6 zm}nl2aF)D}D6=0ECMa{d%a9VYH=#bBM7-SaVo4615qT`guaor|-f)jCm&c=r+vRA~ zVlTA`75^i92NPjgGkEnkJgc_G5UdshgCu-NkWccDD3i<}L6<#G$t;gjnLX7T>v$jh zX#c%-H}PaYZ1-|rKG`nz*#Mn?w2|yaxPtP;7QTnR78e?eGeKG@eL zNZJ|k9rkH@%O%rKkpf__FVcr?Q^%?})73Td?L3Kmd!aowH`XzpD)$X?3N6)NpX#sq zkcxkcGF34g={PL*yh5reu&1Wt-*zg$8J{Q47e1uo-_9%OT3*M$otlb&+o}B0MEQa0 zXxf*=znv64Dl@O*-(JP}Z>w#KQ?kJb-^bL@y{=JZizO~?b0y3&>v^4=iWAU~)m$xK z4mz*)2O}vD2+9wj=7jG;ceB&^!~GC9KMKH z$2Q5GkKXlDNAJ3Q(OHxQ`GiUSZxldH4G3d+4|`&dP}{jleL;e_bx-S?$HQiS+5XzG34l-76s*h&fxlvUT?S0q5I|dyxR&RyRJzbX= z?`b#d+6nokyKf%vX_zn2qu{0?OU8TRBcrQ$v0dvH@?$kELz(ufb-ex)Pw;(X$zE%x zXHxLZA;A+pjaBf_urJM#`{L&3#**^fcB^&Xme_TvqIx}-G+VDy+BzQm&{)#8Zr}Udx}(gHq1js!*iZGpKnIkVp{vo)zMynX|AlNYx6ElWQrz!Z8R9wR_q( zd*UI4M@RHygO%<UXhr|UKXK+3OGl{jMe%TgF-YeF5?xDUG7z87Dfjoa=PSeONkE+cdS zyP9ik-aR=K&g4|dYJ|547ZGSAu(7vUs&a9rd}x02(0q9olqq~{q-w)hbz;<>>#@6w z8u)BZ{<-{p?fHp_(rK%K-CdtA5d)7%^c5TUQ`Le9r(6lSJGpA7H8OYcwW_Oa=Duz) z_*TWfzzp-llYyM*Sjs`4M7dbWqt#|VH_YJ`& z#I`5Tj$X~Z$OH!59(!2mkFl!aE#Z@J>WSMG8&IwJB_sg+29x6rz;A4J25xST>m85* z{@tqT;_qghK`mDMYgw$_=#}o>bU9y{W4`Kh%I|Z#1~K|uS%g8b_t)@G;YaJQ84+F@ z1zPT$m3jU}`_msN<^~u0z8w2DpHF1JWPR<%n;#Z9zShUq!Z-mbRj_HuKV85|y zq(C;xA8#M?*eO=kJ}dCHv8rWB#bLvIP{5$VxpS+V|01J3vaPEc&AMExVnggSd%=D? z@HVwBdRbuSc*JwAHRZsd-Nb>OcNw2UzvXk$o)kP7tEvuP<;&w9Mg}*$WOv|`=wfoD z(j)V`vHu1&2PQ^tY~#Ey>s__kt~y{(XpH3doW9H+bC%lo3uIi z5u<-O^S)rG5&A2AwX62K+luxKI(AXjfpitij8zj3UzMBZ?>W3=SKv_eC33n?*cJ45 zUu1GOxAo4T?IMd$Vead_^>&qG-HI(pg7t9b*4=t@Jg}kK*>|@xh=_IncPgbt;b%D! zhAk&RPODvJ^&PX_8}a1L$t&6GA8!%1TKB$(zkeCAOAp0}bc!6@Fwt4{3q@B&Wignz zipT9K2Uw`c_NR)mF`#@hin=bkjcK_3W#Ms6<`mRJ?75)$sznrFzCj(-;TY)M61q@p!a^;uKJ zF<0$YEUz`Kfnl!d86M{~_%t|X_SG>)(`Gds+3MXybUMGsyCF8vuK4Uo8A0iM64NcK z=2YQpddjoA#sceA)QlU4nf zI)6euGV2XR&2F`?6RiIf115Z8*|mQYz^?SodC^n^Ci4ma6o) zqYp}U+KP^m2jRAOfpaU=jhFBh^L`Jv=$8mIw!yQm^en@iLVL!lW6|k?7bt-GBM1M* z8D2Vs9T>v>CmN9RkiF?k2(z8&A;i062)~JbMbF?_N=vN4LU8g$bW3;gBT}N$w(8A; zInl+El^daT_HAUAAWSlzACVH;<3CrA`OMnKr!wX&XAB|{dMC*-OUkMlIkVOvR3VIO zQ95-TsZ_TP$v`$$Ij_-Q@rRR1Mo2gU6qR{2ou3$E6Jx}xEn~b!QYVT<2-P8fIdd7_ z3fDlGH2!jkh|I7^3{UpReDhtjHpA7kK<&U; z8Bi-_pQEhAW|f;L`ZuP=_XAy}2K7N53%x*Wii4y_an5v!lv%Wq3zlHYksEz;dIn2- zOx1@ON_aQqf9B$G5j9P!1stLla3J@m(GoQj_rk5m2uYMV1xiNfXFMi> z?Y-+$iy>MznV}clKt`DO{}Sn#9u9QA2jI~6(6{*z&E}76k6gVy!tXLLwrtQ4^8+OU<9_zK}8!{#VXCqY4W3msor~44(vkX7{o=^y#^~aL+*HXFL zxlCt`KFqguOjW$BsWFp zeR!Q3x@_CPm8{Ue`DB#-D_(Gek=3S4jp^l#tiBWqg5u*ImGFIG{YtA)wW)%AYG0QA zdK^<1q=nx~znnSPd3br6Y%jQ(wLZ0{>1};mD&y}MzyH_q(Z1MnU0HtZe@SKCoGX%f zjPNZCnN%V(ZZzY={pXqSae=F;@Alf&y5XMbyo1Rbs~~Y`?Fvck;e32;YS9fz#K{;3 zxEz1}STT!MJCPqty27V2dok7`<`K{kaH3g(sKCF;GX7fD<|V1s?L3*Tb*-$PmVxC; z#auMe>|lxGIImhaG2=PagjBy&GHbZg3oe(h*9zf_zRwdKA6EP9{i=!LbQ2SkO;iJ` z{sIN3udk2oxV>b&zoP6uX8zhm%QJie1gAKA)yCz)S{}DD{d+rBP3+mu4uI;yls#4 z5OhiywEc4~gj)%gUNtpvE@_c5Ztk6faq*5RX*YL+b}pjE=s{I-KL~cgK4RdFXjYjj z()V3oLq(^mio77rUy{9~3Xbm+Rn7rrf&vpL^MVx0>V&&p;mKJ7aPt|93N8gQB0agg zCtM>yF(kTF^{aI#trD-EVGqS^$ZQ;}gcu4P zqByMZ+hAT1w&@)vnUOy8JvS0z6L?oi-W401rGz3ULs}Z|PU2{9?1dHX*e?Nqq`o27 zzRzP1^Om&D`G$RIu(bOeD{vriUSy`bq}`hBopUK1ZLsvRsewW8FvtkQyxbM>AsuTT z3a*kL5VQE%EP*l1RlNiPtO@(MePt_Zij=%zSL`ck0s^Y2t_TQNNU4%m|My|xS^OcX z#s6Ar@lR#(`=-h#*MH8ndhOS0V)?Enmi0vDUsiFLh2I2Ut=9f9R5FlbH5;p5bHAz9 zUCYx>(@?i-g*(cnK8OZDY^7RW=8>>)&2A(IGFxo}S}<7pxA8?@@AX<^eNj$&#x*ya zokJ zFzg~vOKG;T_?x^}hU$t{b$vW=T9=gs5hq#fLF&!s9qYziIn zOSW~!qwi4+N3kawu9@Pb68e#_k%Zua1KgvZEoYJV&|FjDCD zcJ5~l{|;hbBRVOLc{oqst|UqB$$@ZHtu#W#!fzX#9JK;#(ED%}%&KTKR=Hrrgm+(3 zalonqIlRi7Jz;}gQCAd?__9E~iSw1lsa_1Ujw zA6|GuSTg^3v%wP^Q2#-9vmqpX3Z0MGK5D2bnO|%>GN|PDlJNVan z`cEoogbP8fz~;z2H|np* z<9}LC6;|M_S4NV8(xLV&e%%|G1*>AknnGUG>9Rqs97VTwAyu0#=!(uVhBv^{!p+X^ zW9;T{%Mhr7x#7RjPPYpmCVT7ib%K6=ny0a~aN(tMFh z+<&>4Q%-T=2YhY(C<4jiK73!eV82Y65$ev1$!X@rp97gtwFw6XZNUkX7xP$;xi{MZ zhtOMF&3$cXGKnJ;c#E0%Lq&_bbx?y{v6IsloTNSgem-gR_M!=GHS6}74Q+uaXwaT; z0F!)3km#W16n?Wqe|klGN|j%Iv(P<}L6xiladd^eMeY%dA~% z$N+4$UR3qAF>J#cnOcrLm|OVE3~sn)!U`iK;g1ys!CoC~cqVxKtl6i_Q%mqTQ`(zU z&qs2M;t*r{?g1kxB7nPMpRwo&59lfv@0B(x4yfKR@#gx%L7TunAn}Fw0TqhBR`_b+ z@gP|ZQ_lDTSzy|0s#aK`Cj^L$knjuyzUHp&qk)%Mp{An@s@{EY;js;*%)S&#^Ttbm z9(vAOsrj)sn9YSp-%q;uoU-uPBq)G&aNY;8?#f>E2CYOqf*&z!Ur@9)UU4AesWVpf zn|}0&S+Us@uWE@`yk%DG^eCtUSXpB9{B-SryWL(ukTxeepWdN^VPpbF<{EjeSYywG zeau7mJtMm&=Ud6B#ENUbf}IS)>0gb0DH*IdFaY^;XdsynW;-VySQ!sACNucYw{|XU z6d*+)tjvqg^Te;zTAYQA65&db(S+uOGO=hR@VkhDi}ZrqJ*LSt zVKz4}a=+HPJb>!tLGO~>LIpbg=G_ONtXp^D;c#pdnJtI=zhQf#ps9q8p!sEht zSrdK6Vw=y#DlG0z>&*5(#^U=)Kr2+URB44Ie?p=X)d0f6jC2!$`w+f9f;Crb`aLeQ z7|Qhw2{~#LgaMt%H99+!TxA_pYD4EK#q0;JO$<$fA=HWrA$lc?m~1VfMmjk}ja4;} zmfzf&@|zlyYV;emIa(%b$|$v6o3tNNbu)OcvU`um&k2K$c-Z{=wy z-an(gBH%l>Uy>B>L+wn9cpoa=5)^43IYo|ooZqBw3B9A9VJ!`b;<$G5GA;k4dt`*m zgb<9*iPn%ode1C=HW-rSO_XP6AGB`wI`J5&;cz08o|t~Mn`K@-&ij|7CdLSV${J9~ zB}0=B!$AIa=3bl}XE;y#CfX-)q-)FG zV1ynZujMP$?VnlOBJFo`zN6b0$}mf{ZXG8Uw=;^f)dHK>OS389=~sKIUYKuW_PRv( zI-5yGq^6>Qd@zr@1Lvu>j8GPp)XGI4fZgGX)p(^nw+v2C@jigJRxbD;#}=JKq5s6J zUWbVy`bY8Lu`l_#ZKql~0`?14mGTTq82_fa_2T1aBKM;{f8#sz+C)_iMA zcD?^H>mc!-;365U-feOSIypzf!D962`cy69DpKjf&G_OdQ0-S+r`Pdpok!PkA-Pz` zCsBUr4`=W}DV2KhfMy}ujbG&I0f~;{dF^p_ej_4~=Rw(M;N zd>!C5ntuZB^7}X&Pkz7Z-)W@FKXIIve^O6B$|d=En}ze=2v(dl(R!mOzRVu&y=Js$ zPItCbh5tJ9Q}^t%^nm-+juQNl<$OKa+rSwN6|hI}5A>2v8fb_i!>w1YiMLOkvVJJr zu&7R+lC5RP4|Z>nzqk6!L@?w^i860V>zvDsRlY3q*iHTt^Vq1s*Mzjqcb6PR4w-W` ztnk!8F`q;tJ7Nu`>S!CS%NNbxM*E}V;kAtYN1P{-&6W3r1=lj+fJo-kmm%kryeSen z^4L|#W4|C%$ua*lEstH5lE?C`dd+(&{iRRd*1?I*xiUix8*wxxh9QEZv2<_Jx=Q%E zdkL&6!uMMNA3j?LM`&SfzSqA>%i>lG z1GNrSJ3s3oFq{)hT;ZHS->$NY6f62oIquWIIEAiB2;HGx&;j?&nVb2@1oJ%~de>dV zJ!l;vjF*fr>@i|tT+x_&p)txed)27I9&ntId)I-1^UU~F#$wU`cgz3NRZl=`-cR7; z{5h8dOHaEeFv#{5iUB}E$!N=h@CNLv=+`qa-(G4UqwGy*xi^XaHp*8|Ar^Q-S@9en z>5IM~-=rp-(s@i0)1T?uqRO0-Xegmx2xc}kS65lICq2i}Zzrovn>|KlmKoNij5~U< zd@C;0xYzj;q8oizO51I0D?S(CW18#Sfx`l*&*dC!=il>>zle*UmD0SWdLZi2W!s&i~2|UQ0b9nLvOV9Xjpic_koeNJR z+oh82D|-Cig&95GNh=vknP7v~WoCJiEU3DB+)Q}?1XwV(1QmKBGp8+;`ShYYF=$b= zyL|bIzD&$4KB{A15|sb4%q)X+3(j>p$+?p&MuhZ#E!npOjZRSizg?gPE7bpEkWMm_ ze{Pcazoj$8zNM3*#Sp!(>hZHWzoxR{xRt81Z<1wOn9dToUsi7!fG7n5-?Q4JCqHFs zlEsYBQr?vAn>|#ii$?F^TiW8{zaDTJpI{ban*B{%Urx_xF3st7uvAbyW!QNCYidTm zz*rM|Lt}$*KU$Bi7&aTqb=@V7Z^_iWpERUkX6N6_R|J?6oez38zFa z5~eT-G-oE6xoN4A+NzMAa0&g}oQuC>tmk=DkeBC>$v-% zv3gg1EbGoa%4ssz`>qpr3_7+>93@MRz9Iv_d)8iXpdVheE0xP6D$2Ast-o&z-n4zU zMs7Sg`^Ts_W2fk*8^o9LLSuDH{YP1MZdFx()cdYOcc4I9U1zKgxwP|T`KepY`gU=n z?5W%+Pu+GyEQjQT=dDhDMFtZoKlLp|BL}@~=Y?6b`;IGOs zA-#bG2^L7@B%0jN?L^160Z#`$Hs`3qq~#(tcd3<@dBF$Zu3}s5lHjmNQz4u zVLTgKf+u?#KWq%HenQuizx;k$w049$Wl;uBb-n#Uw-Ri+C? zuajhHb;rD?`1K6h3vD0h-feB0 zc#K8INY|_AFZ6Og5b|k>ZGU|Nh;FWS=9eac$i2&0eXRb|tUKdskw5Ky*U>u$9ehRL z2f!(LM}g4Ue&%@f%Ijs(k$gQF@Kc%t{FI&^86)sRd(oiko&S5NGQ@`&aps!^YG#BU z>>*JO5l(U{(;vlvsP3&SVKh0OXT+05w6&Nj)@mIP=Zv1?gPj`|#gBC`UK|$HkK@*r zU@1;srn_Db{HWQcJj4GU`1(!qWX4(GOrtojd? zMeA5;tM%sWDp*zX{T_U)fB$suVeV$udoX`xw`H%F8qbZD*1zA~tk23m!Ry>h(se8Q zgZWwks)O~eAzYhKr)rGsKBdgT`%6%?+ZyXhI^C?z7xnt1BRZ+qQ~VD0K_eu%-X5BV&*&R7 zE{xCOKH(f#4PkkW@cX1irZ{H&yud}`)Ont=1V}pUN^f*Z{{2z$2kbOH?QUg+eoqn? zPaJn%Bce`G-PIM&tnn`d#m(RVfxD6ND_Y4Rep$Gjj?Sx=+Q&-mu>W^jJCE#~`Da{0 z4_#exc+Mze)rR)EB^8ID#-}>5EtxHX=J8QpHRaG%>x~iXja6-U#-b}JKSwGZo>{B^ zud&|vu&B;Q2~QX&245Bz&%&A|^f(Oi!sH-xV=>K9B=PS+x#dpgYyrKN+wMePm3*)_y^AMrdWt(%|gIScuz)dcZc=|Hk9J< zc|fY3vTrn8L$IzCmDe&`^oMZtd8MQXp7Z4 zXr~0?{26)NXt*K^Hj6!Q3V&NLb2eAN|A|5LZJ6s03s@$nJF_t#-~rUzHT%%DZhp9U zkHO9tAF;Xi2!RC7L8c%ja0j--2*&}wxOPX>L%N!Z4uyxrhTsyT*`Sorbx)qc!b<4M zl+hFiJy2no$dt{Iz<$*~Bea43n{5{c&X%e#O!Z4B>SH8Df5{&>h6bksZd{;n|Ev)j z3Y`QL%A6}G+;5Ko3K>}v3TskO_{r53pU&aVTLp!m2o&y*Z2|B;1@Jye0A6d!JASU! zC_hK)0r1il0eI)oWBN^~g8xD!QnO)%OMpQYJL7q`y6G|(hRSOd6YJo(-mr# zRhHPHB>q8yHDi+c=Q12B&a%e{C!10~|K}NM38}dVheCFNzK+J}rkm?y z_oze@y(Z=@IwEb3@rtR*I;2_^!OazGP4e>co7{gxqzi01BV+)yswa~?xHUkmaNZGO zRbhn0ZSXGLHHVQ8n zen;1!q@L%UldBrTbETK)PDgmvo)iCDIY!aHjQ#7Byd(N+x- ze4$TKw)hk&`miMBPj-@$8UAD`2i)d_`>p#pF8xZMElxJ!*U6sgbvsAEOKk6IGW2Ru1I0w`5xi9LZYaQIqwX2A4wj%Hc8Xco!h2he@?y*$?YKnXA6D^lr2O=DT#KOZ5r6lrlz#+n3T|R-a96`T;LEOf*(%~Bi@VJl$_asq z2>&H>^UY1d6E!?zHYlE`cbdQxAHM;9Xsto-UgDptWJ|yG#v<9DgiMPwd;Mkh_9SO- zedbXw&2KT8drIm>B6RiJh1a05=>M|!F7Q!R*W&+7GQb1|&Y;o88YSx3l8QD_R3fnk zNCNUQJQA=1Qj0XGtq3!K6>wlCnB!p-y|mT-Y^AO3wLWgqS_FKM377zKd58)qRZ!G3 z4r&m~Q=_69FIf{(jugUt2Qg?6co{t+m%)uPr8H#*kGFZ@F-V3^irBH`ZtO za~?FXgQ^>%I=MRbH$c!Bve7^alPiR0$YWRnSvHh#kg$*KB3C=(*Jw*_al)y+*Dds} z@QXcIQEkbvEU{^N>x!I@OIxONR~+Jd9infwk6i7FzIT$aiW_qd#7~2Pj9i_Sv!8X- z55&FE_dJoSvvUriII7-FTFK_X`D&N*7lu}iSD>Bd_|-$WyUD$A7%Y{izg*7MO}!mv z7dG3-kX@1L$C@h^KFgQp3emgp^N>t{9(X{nc&fQ#k*qd8PLUzJY+U?!U!+086$Jm* z*Rgy%#V}2Z3|ZJ*v7_vXGKabKverE)Nsse{|8R^B9s2(Cle1mF>h%~JrX3TTonwkr zEP~LXF@cWqIwmF==Nw81%+EU@?XDLuCG(u0;X8Opc4{&A*AGNL%5AE6-vN+OKwI)) z!3?^wCA8WZ{m835w>tEm7TS}Kt9Jd^cBd`s$XZiaoO?HkWsPcTAuO2J<%U=^-sb-^__nl4p zRd(tazgPzJi}-it=lk)K=JJvCVp^(j|o?C(_G zWUj;USt9b};@zJn z~uaCClOe?wms(IlDNfpG4{rNj-CEmcv3UeM?it z8?wSEx%G`KIIEjlB<;BtRywpVp9PA3m=&o!Qa_dhtPKarW!{mNVlDH|RLZfI`K|g{ zZ`W7vYN~igmigk>WSKAC&N5f;l4agLQ6DDD?5yw8v8hj|jf{-Uy9_Pf|6waqI=Z^>;6d^Co5mgi1a+w29M73EJ;=<~fsf3kOzm(_HR8Ah^z6Y0wC5A9_zomzgv0whf zu0S_L0J+P&XWTR*kC2Z zC!UuTyNteG&6^dG`)qfM`DVq->~eU&*5g5bFjBFdYpv93;)caexo~lBYMg~qPgd<x7!y~S9eZ^K8v>L=NhLbjw zaa1FdEFRuCgwkX)BTNL=S*s8mcZ~GeppXTj0-x2&A2Tgr#2vV(aMYJ2x7OQ^5Yjha z-}10GQ2KB|;8t}-;RJ2&db+^Tt9V}|SC4o$MPr^DH%;iiQQ~2w6bW?!`k}9mJ*x6L zpP$2T{VKKp9{ld;usAKxZ_>X8fRY4i{ zsFc?C5M6=L3Y#SsM{8&W!g0J-s!{lXi>0tcSJ?QBw2fF;oqQtU&LYt*+^ZiPk&Lhq zzv6k>97;P*?S#xYe6eJR=O)J@o@s1LW?~Em~M75AooQQ9Usz2}|cN>oKk>LeGVh!4O#GQml3(gWU=4774AQd85D2 z2<(8z*JlCbCMRos?8d?-J+Rsc>~bP+TqWWK

FGhHUij1-L}|C3MVDF*2`MZ1nfQ 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

MNZ)38XeUjCbt51|mSCKHZ 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 M`Tzg@U*Eug0=z;>82|tP diff --git a/cgmanifest.json b/cgmanifest.json index 24c00b37..eb5b37d3 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "46a5bd1e987735e0cdc41cf48a7db4988ae73d16" + "commitHash": "5c0cb964bca15fcf41718d54f4b8d70d6b9079de" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "132.0.6834.196" + "version": "132.0.6834.210" }, { "component": { @@ -516,11 +516,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "53a57efd83a18efe7a84bf1b460acc789139939b" + "commitHash": "4819c99baa28bf2c1baf411ba100c467fec3d486" } }, "isOnlyProductionDependency": true, - "version": "20.18.2" + "version": "20.18.3" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "ddc7afd3f006dab49a3b6bf73bf3222b017556d5" + "commitHash": "f98501308a973e0aee2414315b426e5de2c03a60" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "34.2.0" + "version": "34.3.2" }, { "component": { diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index cc255c04..3d6131c6 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -2949,7 +2949,7 @@ getrandom 0.1.16 - MIT OR Apache-2.0 getrandom 0.2.15 - MIT OR Apache-2.0 https://github.com/rust-random/getrandom -Copyright (c) 2018-2024 The rust-random Project Developers +Copyright (c) 2018-2025 The rust-random Project Developers Copyright (c) 2014 The Rust Project Developers Permission is hereby granted, free of charge, to any @@ -5513,7 +5513,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- opentelemetry 0.19.0 - Apache-2.0 -https://github.com/open-telemetry/opentelemetry-rust +https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry Apache License Version 2.0, January 2004 @@ -5929,7 +5929,7 @@ Apache License --------------------------------------------------------- opentelemetry_sdk 0.19.0 - Apache-2.0 -https://github.com/open-telemetry/opentelemetry-rust +https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-sdk Apache License Version 2.0, January 2004 @@ -8584,7 +8584,6 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg [deps-image]: https://deps.rs/repo/github/RustCrypto/hashes/status.svg [deps-link]: https://deps.rs/repo/github/RustCrypto/hashes -[msrv-1.85]: https://img.shields.io/badge/rustc-1.85.0+-blue.svg [//]: # (crates) @@ -8596,6 +8595,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [`groestl`]: ./groestl [`jh`]: ./jh [`k12`]: ./k12 +[`kupyna`]: ./kupyna [`md2`]: ./md2 [`md4`]: ./md4 [`md5`]: ./md5 @@ -8638,6 +8638,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [Grøstl]: https://en.wikipedia.org/wiki/Grøstl [JH]: https://www3.ntu.edu.sg/home/wuhj/research/jh [KangarooTwelve]: https://keccak.team/kangarootwelve.html +[Kupyna]: https://eprint.iacr.org/2015/885.pdf [MD2]: https://en.wikipedia.org/wiki/MD2_(cryptography) [MD4]: https://en.wikipedia.org/wiki/MD4 [MD5]: https://en.wikipedia.org/wiki/MD5 @@ -8677,7 +8678,6 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg [deps-image]: https://deps.rs/repo/github/RustCrypto/hashes/status.svg [deps-link]: https://deps.rs/repo/github/RustCrypto/hashes -[msrv-1.85]: https://img.shields.io/badge/rustc-1.85.0+-blue.svg [//]: # (crates) @@ -8689,6 +8689,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [`groestl`]: ./groestl [`jh`]: ./jh [`k12`]: ./k12 +[`kupyna`]: ./kupyna [`md2`]: ./md2 [`md4`]: ./md4 [`md5`]: ./md5 @@ -8731,6 +8732,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [Grøstl]: https://en.wikipedia.org/wiki/Grøstl [JH]: https://www3.ntu.edu.sg/home/wuhj/research/jh [KangarooTwelve]: https://keccak.team/kangarootwelve.html +[Kupyna]: https://eprint.iacr.org/2015/885.pdf [MD2]: https://en.wikipedia.org/wiki/MD2_(cryptography) [MD4]: https://en.wikipedia.org/wiki/MD4 [MD5]: https://en.wikipedia.org/wiki/MD5 @@ -9395,7 +9397,7 @@ DEALINGS IN THE SOFTWARE. tar 0.4.40 - MIT/Apache-2.0 https://github.com/alexcrichton/tar-rs -Copyright (c) 2014 Alex Crichton +Copyright (c) The tar-rs Project Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -9519,7 +9521,7 @@ DEALINGS IN THE SOFTWARE. time 0.3.36 - MIT OR Apache-2.0 https://github.com/time-rs/time -Copyright (c) 2024 Jacob Pratt et al. +Copyright (c) Jacob Pratt et al. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9545,7 +9547,7 @@ SOFTWARE. time-core 0.1.2 - MIT OR Apache-2.0 https://github.com/time-rs/time -Copyright (c) 2024 Jacob Pratt et al. +Copyright (c) Jacob Pratt et al. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 9116e483..d4d62a8b 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -665,12 +665,12 @@ impl Auth { .into(); } - return StatusError { + StatusError { body: String::from_utf8_lossy(&body).to_string(), status_code, url: url.to_string(), } - .into(); + .into() } /// Implements the device code flow, returning the credentials upon success. async fn do_device_code_flow(&self) -> Result { diff --git a/cli/src/log.rs b/cli/src/log.rs index 538827ed..f58f49b2 100644 --- a/cli/src/log.rs +++ b/cli/src/log.rs @@ -282,7 +282,7 @@ pub struct DownloadLogger<'a> { logger: &'a Logger, } -impl<'a> crate::util::io::ReportCopyProgress for DownloadLogger<'a> { +impl crate::util::io::ReportCopyProgress for DownloadLogger<'_> { fn report_progress(&mut self, bytes_so_far: u64, total_bytes: u64) { if total_bytes > 0 { self.logger.emit( diff --git a/cli/src/tunnels/singleton_server.rs b/cli/src/tunnels/singleton_server.rs index 7fbd00d0..f183f935 100644 --- a/cli/src/tunnels/singleton_server.rs +++ b/cli/src/tunnels/singleton_server.rs @@ -142,7 +142,7 @@ pub fn make_singleton_server( } } -pub async fn start_singleton_server<'a>( +pub async fn start_singleton_server( args: SingletonServerArgs<'_>, ) -> Result { let shutdown_rx = ShutdownRequest::create_rx([ diff --git a/cli/src/util/io.rs b/cli/src/util/io.rs index c5816ae5..4b8118a2 100644 --- a/cli/src/util/io.rs +++ b/cli/src/util/io.rs @@ -235,6 +235,7 @@ mod tests { .write(true) .read(true) .create(true) + .truncate(true) .open(&file_path) .unwrap(); @@ -271,6 +272,7 @@ mod tests { .write(true) .read(true) .create(true) + .truncate(true) .open(&file_path) .unwrap(); @@ -305,6 +307,7 @@ mod tests { .write(true) .read(true) .create(true) + .truncate(true) .open(&file_path) .unwrap(); let mut rng = rand::thread_rng(); diff --git a/cli/src/util/prereqs.rs b/cli/src/util/prereqs.rs index f8eff34e..44c85977 100644 --- a/cli/src/util/prereqs.rs +++ b/cli/src/util/prereqs.rs @@ -20,19 +20,16 @@ lazy_static! { static ref LIBSTD_CXX_VERSION_RE: BinRegex = BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap(); static ref MIN_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 28, 0); - static ref MIN_LEGACY_LDD_VERSION: SimpleSemver = SimpleSemver::new(2, 17, 0); } #[cfg(target_arch = "arm")] lazy_static! { static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 26); - static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 22); } #[cfg(not(target_arch = "arm"))] lazy_static! { static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 25); - static ref MIN_LEGACY_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 19); } const NIXOS_TEST_PATH: &str = "/etc/NIXOS"; @@ -74,12 +71,11 @@ impl PreReqChecker { } else { println!("!!! WARNING: Skipping server pre-requisite check !!!"); println!("!!! Server stability is not guaranteed. Proceed at your own risk. !!!"); - // Use the legacy server for #210029 (Ok(true), Ok(true)) }; match (&gnu_a, &gnu_b, is_nixos) { - (Ok(false), Ok(false), _) | (_, _, true) => { + (Ok(true), Ok(true), _) | (_, _, true) => { return Ok(if cfg!(target_arch = "x86_64") { Platform::LinuxX64 } else if cfg!(target_arch = "arm") { @@ -88,15 +84,6 @@ impl PreReqChecker { Platform::LinuxARM64 }); } - (Ok(_), Ok(_), _) => { - return Ok(if cfg!(target_arch = "x86_64") { - Platform::LinuxX64Legacy - } else if cfg!(target_arch = "arm") { - Platform::LinuxARM32Legacy - } else { - Platform::LinuxARM64Legacy - }); - } _ => {} }; @@ -149,7 +136,7 @@ async fn check_musl_interpreter() -> Result<(), String> { Ok(()) } -/// Checks the glibc version, returns "true" if the legacy server is required. +/// Checks the glibc version, returns "true" if the default server is required. #[cfg(target_os = "linux")] async fn check_glibc_version() -> Result { #[cfg(target_env = "gnu")] @@ -169,8 +156,6 @@ async fn check_glibc_version() -> Result { if let Some(v) = version { return if v >= *MIN_LDD_VERSION { - Ok(false) - } else if v >= *MIN_LEGACY_LDD_VERSION { Ok(true) } else { Err(format!( @@ -192,10 +177,17 @@ async fn check_is_nixos() -> bool { /// Do not remove this check. /// Provides a way to skip the server glibc requirements check from -/// outside the install flow. A system process can create this -/// file before the server is downloaded and installed. +/// outside the install flow. +/// +/// 1) A system process can create this +/// file before the server is downloaded and installed. +/// +/// 2) An environment variable declared in host +/// that contains path to a glibc sysroot satisfying the +/// minimum requirements. #[cfg(not(windows))] pub async fn skip_requirements_check() -> bool { + std::env::var("VSCODE_SERVER_CUSTOM_GLIBC_LINKER").is_ok() || fs::metadata("/tmp/vscode-skip-server-requirements-check") .await .is_ok() @@ -206,7 +198,7 @@ pub async fn skip_requirements_check() -> bool { false } -/// Checks the glibc++ version, returns "true" if the legacy server is required. +/// Checks the glibc++ version, returns "true" if the default server is required. #[cfg(target_os = "linux")] async fn check_glibcxx_version() -> Result { let mut libstdc_path: Option = None; @@ -250,10 +242,6 @@ fn check_for_sufficient_glibcxx_versions(contents: Vec) -> Result= &*MIN_CXX_VERSION { - return Ok(false); - } - - if max_version >= &*MIN_LEGACY_CXX_VERSION { return Ok(true); } } diff --git a/eslint.config.js b/eslint.config.js index be583f8b..7acc50f2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -11,7 +11,6 @@ import { fileURLToPath } from 'url'; import stylisticTs from '@stylistic/eslint-plugin-ts'; import pluginLocal from 'eslint-plugin-local'; import pluginJsdoc from 'eslint-plugin-jsdoc'; -// import pluginReact from 'eslint-plugin-react'; // Void import pluginHeader from 'eslint-plugin-header'; pluginHeader.rules.header.meta.schema = false; @@ -25,7 +24,10 @@ const ignores = fs.readFileSync(path.join(__dirname, '.eslint-ignore'), 'utf8') export default tseslint.config( // Global ignores { - ignores, + ignores: [ + ...ignores, + '!**/.eslint-plugin-local/**/*' + ], }, // All files (JS and TS) { @@ -81,7 +83,7 @@ export default tseslint.config( 'local/code-parameter-properties-must-have-explicit-accessibility': 'warn', 'local/code-no-nls-in-standalone-editor': 'warn', 'local/code-no-potentially-unsafe-disposables': 'warn', - 'local/code-no-dangerous-type-assertions': 'off', + 'local/code-no-dangerous-type-assertions': 'off', // Void warn -> off 'local/code-no-standalone-editor': 'warn', 'local/code-no-unexternalized-strings': 'warn', 'local/code-must-use-super-dispose': 'warn', @@ -958,7 +960,7 @@ export default tseslint.config( ] }, { - 'target': 'src/vs/editor/editor.worker.ts', + 'target': 'src/vs/editor/editor.worker.start.ts', 'layer': 'worker', 'restrictions': [ 'vs/base/~', @@ -999,6 +1001,7 @@ export default tseslint.config( { 'target': 'src/vs/workbench/api/~', 'restrictions': [ + '@c4312/eventsource-umd', 'vscode', 'vs/base/~', 'vs/base/parts/*/~', diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 224ea33c..531ba21f 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -49,6 +49,7 @@ "settings.json", "launch.json", "tasks.json", + "mcp.json", "keybindings.json", "extensions.json", "argv.json", @@ -117,6 +118,10 @@ "fileMatch": "/.vscode/tasks.json", "url": "vscode://schemas/tasks" }, + { + "fileMatch": "/.vscode/mcp.json", + "url": "vscode://schemas/mcp" + }, { "fileMatch": "%APP_SETTINGS_HOME%/tasks.json", "url": "vscode://schemas/tasks" diff --git a/extensions/csharp/language-configuration.json b/extensions/csharp/language-configuration.json index 60814ae0..7db6640f 100644 --- a/extensions/csharp/language-configuration.json +++ b/extensions/csharp/language-configuration.json @@ -84,9 +84,10 @@ }, "onEnterRules": [ // Add // when pressing enter from inside line comment + // We do not want to match /// (a documentation comment) { "beforeText": { - "pattern": "\/\/.*" + "pattern": "[^\/]\/\/[^\/].*" }, "afterText": { "pattern": "^(?!\\s*$).+" @@ -96,5 +97,16 @@ "appendText": "// " } }, + // Add /// when pressing enter from anywhere inside a documentation comment. + // Documentation comments are not valid after non-whitespace. + { + "beforeText": { + "pattern": "^\\s*\/\/\/" + }, + "action": { + "indent": "none", + "appendText": "/// " + } + }, ] } diff --git a/extensions/css-language-features/package-lock.json b/extensions/css-language-features/package-lock.json index a645c2ab..c843143c 100644 --- a/extensions/css-language-features/package-lock.json +++ b/extensions/css-language-features/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "vscode-languageclient": "^10.0.0-next.13", + "vscode-languageclient": "^10.0.0-next.14", "vscode-uri": "^3.0.8" }, "devDependencies": { @@ -31,48 +31,38 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -87,50 +77,48 @@ "dev": true }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.6", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.6.tgz", - "integrity": "sha512-KCSvUNsFiVciG9iqjJKBZOd66CN3ZKohDlYRmoOi+pd8l15MFLZ8wRG4c+wuzePGba/8WcCG2TM+C/GVlvuaeA==", + "version": "9.0.0-next.7", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.7.tgz", + "integrity": "sha512-7SgnbbbJfYr3off0T2KV/RCMYhVsuLeFPw8l3bkxSiavtoTLsOdu1jyxK3yWbdQuO8QOJC7+no0TXmYjRWSC+g==", + "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "10.0.0-next.13", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.13.tgz", - "integrity": "sha512-KLsOMJoYpkk36PIgcOjyZ4AekOfzp4kdWdRRbVKeVvSIrwrn/4RSZr0NlD6EvUBBJSsJW4WDrYY7Y3znkqa6+w==", + "version": "10.0.0-next.14", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.14.tgz", + "integrity": "sha512-4m/cpNocRgrAkWc8IH4wd3zllAs16NvMmeGcQxFa6xt+mGXJASIeqp0NAFWKZERKg6ClVgBph+SDSZSVvNZ2oA==", "license": "MIT", "dependencies": { - "minimatch": "^9.0.3", - "semver": "^7.6.0", - "vscode-languageserver-protocol": "3.17.6-next.11" + "minimatch": "^10.0.1", + "semver": "^7.6.3", + "vscode-languageserver-protocol": "3.17.6-next.12" }, "engines": { "vscode": "^1.91.0" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.11.tgz", - "integrity": "sha512-GeJxEp1TiLsp79f8WG5n10wLViXfgFKb99hU9K8m7KDWM95/QFEqWkm79f9LVm54tUK74I91a9EeiQLCS/FABQ==", + "version": "3.17.6-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.12.tgz", + "integrity": "sha512-EqrbwF0glTWD2HiDpFc32pJOr6/bJvyKSfCpRQrKy3XsfdloH4p3o/rNJYcpujM0OVLmPZgl1i9g57z9g2YRJA==", + "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.6", - "vscode-languageserver-types": "3.17.6-next.5" + "vscode-jsonrpc": "9.0.0-next.7", + "vscode-languageserver-types": "3.17.6-next.6" } }, "node_modules/vscode-languageserver-types": { - "version": "3.17.6-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.5.tgz", - "integrity": "sha512-QFmf3Yl1tCgUQfA77N9Me/LXldJXkIVypQbty2rJ1DNHQkC+iwvm4Z2tXg9czSwlhvv0pD4pbF5mT7WhAglolw==" + "version": "3.17.6-next.6", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.6.tgz", + "integrity": "sha512-aiJY5/yW+xzw7KPNlwi3gQtddq/3EIn5z8X8nCgJfaiAij2R1APKePngv+MUdLdYJBVTLu+Qa0ODsT+pHgYguQ==", + "license": "MIT" }, "node_modules/vscode-uri": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index 05338813..a24a5352 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -994,7 +994,7 @@ ] }, "dependencies": { - "vscode-languageclient": "^10.0.0-next.13", + "vscode-languageclient": "^10.0.0-next.14", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/css-language-features/server/package-lock.json b/extensions/css-language-features/server/package-lock.json index b9c936d8..9e560d6e 100644 --- a/extensions/css-language-features/server/package-lock.json +++ b/extensions/css-language-features/server/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.2", + "vscode-css-languageservice": "^6.3.3", "vscode-languageserver": "^10.0.0-next.11", "vscode-uri": "^3.0.8" }, @@ -49,9 +49,9 @@ "dev": true }, "node_modules/vscode-css-languageservice": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.2.tgz", - "integrity": "sha512-GEpPxrUTAeXWdZWHev1OJU9lz2Q2/PPBxQ2TIRmLGvQiH3WZbqaNoute0n0ewxlgtjzTW3AKZT+NHySk5Rf4Eg==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.3.tgz", + "integrity": "sha512-xXa+ftMPv6JxRgzkvPwZuDCafIdwDW3kyijGcfij1a2qBVScr2qli6MfgJzYm/AMYdbHq9I/4hdpKV0Thim2EA==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 9b6a8b47..6898d432 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -11,7 +11,7 @@ "browser": "./dist/browser/cssServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.2", + "vscode-css-languageservice": "^6.3.3", "vscode-languageserver": "^10.0.0-next.11", "vscode-uri": "^3.0.8" }, @@ -22,7 +22,7 @@ "scripts": { "compile": "gulp compile-extension:css-language-features-server", "watch": "gulp watch-extension:css-language-features-server", - "install-service-next": "npm install vscode-css-languageservice@next", + "install-service-next": "npm install vscode-css-languageservice", "install-service-local": "npm link vscode-css-languageservice", "install-server-next": "npm install vscode-languageserver@next", "install-server-local": "npm install vscode-languageserver", diff --git a/extensions/debug-server-ready/src/extension.ts b/extensions/debug-server-ready/src/extension.ts index 22a8ff83..40155f30 100644 --- a/extensions/debug-server-ready/src/extension.ts +++ b/extensions/debug-server-ready/src/extension.ts @@ -23,7 +23,14 @@ interface ServerReadyAction { } // From src/vs/base/common/strings.ts -const CSI_SEQUENCE = /(?:(?:\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])|(:?\x1b\].*?\x07)/g; +const CSI_SEQUENCE = /(?:\x1b\[|\x9b)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~]/; +const OSC_SEQUENCE = /(?:\x1b\]|\x9d).*?(?:\x1b\\|\x07|\x9c)/; +const ESC_SEQUENCE = /\x1b(?:[ #%\(\)\*\+\-\.\/]?[a-zA-Z0-9\|}~@])/; +const CONTROL_SEQUENCES = new RegExp('(?:' + [ + CSI_SEQUENCE.source, + OSC_SEQUENCE.source, + ESC_SEQUENCE.source, +].join('|') + ')', 'g'); /** * Froms vs/base/common/strings.ts in core @@ -31,7 +38,7 @@ const CSI_SEQUENCE = /(?:(?:\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])| */ function removeAnsiEscapeCodes(str: string): string { if (str) { - str = str.replace(CSI_SEQUENCE, ''); + str = str.replace(CONTROL_SEQUENCES, ''); } return str; diff --git a/extensions/git/package-lock.json b/extensions/git/package-lock.json index 7a9cb2ad..2629a940 100644 --- a/extensions/git/package-lock.json +++ b/extensions/git/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@joaomoreno/unique-names-generator": "^5.2.0", "@vscode/extension-telemetry": "^0.9.8", - "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", "picomatch": "2.3.1", @@ -217,11 +216,6 @@ "vscode": "^1.75.0" } }, - "node_modules/@vscode/iconv-lite-umd": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz", - "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" - }, "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", diff --git a/extensions/git/package.json b/extensions/git/package.json index 23feced6..6761f02c 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -21,7 +21,6 @@ "contribSourceControlInputBoxMenu", "contribSourceControlTitleMenu", "contribViewsWelcome", - "diffCommand", "editSessionIdentityProvider", "quickDiffProvider", "quickInputButtonLocation", @@ -3327,6 +3326,11 @@ "type": "boolean", "default": true, "markdownDescription": "%config.discardUntrackedChangesToTrash%" + }, + "git.showReferenceDetails": { + "type": "boolean", + "default": true, + "markdownDescription": "%config.showReferenceDetails%" } } }, @@ -3435,10 +3439,10 @@ "id": "git.blame.editorDecorationForeground", "description": "%colors.blameEditorDecoration%", "defaults": { - "dark": "editorCodeLens.foreground", - "light": "editorCodeLens.foreground", - "highContrast": "editorCodeLens.foreground", - "highContrastLight": "editorCodeLens.foreground" + "dark": "editorInlayHint.foreground", + "light": "editorInlayHint.foreground", + "highContrast": "editorInlayHint.foreground", + "highContrastLight": "editorInlayHint.foreground" } } ], @@ -3564,7 +3568,6 @@ "dependencies": { "@joaomoreno/unique-names-generator": "^5.2.0", "@vscode/extension-telemetry": "^0.9.8", - "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "16.5.4", "picomatch": "2.3.1", diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 101618a8..52ba3819 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -289,6 +289,7 @@ "config.diagnosticsCommitHook.Enabled": "Controls whether to check for unresolved diagnostics before committing.", "config.diagnosticsCommitHook.Sources": "Controls the list of sources (**Item**) and the minimum severity (**Value**) to be considered before committing. **Note:** To ignore diagnostics from a particular source, add the source to the list and set the minimum severity to `none`.", "config.discardUntrackedChangesToTrash": "Controls whether discarding untracked changes moves the file(s) to the Recycle Bin (Windows), Trash (macOS, Linux) instead of deleting them permanently. **Note:** This setting has no effect when connected to a remote or when running in Linux as a snap package.", + "config.showReferenceDetails": "Controls whether to show the details of the last commit for Git refs in the checkout, branch, and tag pickers.", "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 5bbb4b03..3e8c9234 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -30,6 +30,7 @@ export interface Ref { readonly type: RefType; readonly name?: string; readonly commit?: string; + readonly commitDetails?: Commit; readonly remote?: string; } diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index f29212c5..e4dda68c 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -69,6 +69,41 @@ function isResourceSchemeSupported(uri: Uri): boolean { return uri.scheme === 'file' || isGitUri(uri); } +function isResourceBlameInformationEqual(a: ResourceBlameInformation | undefined, b: ResourceBlameInformation | undefined): boolean { + if (a === b) { + return true; + } + + if (!a || !b || + a.resource.toString() !== b.resource.toString() || + a.blameInformation.length !== b.blameInformation.length) { + return false; + } + + for (let index = 0; index < a.blameInformation.length; index++) { + if (a.blameInformation[index].lineNumber !== b.blameInformation[index].lineNumber) { + return false; + } + + const aBlameInformation = a.blameInformation[index].blameInformation; + const bBlameInformation = b.blameInformation[index].blameInformation; + + if (typeof aBlameInformation === 'string' && typeof bBlameInformation === 'string') { + if (aBlameInformation !== bBlameInformation) { + return false; + } + } else if (typeof aBlameInformation !== 'string' && typeof bBlameInformation !== 'string') { + if (aBlameInformation.hash !== bBlameInformation.hash) { + return false; + } + } else { + return false; + } + } + + return true; +} + type BlameInformationTemplateTokens = { readonly hash: string; readonly hashShort: string; @@ -79,6 +114,11 @@ type BlameInformationTemplateTokens = { readonly authorDateAgo: string; }; +interface ResourceBlameInformation { + readonly resource: Uri; + readonly blameInformation: readonly LineBlameInformation[]; +} + interface LineBlameInformation { readonly lineNumber: number; readonly blameInformation: BlameInformation | string; @@ -116,11 +156,15 @@ export class GitBlameController { private readonly _onDidChangeBlameInformation = new EventEmitter(); public readonly onDidChangeBlameInformation = this._onDidChangeBlameInformation.event; - private _textEditorBlameInformation: LineBlameInformation[] | undefined; - get textEditorBlameInformation(): readonly LineBlameInformation[] | undefined { + private _textEditorBlameInformation: ResourceBlameInformation | undefined; + get textEditorBlameInformation(): ResourceBlameInformation | undefined { return this._textEditorBlameInformation; } - private set textEditorBlameInformation(blameInformation: LineBlameInformation[] | undefined) { + private set textEditorBlameInformation(blameInformation: ResourceBlameInformation | undefined) { + if (isResourceBlameInformationEqual(this._textEditorBlameInformation, blameInformation)) { + return; + } + this._textEditorBlameInformation = blameInformation; this._onDidChangeBlameInformation.fire(); } @@ -204,7 +248,6 @@ export class GitBlameController { const markdownString = new MarkdownString(); markdownString.isTrusted = true; - markdownString.supportHtml = true; markdownString.supportThemeIcons = true; // Author, date @@ -318,7 +361,7 @@ export class GitBlameController { } window.onDidChangeActiveTextEditor(e => this._updateTextEditorBlameInformation(e), this, this._enablementDisposables); - window.onDidChangeTextEditorSelection(e => this._updateTextEditorBlameInformation(e.textEditor, true), this, this._enablementDisposables); + window.onDidChangeTextEditorSelection(e => this._updateTextEditorBlameInformation(e.textEditor, 'selection'), this, this._enablementDisposables); window.onDidChangeTextEditorDiffInformation(e => this._updateTextEditorBlameInformation(e.textEditor), this, this._enablementDisposables); } } else { @@ -377,7 +420,7 @@ export class GitBlameController { } @throttle - private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined, showBlameInformationForPositionZero = false): Promise { + private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined, reason?: 'selection'): Promise { if (textEditor) { if (!textEditor.diffInformation || textEditor !== window.activeTextEditor) { return; @@ -401,7 +444,7 @@ export class GitBlameController { // Do not show blame information when there is a single selection and it is at the beginning // of the file [0, 0, 0, 0] unless the user explicitly navigates the cursor there. We do this // to avoid showing blame information when the editor is not focused. - if (!showBlameInformationForPositionZero && textEditor.selections.length === 1 && + if (reason !== 'selection' && textEditor.selections.length === 1 && textEditor.selections[0].start.line === 0 && textEditor.selections[0].start.character === 0 && textEditor.selections[0].end.line === 0 && textEditor.selections[0].end.character === 0) { this.textEditorBlameInformation = undefined; @@ -426,8 +469,11 @@ export class GitBlameController { // Resource on the right-hand side of the diff editor when viewing a resource from the index. const diffInformationWorkingTreeAndIndex = getWorkingTreeAndIndexDiffInformation(textEditor); - // Working tree + index diff information is present and it is stale + // Working tree + index diff information is present and it is stale. Diff information + // may be stale when the selection changes because of a content change and the diff + // information is not yet updated. if (diffInformationWorkingTreeAndIndex && diffInformationWorkingTreeAndIndex.isStale) { + this.textEditorBlameInformation = undefined; return; } @@ -440,16 +486,22 @@ export class GitBlameController { // Working tree diff information. Diff Editor (Working Tree) -> Text Editor const diffInformationWorkingTree = getWorkingTreeDiffInformation(textEditor); - // Working tree diff information is not present or it is stale + // Working tree diff information is not present or it is stale. Diff information + // may be stale when the selection changes because of a content change and the diff + // information is not yet updated. if (!diffInformationWorkingTree || diffInformationWorkingTree.isStale) { + this.textEditorBlameInformation = undefined; return; } // Working tree + index diff information const diffInformationWorkingTreeAndIndex = getWorkingTreeAndIndexDiffInformation(textEditor); - // Working tree + index diff information is present and it is stale + // Working tree + index diff information is present and it is stale. Diff information + // may be stale when the selection changes because of a content change and the diff + // information is not yet updated. if (diffInformationWorkingTreeAndIndex && diffInformationWorkingTreeAndIndex.isStale) { + this.textEditorBlameInformation = undefined; return; } @@ -482,7 +534,10 @@ export class GitBlameController { for (const lineNumber of new Set(textEditor.selections.map(s => s.active.line))) { // Check if the line is contained in the working tree diff information if (lineRangesContainLine(workingTreeChanges, lineNumber + 1)) { - lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet') }); + if (reason === 'selection') { + // Only show the `Not Committed Yet` message upon selection change due to navigation + lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet') }); + } continue; } @@ -505,7 +560,10 @@ export class GitBlameController { } } - this.textEditorBlameInformation = lineBlameInformation; + this.textEditorBlameInformation = { + resource: textEditor.document.uri, + blameInformation: lineBlameInformation + }; } dispose() { @@ -555,7 +613,7 @@ class GitBlameEditorDecoration implements HoverProvider { } // Get blame information - const blameInformation = this._controller.textEditorBlameInformation; + const blameInformation = this._controller.textEditorBlameInformation?.blameInformation; const lineBlameInformation = blameInformation?.find(blame => blame.lineNumber === position.line); if (!lineBlameInformation || typeof lineBlameInformation.blameInformation === 'string') { @@ -601,8 +659,8 @@ class GitBlameEditorDecoration implements HoverProvider { } // Get blame information - const blameInformation = this._controller.textEditorBlameInformation; - if (!blameInformation) { + const blameInformation = this._controller.textEditorBlameInformation?.blameInformation; + if (!blameInformation || blameInformation.length === 0) { textEditor.setDecorations(this._decoration, []); return; } @@ -680,7 +738,7 @@ class GitBlameStatusBarItem { return; } - const blameInformation = this._controller.textEditorBlameInformation; + const blameInformation = this._controller.textEditorBlameInformation?.blameInformation; if (!blameInformation || blameInformation.length === 0) { this._statusBarItem.hide(); return; diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index fa566adb..176a95b5 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,16 +5,16 @@ import * as os from 'os'; import * as path from 'path'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages } from 'vscode'; +import { Command, commands, Disposable, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; -import { ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote } from './api/git'; +import { ForcePushMode, GitErrorCodes, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote, Branch, Ref } from './api/git'; import { Git, Stash } from './git'; import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; -import { DiffEditorSelectionHunkToolbarContext, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; +import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; -import { DiagnosticSeverityConfig, dispose, getCommitShortHash, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, toDiagnosticSeverity, truncate } from './util'; +import { DiagnosticSeverityConfig, dispose, fromNow, getCommitShortHash, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, toDiagnosticSeverity, truncate } from './util'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; @@ -73,6 +73,10 @@ class RefItem implements QuickPickItem { } get description(): string { + if (this.ref.commitDetails?.commitDate) { + return fromNow(this.ref.commitDetails.commitDate, true, true); + } + switch (this.ref.type) { case RefType.Head: return this.shortCommit; @@ -85,6 +89,14 @@ class RefItem implements QuickPickItem { } } + get detail(): string | undefined { + if (this.ref.commitDetails?.authorName && this.ref.commitDetails?.message) { + return `${this.ref.commitDetails?.authorName}$(circle-small-filled)${this.ref.commitDetails?.message}`; + } + + return undefined; + } + get refName(): string | undefined { return this.ref.name; } get refRemote(): string | undefined { return this.ref.remote; } get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); } @@ -96,8 +108,26 @@ class RefItem implements QuickPickItem { constructor(protected readonly ref: Ref) { } } -class CheckoutItem extends RefItem { +class BranchItem extends RefItem { + override get description(): string { + const description: string[] = []; + if (typeof this.ref.behind === 'number' && typeof this.ref.ahead === 'number') { + description.push(`${this.ref.behind}↓ ${this.ref.ahead}↑`); + } + if (this.ref.commitDetails?.commitDate) { + description.push(fromNow(this.ref.commitDetails.commitDate, true, true)); + } + + return description.length > 0 ? description.join('$(circle-small-filled)') : this.shortCommit; + } + + constructor(override readonly ref: Branch) { + super(ref); + } +} + +class CheckoutItem extends BranchItem { async run(repository: Repository, opts?: { detached?: boolean }): Promise { if (!this.ref.name) { return; @@ -116,7 +146,6 @@ class CheckoutProtectedItem extends CheckoutItem { override get label(): string { return `$(lock) ${this.ref.name ?? this.shortCommit}`; } - } class CheckoutRemoteHeadItem extends RefItem { @@ -152,7 +181,7 @@ class CheckoutTagItem extends RefItem { } } -class BranchDeleteItem extends RefItem { +class BranchDeleteItem extends BranchItem { async run(repository: Repository, force?: boolean): Promise { if (this.ref.type === RefType.Head && this.refName) { @@ -186,7 +215,7 @@ class RemoteTagDeleteItem extends RefItem { } } -class MergeItem extends RefItem { +class MergeItem extends BranchItem { async run(repository: Repository): Promise { if (this.ref.name || this.ref.commit) { @@ -195,7 +224,7 @@ class MergeItem extends RefItem { } } -class RebaseItem extends RefItem { +class RebaseItem extends BranchItem { async run(repository: Repository): Promise { if (this.ref?.name) { @@ -275,7 +304,6 @@ class StashItem implements QuickPickItem { interface ScmCommandOptions { repository?: boolean; - diff?: boolean; } interface ScmCommand { @@ -327,6 +355,8 @@ async function categorizeResourceByResolution(resources: Resource[]): Promise<{ async function createCheckoutItems(repository: Repository, detached = false): Promise { const config = workspace.getConfiguration('git'); const checkoutTypeConfig = config.get('checkoutType'); + const showRefDetails = config.get('showReferenceDetails') === true; + let checkoutTypes: string[]; if (checkoutTypeConfig === 'all' || !checkoutTypeConfig || checkoutTypeConfig.length === 0) { @@ -342,12 +372,12 @@ async function createCheckoutItems(repository: Repository, detached = false): Pr checkoutTypes = checkoutTypes.filter(t => t !== 'tags'); } - const refs = await repository.getRefs(); + const refs = await repository.getRefs({ includeCommitDetails: showRefDetails }); const refProcessors = checkoutTypes.map(type => getCheckoutRefProcessor(repository, type)) .filter(p => !!p) as RefProcessor[]; const buttons = await getRemoteRefItemButtons(repository); - const itemsProcessor = new CheckoutItemsProcessor(refProcessors, repository, buttons, detached); + const itemsProcessor = new CheckoutItemsProcessor(repository, refProcessors, buttons, detached); return itemsProcessor.processRefs(refs); } @@ -405,10 +435,22 @@ class RefProcessor { class RefItemsProcessor { - constructor(protected readonly processors: RefProcessor[]) { } + constructor( + protected readonly repository: Repository, + protected readonly processors: RefProcessor[], + protected readonly options: { + skipCurrentBranch?: boolean; + skipCurrentBranchRemote?: boolean; + } = {} + ) { } processRefs(refs: Ref[]): QuickPickItem[] { + const refsToSkip = this.getRefsToSkip(); + for (const ref of refs) { + if (ref.name && refsToSkip.includes(ref.name)) { + continue; + } for (const processor of this.processors) { if (processor.processRef(ref)) { break; @@ -423,48 +465,19 @@ class RefItemsProcessor { return result; } -} -class RebaseItemsProcessors extends RefItemsProcessor { + protected getRefsToSkip(): string[] { + const refsToSkip = ['origin/HEAD']; - private upstreamName: string | undefined; - - constructor(private readonly repository: Repository) { - super([ - new RefProcessor(RefType.Head, RebaseItem), - new RefProcessor(RefType.RemoteHead, RebaseItem) - ]); - - if (this.repository.HEAD?.upstream) { - this.upstreamName = `${this.repository.HEAD?.upstream.remote}/${this.repository.HEAD?.upstream.name}`; - } - } - - override processRefs(refs: Ref[]): QuickPickItem[] { - const result: QuickPickItem[] = []; - - for (const ref of refs) { - if (ref.name === this.repository.HEAD?.name) { - continue; - } - - if (ref.name === this.upstreamName) { - result.push(new RebaseUpstreamItem(ref)); - continue; - } - - for (const processor of this.processors) { - if (processor.processRef(ref)) { - break; - } - } + if (this.options.skipCurrentBranch && this.repository.HEAD?.name) { + refsToSkip.push(this.repository.HEAD.name); } - for (const processor of this.processors) { - result.push(...processor.items); + if (this.options.skipCurrentBranchRemote && this.repository.HEAD?.upstream) { + refsToSkip.push(`${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`); } - return result; + return refsToSkip; } } @@ -490,11 +503,11 @@ class CheckoutItemsProcessor extends RefItemsProcessor { private defaultButtons: RemoteSourceActionButton[] | undefined; constructor( + repository: Repository, processors: RefProcessor[], - private readonly repository: Repository, private readonly buttons: Map, private readonly detached = false) { - super(processors); + super(repository, processors); // Default button(s) const remote = repository.remotes.find(r => r.pushUrl === repository.HEAD?.remote || r.fetchUrl === repository.HEAD?.remote) ?? repository.remotes[0]; @@ -712,12 +725,7 @@ export class CommandCenter { ) { this.disposables = Commands.map(({ commandId, key, method, options }) => { const command = this.createCommand(commandId, key, method, options); - - if (options.diff) { - return commands.registerDiffInformationCommand(commandId, command); - } else { - return commands.registerCommand(commandId, command); - } + return commands.registerCommand(commandId, command); }); this.disposables.push(workspace.registerTextDocumentContentProvider('git-output', this.commandErrors)); @@ -1635,7 +1643,6 @@ export class CommandCenter { if (!textEditor) { return; } - modifiedDocument = textEditor.document; modifiedUri = modifiedDocument.uri; } @@ -1653,8 +1660,8 @@ export class CommandCenter { await repository.stage(resource, result, modifiedDocument.encoding)); } - @command('git.stageSelectedRanges', { diff: true }) - async stageSelectedChanges(changes: LineChange[]): Promise { + @command('git.stageSelectedRanges') + async stageSelectedChanges(): Promise { const textEditor = window.activeTextEditor; if (!textEditor) { @@ -1668,7 +1675,6 @@ export class CommandCenter { const workingTreeLineChanges = toLineChanges(workingTreeDiffInformation); - this.logger.trace(`[CommandCenter][stageSelectedChanges] changes: ${JSON.stringify(changes)}`); this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`); this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation changes: ${JSON.stringify(workingTreeLineChanges)}`); @@ -1849,8 +1855,8 @@ export class CommandCenter { textEditor.selections = [new Selection(firstStagedLine, 0, firstStagedLine, 0)]; } - @command('git.revertSelectedRanges', { diff: true }) - async revertSelectedRanges(changes: LineChange[]): Promise { + @command('git.revertSelectedRanges') + async revertSelectedRanges(): Promise { const textEditor = window.activeTextEditor; if (!textEditor) { @@ -1864,7 +1870,6 @@ export class CommandCenter { const workingTreeLineChanges = toLineChanges(workingTreeDiffInformation); - this.logger.trace(`[CommandCenter][revertSelectedRanges] changes: ${JSON.stringify(changes)}`); this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`); this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation changes: ${JSON.stringify(workingTreeLineChanges)}`); @@ -1939,8 +1944,8 @@ export class CommandCenter { await repository.revert([]); } - @command('git.unstageSelectedRanges', { diff: true }) - async unstageSelectedRanges(changes: LineChange[]): Promise { + @command('git.unstageSelectedRanges') + async unstageSelectedRanges(): Promise { const textEditor = window.activeTextEditor; if (!textEditor) { @@ -1978,7 +1983,6 @@ export class CommandCenter { const indexLineChanges = toLineChanges(indexDiffInformation); - this.logger.trace(`[CommandCenter][unstageSelectedRanges] changes: ${JSON.stringify(changes)}`); this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation: ${JSON.stringify(indexDiffInformation)}`); this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation changes: ${JSON.stringify(indexLineChanges)}`); @@ -2716,6 +2720,7 @@ export class CommandCenter { const quickPick = window.createQuickPick(); quickPick.busy = true; quickPick.sortByLabel = false; + quickPick.matchOnDetail = false; quickPick.placeholder = opts?.detached ? l10n.t('Select a branch to checkout in detached mode') : l10n.t('Select a branch or tag to checkout'); @@ -2936,10 +2941,13 @@ export class CommandCenter { private async _branch(repository: Repository, defaultName?: string, from = false, target?: string): Promise { target = target ?? 'HEAD'; + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + if (from) { const getRefPicks = async () => { - const refs = await repository.getRefs(); - const refProcessors = new RefItemsProcessor([ + const refs = await repository.getRefs({ includeCommitDetails: showRefDetails }); + const refProcessors = new RefItemsProcessor(repository, [ new RefProcessor(RefType.Head), new RefProcessor(RefType.RemoteHead), new RefProcessor(RefType.Tag) @@ -3041,6 +3049,9 @@ export class CommandCenter { private async _deleteBranch(repository: Repository, remote: string | undefined, name: string | undefined, options: { remote: boolean; force?: boolean }): Promise { let run: (force?: boolean) => Promise; + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + if (!options.remote && typeof name === 'string') { // Local branch run = force => repository.deleteBranch(name!, force); @@ -3050,34 +3061,26 @@ export class CommandCenter { } else { const getBranchPicks = async () => { const pattern = options.remote ? 'refs/remotes' : 'refs/heads'; - const refs = await repository.getRefs({ pattern }); + const refs = await repository.getRefs({ pattern, includeCommitDetails: showRefDetails }); + const processors = options.remote + ? [new RefProcessor(RefType.RemoteHead, BranchDeleteItem)] + : [new RefProcessor(RefType.Head, BranchDeleteItem)]; - const refsToExclude: string[] = []; - if (options.remote) { - refsToExclude.push('origin/HEAD'); + const itemsProcessor = new RefItemsProcessor(repository, processors, { + skipCurrentBranch: true, + skipCurrentBranchRemote: true + }); - if (repository.HEAD?.upstream) { - // Current branch's upstream - refsToExclude.push(`${repository.HEAD.upstream.remote}/${repository.HEAD.upstream.name}`); - } - } else { - if (repository.HEAD?.name) { - // Current branch - refsToExclude.push(repository.HEAD.name); - } - } - - return refs.filter(ref => ref.name && !refsToExclude.includes(ref.name)) - .map(ref => new BranchDeleteItem(ref)); + return itemsProcessor.processRefs(refs); }; const placeHolder = !options.remote ? l10n.t('Select a branch to delete') : l10n.t('Select a remote branch to delete'); - const choice = await this.pickRef(getBranchPicks(), placeHolder); + const choice = await this.pickRef(getBranchPicks(), placeHolder); - if (!choice || !choice.refName) { + if (!(choice instanceof BranchDeleteItem) || !choice.refName) { return; } name = choice.refName; @@ -3128,13 +3131,19 @@ export class CommandCenter { @command('git.merge', { repository: true }) async merge(repository: Repository): Promise { + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + const getQuickPickItems = async (): Promise => { - const refs = await repository.getRefs(); - const itemsProcessor = new RefItemsProcessor([ + const refs = await repository.getRefs({ includeCommitDetails: showRefDetails }); + const itemsProcessor = new RefItemsProcessor(repository, [ new RefProcessor(RefType.Head, MergeItem), new RefProcessor(RefType.RemoteHead, MergeItem), new RefProcessor(RefType.Tag, MergeItem) - ]); + ], { + skipCurrentBranch: true, + skipCurrentBranchRemote: true + }); return itemsProcessor.processRefs(refs); }; @@ -3154,11 +3163,31 @@ export class CommandCenter { @command('git.rebase', { repository: true }) async rebase(repository: Repository): Promise { - const getQuickPickItems = async (): Promise => { - const refs = await repository.getRefs(); - const itemsProcessor = new RebaseItemsProcessors(repository); + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; - return itemsProcessor.processRefs(refs); + const getQuickPickItems = async (): Promise => { + const refs = await repository.getRefs({ includeCommitDetails: showRefDetails }); + const itemsProcessor = new RefItemsProcessor(repository, [ + new RefProcessor(RefType.Head, RebaseItem), + new RefProcessor(RefType.RemoteHead, RebaseItem) + ], { + skipCurrentBranch: true, + skipCurrentBranchRemote: true + }); + + const quickPickItems = itemsProcessor.processRefs(refs); + + if (repository.HEAD?.upstream) { + const upstreamRef = refs.find(ref => ref.type === RefType.RemoteHead && + ref.name === `${repository.HEAD!.upstream!.remote}/${repository.HEAD!.upstream!.name}`); + + if (upstreamRef) { + quickPickItems.splice(0, 0, new RebaseUpstreamItem(upstreamRef)); + } + } + + return quickPickItems; }; const placeHolder = l10n.t('Select a branch to rebase onto'); @@ -3193,8 +3222,11 @@ export class CommandCenter { @command('git.deleteTag', { repository: true }) async deleteTag(repository: Repository): Promise { + const config = workspace.getConfiguration('git'); + const showRefDetails = config.get('showReferenceDetails') === true; + const tagPicks = async (): Promise => { - const remoteTags = await repository.getRefs({ pattern: 'refs/tags' }); + const remoteTags = await repository.getRefs({ pattern: 'refs/tags', includeCommitDetails: showRefDetails }); return remoteTags.length === 0 ? [{ label: l10n.t('$(info) This repository has no tags.') }] : remoteTags.map(ref => new TagDeleteItem(ref)); }; diff --git a/extensions/git/src/decorators.ts b/extensions/git/src/decorators.ts index b1a25d4f..f89ff232 100644 --- a/extensions/git/src/decorators.ts +++ b/extensions/git/src/decorators.ts @@ -98,4 +98,4 @@ export function debounce(delay: number): Function { this[timerKey] = setTimeout(() => fn.apply(this, args), delay); }; }); -} \ No newline at end of file +} diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 29fc38ab..e42ca1e0 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -10,11 +10,10 @@ import * as cp from 'child_process'; import { fileURLToPath } from 'url'; import which from 'which'; import { EventEmitter } from 'events'; -import * as iconv from '@vscode/iconv-lite-umd'; import * as filetype from 'file-type'; -import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant } from './util'; +import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant, relativePath } from './util'; import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode'; -import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery, InitOptions } from './api/git'; +import { Commit as ApiCommit, Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery as ApiRefQuery, InitOptions } from './api/git'; import * as byline from 'byline'; import { StringDecoder } from 'string_decoder'; @@ -193,7 +192,6 @@ function cpErrorHandler(cb: (reason?: any) => void): (reason?: any) => void { export interface SpawnOptions extends cp.SpawnOptions { input?: string; - encoding?: string; log?: boolean; cancellationToken?: CancellationToken; onSpawn?: (childProcess: cp.ChildProcess) => void; @@ -354,6 +352,10 @@ function sanitizePath(path: string): string { return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`); } +function sanitizeRelativePath(from: string, to: string): string { + return path.isAbsolute(to) ? relativePath(from, to).replace(/\\/g, '/') : to; +} + const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B'; const STASH_FORMAT = '%H%n%P%n%gd%n%gs'; @@ -616,12 +618,9 @@ export class Git { } } - let encoding = options.encoding || 'utf8'; - encoding = iconv.encodingExists(encoding) ? encoding : 'utf8'; - const result: IExecutionResult = { exitCode: bufferResult.exitCode, - stdout: iconv.decode(bufferResult.stdout, encoding), + stdout: bufferResult.stdout.toString('utf8'), stderr: bufferResult.stderr }; @@ -730,6 +729,10 @@ export interface Commit { shortStat?: CommitShortStat; } +export interface RefQuery extends ApiRefQuery { + readonly includeCommitDetails?: boolean; +} + interface GitConfigSection { name: string; subSectionName?: string; @@ -1125,6 +1128,75 @@ function parseGitBlame(data: string): BlameInformation[] { return Array.from(blameInformation.values()); } +const REFS_FORMAT = '%(refname)%00%(objectname)%00%(*objectname)'; +const REFS_WITH_DETAILS_FORMAT = `${REFS_FORMAT}%00%(parent)%00%(*parent)%00%(authorname)%00%(*authorname)%00%(committerdate:unix)%00%(*committerdate:unix)%00%(subject)%00%(*subject)`; + +function parseRefs(data: string): (Ref | Branch)[] { + const refRegex = /^(refs\/[^\0]+)\0([0-9a-f]{40})\0([0-9a-f]{40})?(?:\0(.*))?$/gm; + + const headRegex = /^refs\/heads\/([^ ]+)$/; + const remoteHeadRegex = /^refs\/remotes\/([^/]+)\/([^ ]+)$/; + const tagRegex = /^refs\/tags\/([^ ]+)$/; + const statusRegex = /\[(?:ahead ([0-9]+))?[,\s]*(?:behind ([0-9]+))?]|\[gone]/; + + let ref: string | undefined; + let commitHash: string | undefined; + let tagCommitHash: string | undefined; + let details: string | undefined; + let commitParents: string | undefined; + let tagCommitParents: string | undefined; + let commitSubject: string | undefined; + let tagCommitSubject: string | undefined; + let authorName: string | undefined; + let tagAuthorName: string | undefined; + let committerDate: string | undefined; + let tagCommitterDate: string | undefined; + let status: string | undefined; + + const refs: (Ref | Branch)[] = []; + + let match: RegExpExecArray | null; + let refMatch: RegExpExecArray | null; + + do { + match = refRegex.exec(data); + if (match === null) { + break; + } + + [, ref, commitHash, tagCommitHash, details] = match; + [commitParents, tagCommitParents, authorName, tagAuthorName, committerDate, tagCommitterDate, commitSubject, tagCommitSubject, status] = details?.split('\0') ?? []; + + const parents = tagCommitParents || commitParents; + const subject = tagCommitSubject || commitSubject; + const author = tagAuthorName || authorName; + const date = tagCommitterDate || committerDate; + + const commitDetails = parents && subject && author && date + ? { + hash: commitHash, + message: subject, + parents: parents.split(' '), + authorName: author, + commitDate: date ? new Date(Number(date) * 1000) : undefined, + } satisfies ApiCommit : undefined; + + if (refMatch = headRegex.exec(ref)) { + const [, aheadCount, behindCount] = statusRegex.exec(status) ?? []; + const ahead = status ? aheadCount ? Number(aheadCount) : 0 : undefined; + const behind = status ? behindCount ? Number(behindCount) : 0 : undefined; + refs.push({ name: refMatch[1], commit: commitHash, commitDetails, ahead, behind, type: RefType.Head }); + } else if (refMatch = remoteHeadRegex.exec(ref)) { + const name = `${refMatch[1]}/${refMatch[2]}`; + refs.push({ name, remote: refMatch[1], commit: commitHash, commitDetails, type: RefType.RemoteHead }); + } else if (refMatch = tagRegex.exec(ref)) { + refs.push({ name: refMatch[1], commit: tagCommitHash ?? commitHash, commitDetails, type: RefType.Tag }); + } + } while (true); + + return refs; +} + export interface PullOptions { readonly unshallow?: boolean; readonly tags?: boolean; @@ -1329,8 +1401,9 @@ export class Repository { .filter(entry => !!entry); } - async buffer(object: string): Promise { - const child = this.stream(['show', '--textconv', object]); + async buffer(ref: string, filePath: string): Promise { + const relativePath = sanitizeRelativePath(this.repositoryRoot, filePath); + const child = this.stream(['show', '--textconv', `${ref}:${relativePath}`]); if (!child.stdout) { return Promise.reject('Can\'t open file from git'); @@ -1382,10 +1455,17 @@ export class Repository { return { mode, object, size: parseInt(size) || 0 }; } - async lstree(treeish: string, path?: string): Promise { - const args = ['ls-tree', '-l', treeish]; + async lstree(treeish: string, path?: string, options?: { recursive?: boolean }): Promise { + const args = ['ls-tree', '-l']; + + if (options?.recursive) { + args.push('-r'); + } + + args.push(treeish); + if (path) { - args.push('--', sanitizePath(path)); + args.push('--', sanitizeRelativePath(this.repositoryRoot, path)); } const { stdout } = await this.exec(args); @@ -1393,15 +1473,24 @@ export class Repository { } async lsfiles(path: string): Promise { - const { stdout } = await this.exec(['ls-files', '--stage', '--', sanitizePath(path)]); + const args = ['ls-files', '--stage']; + const relativePath = sanitizeRelativePath(this.repositoryRoot, path); + + if (relativePath) { + args.push('--', relativePath); + } + + const { stdout } = await this.exec(args); return parseLsFiles(stdout); } - async getGitRelativePath(ref: string, relativePath: string): Promise { - const relativePathLowercase = relativePath.toLowerCase(); - const dirname = path.posix.dirname(relativePath) + '/'; - const elements: { file: string }[] = ref ? await this.lstree(ref, dirname) : await this.lsfiles(dirname); - const element = elements.filter(file => file.file.toLowerCase() === relativePathLowercase)[0]; + async getGitFilePath(ref: string, filePath: string): Promise { + const elements: { file: string }[] = ref + ? await this.lstree(ref, undefined, { recursive: true }) + : await this.lsfiles(this.repositoryRoot); + + const relativePathLowercase = sanitizeRelativePath(this.repositoryRoot, filePath).toLowerCase(); + const element = elements.find(file => file.file.toLowerCase() === relativePathLowercase); if (!element) { throw new GitError({ @@ -1409,7 +1498,7 @@ export class Repository { }); } - return element.file; + return path.join(this.repositoryRoot, element.file); } async detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> { @@ -1489,7 +1578,7 @@ export class Repository { return await this.diffFiles(false); } - const args = ['diff', '--', sanitizePath(path)]; + const args = ['diff', '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout; } @@ -1502,7 +1591,7 @@ export class Repository { return await this.diffFiles(false, ref); } - const args = ['diff', ref, '--', sanitizePath(path)]; + const args = ['diff', ref, '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout; } @@ -1515,7 +1604,7 @@ export class Repository { return await this.diffFiles(true); } - const args = ['diff', '--cached', '--', sanitizePath(path)]; + const args = ['diff', '--cached', '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout; } @@ -1528,7 +1617,7 @@ export class Repository { return await this.diffFiles(true, ref); } - const args = ['diff', '--cached', ref, '--', sanitizePath(path)]; + const args = ['diff', '--cached', ref, '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout; } @@ -1548,7 +1637,7 @@ export class Repository { return await this.diffFiles(false, range); } - const args = ['diff', range, '--', sanitizePath(path)]; + const args = ['diff', range, '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout.trim(); @@ -1640,7 +1729,7 @@ export class Repository { } if (paths && paths.length) { - for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } else { @@ -1655,14 +1744,15 @@ export class Repository { return; } - args.push(...paths.map(sanitizePath)); + args.push(...paths.map(p => sanitizeRelativePath(this.repositoryRoot, p))); await this.exec(args); } - async stage(path: string, data: string, encoding: string): Promise { - const child = this.stream(['hash-object', '--stdin', '-w', '--path', sanitizePath(path)], { stdio: [null, null, null] }); - child.stdin!.end(iconv.encode(data, encoding)); + async stage(path: string, data: Uint8Array): Promise { + const relativePath = sanitizeRelativePath(this.repositoryRoot, path); + const child = this.stream(['hash-object', '--stdin', '-w', '--path', relativePath], { stdio: [null, null, null] }); + child.stdin!.end(data); const { exitCode, stdout } = await exec(child); const hash = stdout.toString('utf8'); @@ -1690,7 +1780,7 @@ export class Repository { add = '--add'; } - await this.exec(['update-index', add, '--cacheinfo', mode, hash, path]); + await this.exec(['update-index', add, '--cacheinfo', mode, hash, relativePath]); } async checkout(treeish: string, paths: string[], opts: { track?: boolean; detached?: boolean } = Object.create(null)): Promise { @@ -1710,7 +1800,7 @@ export class Repository { try { if (paths && paths.length > 0) { - for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } else { @@ -1924,7 +2014,7 @@ export class Repository { const args = ['clean', '-f', '-q']; for (const paths of groups) { - for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { promises.push(limiter.queue(() => this.exec([...args, '--', ...chunk]))); } } @@ -1964,7 +2054,7 @@ export class Repository { try { if (paths && paths.length > 0) { - for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } else { @@ -2209,7 +2299,7 @@ export class Repository { async blame(path: string): Promise { try { - const args = ['blame', sanitizePath(path)]; + const args = ['blame', '--', sanitizeRelativePath(this.repositoryRoot, path)]; const result = await this.exec(args); return result.stdout.trim(); } catch (err) { @@ -2229,7 +2319,7 @@ export class Repository { args.push(ref); } - args.push('--', sanitizePath(path)); + args.push('--', sanitizeRelativePath(this.repositoryRoot, path)); const result = await this.exec(args); @@ -2535,7 +2625,7 @@ export class Repository { .map(([ref]): Branch => ({ name: ref, type: RefType.Head })); } - async getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise { + async getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<(Ref | Branch)[]> { if (cancellationToken && cancellationToken.isCancellationRequested) { throw new CancellationError(); } @@ -2550,7 +2640,14 @@ export class Repository { args.push('--sort', `-${query.sort}`); } - args.push('--format', '%(refname) %(objectname) %(*objectname)'); + if (query.includeCommitDetails) { + const format = this._git.compareGitVersionTo('1.9.0') !== -1 + ? `${REFS_WITH_DETAILS_FORMAT}%00%(upstream:track)` + : REFS_WITH_DETAILS_FORMAT; + args.push('--format', format); + } else { + args.push('--format', REFS_FORMAT); + } if (query.pattern) { const patterns = Array.isArray(query.pattern) ? query.pattern : [query.pattern]; @@ -2564,25 +2661,7 @@ export class Repository { } const result = await this.exec(args, { cancellationToken }); - - const fn = (line: string): Ref | null => { - let match: RegExpExecArray | null; - - if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) { - return { name: match[1], commit: match[2], type: RefType.Head }; - } else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) { - return { name: `${match[1]}/${match[2]}`, commit: match[3], type: RefType.RemoteHead, remote: match[1] }; - } else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) { - return { name: match[1], commit: match[3] ?? match[2], type: RefType.Tag }; - } - - return null; - }; - - return result.stdout.split('\n') - .filter(line => !!line) - .map(fn) - .filter(ref => !!ref) as Ref[]; + return parseRefs(result.stdout); } async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean; cancellationToken?: CancellationToken }): Promise { @@ -2840,7 +2919,7 @@ export class Repository { } async getCommit(ref: string): Promise { - const result = await this.exec(['show', '-s', '--decorate=full', '--shortstat', `--format=${COMMIT_FORMAT}`, '-z', ref]); + const result = await this.exec(['show', '-s', '--decorate=full', '--shortstat', `--format=${COMMIT_FORMAT}`, '-z', ref, '--']); const commits = parseGitCommits(result.stdout); if (commits.length === 0) { return Promise.reject('bad commit format'); @@ -2879,7 +2958,7 @@ export class Repository { async updateSubmodules(paths: string[]): Promise { const args = ['submodule', 'update']; - for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(p => sanitizeRelativePath(this.repositoryRoot, p)), MAX_CLI_LENGTH)) { await this.exec([...args, '--', ...chunk]); } } diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index db316c81..8e5a8f9d 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -163,7 +163,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Compute base if the branch has changed const mergeBase = await this.resolveHEADMergeBase(); - this._currentHistoryItemBaseRef = mergeBase && + this._currentHistoryItemBaseRef = mergeBase && mergeBase.name && mergeBase.remote && (mergeBase.remote !== this.repository.HEAD.upstream?.remote || mergeBase.name !== this.repository.HEAD.upstream?.name) ? { id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 25445d26..910256ff 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -10,11 +10,11 @@ import picomatch from 'picomatch'; import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, Uri, window, workspace, WorkspaceEdit } from 'vscode'; import { ActionButton } from './actionButton'; import { ApiRepository } from './api/api1'; -import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefQuery, RefType, Remote, Status } from './api/git'; +import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git'; import { AutoFetcher } from './autofetch'; import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; import { debounce, memoize, throttle } from './decorators'; -import { Repository as BaseRepository, BlameInformation, Commit, GitError, LogFileOptions, LsTreeElement, PullOptions, Stash, Submodule } from './git'; +import { Repository as BaseRepository, BlameInformation, Commit, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule } from './git'; import { GitHistoryProvider } from './historyProvider'; import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands'; @@ -1222,8 +1222,8 @@ export class Repository implements Disposable { async stage(resource: Uri, contents: string, encoding: string): Promise { await this.run(Operation.Stage, async () => { - const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/'); - await this.repository.stage(path, contents, encoding); + const data = await workspace.encode(contents, resource, { encoding }); + await this.repository.stage(resource.fsPath, data); this._onDidChangeOriginalResource.fire(resource); this.closeDiffEditors([], [...resource.fsPath]); @@ -1388,22 +1388,29 @@ export class Repository implements Disposable { } }); - if (discardUntrackedChangesToTrash) { - const limiter = new Limiter(5); - await Promise.all(toClean.map(fsPath => limiter.queue( - async () => await workspace.fs.delete(Uri.file(fsPath), { useTrash: true })))); - } else { - await this.repository.clean(toClean); - } - - try { - await this.repository.checkout('', toCheckout); - } catch (err) { - if (err.gitErrorCode !== GitErrorCodes.BranchNotYetBorn) { - throw err; + if (toClean.length > 0) { + if (discardUntrackedChangesToTrash) { + const limiter = new Limiter(5); + await Promise.all(toClean.map(fsPath => limiter.queue( + async () => await workspace.fs.delete(Uri.file(fsPath), { useTrash: true })))); + } else { + await this.repository.clean(toClean); } } - await this.repository.updateSubmodules(submodulesToUpdate); + + if (toCheckout.length > 0) { + try { + await this.repository.checkout('', toCheckout); + } catch (err) { + if (err.gitErrorCode !== GitErrorCodes.BranchNotYetBorn) { + throw err; + } + } + } + + if (submodulesToUpdate.length > 0) { + await this.repository.updateSubmodules(submodulesToUpdate); + } this.closeDiffEditors([], [...toClean, ...toCheckout]); }, @@ -1521,7 +1528,11 @@ export class Repository implements Disposable { try { const mergeBase = await this.getConfig(mergeBaseConfigKey); const branchFromConfig = mergeBase !== '' ? await this.getBranch(mergeBase) : undefined; - if (branchFromConfig) { + + // There was a brief period of time when we would consider local branches as a valid + // merge base. Since then we have fixed the issue and only remote branches can be used + // as a merge base so we are adding an additional check. + if (branchFromConfig && branchFromConfig.remote) { return branchFromConfig; } } catch (err) { } @@ -1613,7 +1624,7 @@ export class Repository implements Disposable { } } - async getRefs(query: RefQuery = {}, cancellationToken?: CancellationToken): Promise { + async getRefs(query: RefQuery = {}, cancellationToken?: CancellationToken): Promise<(Ref | Branch)[]> { const config = workspace.getConfiguration('git'); let defaultSort = config.get<'alphabetically' | 'committerdate'>('branchSortOrder'); if (defaultSort !== 'alphabetically' && defaultSort !== 'committerdate') { @@ -1840,18 +1851,12 @@ export class Repository implements Disposable { await this.run(Operation.Push, () => this._push(remote, undefined, false, false, forcePushMode, true)); } - async blame(filePath: string): Promise { - return await this.run(Operation.Blame(true), () => { - const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); - return this.repository.blame(path); - }); + async blame(path: string): Promise { + return await this.run(Operation.Blame(true), () => this.repository.blame(path)); } - async blame2(filePath: string, ref?: string): Promise { - return await this.run(Operation.Blame(false), () => { - const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); - return this.repository.blame2(path, ref); - }); + async blame2(path: string, ref?: string): Promise { + return await this.run(Operation.Blame(false), () => this.repository.blame2(path, ref)); } @throttle @@ -1967,15 +1972,13 @@ export class Repository implements Disposable { async show(ref: string, filePath: string): Promise { return await this.run(Operation.Show, async () => { - const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); - try { - const content = await this.repository.buffer(`${ref}:${path}`); + const content = await this.repository.buffer(ref, filePath); return await workspace.decode(content, Uri.file(filePath)); } catch (err) { if (err.gitErrorCode === GitErrorCodes.WrongCase) { - const gitRelativePath = await this.repository.getGitRelativePath(ref, path); - const content = await this.repository.buffer(`${ref}:${gitRelativePath}`); + const gitFilePath = await this.repository.getGitFilePath(ref, filePath); + const content = await this.repository.buffer(ref, gitFilePath); return await workspace.decode(content, Uri.file(filePath)); } @@ -1985,21 +1988,15 @@ export class Repository implements Disposable { } async buffer(ref: string, filePath: string): Promise { - return this.run(Operation.Show, () => { - const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); - return this.repository.buffer(`${ref}:${path}`); - }); + return this.run(Operation.Show, () => this.repository.buffer(ref, filePath)); } getObjectFiles(ref: string): Promise { return this.run(Operation.GetObjectFiles, () => this.repository.lstree(ref)); } - getObjectDetails(ref: string, filePath: string): Promise<{ mode: string; object: string; size: number }> { - return this.run(Operation.GetObjectDetails, () => { - const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); - return this.repository.getObjectDetails(ref, path); - }); + getObjectDetails(ref: string, path: string): Promise<{ mode: string; object: string; size: number }> { + return this.run(Operation.GetObjectDetails, () => this.repository.getObjectDetails(ref, path)); } detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> { diff --git a/extensions/git/src/staging.ts b/extensions/git/src/staging.ts index 14e4e379..ec7232be 100644 --- a/extensions/git/src/staging.ts +++ b/extensions/git/src/staging.ts @@ -3,9 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, Range, LineChange, Selection, Uri, TextEditor, TextEditorDiffInformation } from 'vscode'; +import { TextDocument, Range, Selection, Uri, TextEditor, TextEditorDiffInformation } from 'vscode'; import { fromGitUri, isGitUri } from './uri'; +export interface LineChange { + readonly originalStartLineNumber: number; + readonly originalEndLineNumber: number; + readonly modifiedStartLineNumber: number; + readonly modifiedEndLineNumber: number; +} + export function applyLineChanges(original: TextDocument, modified: TextDocument, diffs: LineChange[]): string { const result: string[] = []; let currentLine = 0; diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 90d8d28c..ae55d427 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -57,7 +57,6 @@ export class GitTimelineItem extends TimelineItem { setItemDetails(uri: Uri, hash: string | undefined, avatar: string | undefined, author: string, email: string | undefined, date: string, message: string, shortStat?: CommitShortStat, remoteSourceCommands: Command[] = []): void { this.tooltip = new MarkdownString('', true); this.tooltip.isTrusted = true; - this.tooltip.supportHtml = true; const avatarMarkdown = avatar ? `![${author}](${avatar}|width=${AVATAR_SIZE},height=${AVATAR_SIZE})` diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 1d330ea2..fc72bd70 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -11,7 +11,6 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts", - "../../src/vscode-dts/vscode.proposed.diffCommand.d.ts", "../../src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts", "../../src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts", "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", diff --git a/extensions/html-language-features/package-lock.json b/extensions/html-language-features/package-lock.json index 20b14561..562d6751 100644 --- a/extensions/html-language-features/package-lock.json +++ b/extensions/html-language-features/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@vscode/extension-telemetry": "^0.9.8", - "vscode-languageclient": "^10.0.0-next.13", + "vscode-languageclient": "^10.0.0-next.14", "vscode-uri": "^3.0.8" }, "devDependencies": { @@ -168,50 +168,40 @@ } }, "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c= sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -226,50 +216,48 @@ "dev": true }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.6", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.6.tgz", - "integrity": "sha512-KCSvUNsFiVciG9iqjJKBZOd66CN3ZKohDlYRmoOi+pd8l15MFLZ8wRG4c+wuzePGba/8WcCG2TM+C/GVlvuaeA==", + "version": "9.0.0-next.7", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.7.tgz", + "integrity": "sha512-7SgnbbbJfYr3off0T2KV/RCMYhVsuLeFPw8l3bkxSiavtoTLsOdu1jyxK3yWbdQuO8QOJC7+no0TXmYjRWSC+g==", + "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "10.0.0-next.13", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.13.tgz", - "integrity": "sha512-KLsOMJoYpkk36PIgcOjyZ4AekOfzp4kdWdRRbVKeVvSIrwrn/4RSZr0NlD6EvUBBJSsJW4WDrYY7Y3znkqa6+w==", + "version": "10.0.0-next.14", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.14.tgz", + "integrity": "sha512-4m/cpNocRgrAkWc8IH4wd3zllAs16NvMmeGcQxFa6xt+mGXJASIeqp0NAFWKZERKg6ClVgBph+SDSZSVvNZ2oA==", "license": "MIT", "dependencies": { - "minimatch": "^9.0.3", - "semver": "^7.6.0", - "vscode-languageserver-protocol": "3.17.6-next.11" + "minimatch": "^10.0.1", + "semver": "^7.6.3", + "vscode-languageserver-protocol": "3.17.6-next.12" }, "engines": { "vscode": "^1.91.0" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.11.tgz", - "integrity": "sha512-GeJxEp1TiLsp79f8WG5n10wLViXfgFKb99hU9K8m7KDWM95/QFEqWkm79f9LVm54tUK74I91a9EeiQLCS/FABQ==", + "version": "3.17.6-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.12.tgz", + "integrity": "sha512-EqrbwF0glTWD2HiDpFc32pJOr6/bJvyKSfCpRQrKy3XsfdloH4p3o/rNJYcpujM0OVLmPZgl1i9g57z9g2YRJA==", + "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.6", - "vscode-languageserver-types": "3.17.6-next.5" + "vscode-jsonrpc": "9.0.0-next.7", + "vscode-languageserver-types": "3.17.6-next.6" } }, "node_modules/vscode-languageserver-types": { - "version": "3.17.6-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.5.tgz", - "integrity": "sha512-QFmf3Yl1tCgUQfA77N9Me/LXldJXkIVypQbty2rJ1DNHQkC+iwvm4Z2tXg9czSwlhvv0pD4pbF5mT7WhAglolw==" + "version": "3.17.6-next.6", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.6.tgz", + "integrity": "sha512-aiJY5/yW+xzw7KPNlwi3gQtddq/3EIn5z8X8nCgJfaiAij2R1APKePngv+MUdLdYJBVTLu+Qa0ODsT+pHgYguQ==", + "license": "MIT" }, "node_modules/vscode-uri": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index be411fe6..d4356de4 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -259,7 +259,7 @@ }, "dependencies": { "@vscode/extension-telemetry": "^0.9.8", - "vscode-languageclient": "^10.0.0-next.13", + "vscode-languageclient": "^10.0.0-next.14", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/html-language-features/server/package-lock.json b/extensions/html-language-features/server/package-lock.json index 5795436c..d8af9583 100644 --- a/extensions/html-language-features/server/package-lock.json +++ b/extensions/html-language-features/server/package-lock.json @@ -10,9 +10,9 @@ "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.2", - "vscode-html-languageservice": "^5.3.2", - "vscode-languageserver": "^10.0.0-next.11", + "vscode-css-languageservice": "^6.3.3", + "vscode-html-languageservice": "^5.3.3", + "vscode-languageserver": "^10.0.0-next.12", "vscode-languageserver-textdocument": "^1.0.12", "vscode-uri": "^3.0.8" }, @@ -51,9 +51,9 @@ "dev": true }, "node_modules/vscode-css-languageservice": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.2.tgz", - "integrity": "sha512-GEpPxrUTAeXWdZWHev1OJU9lz2Q2/PPBxQ2TIRmLGvQiH3WZbqaNoute0n0ewxlgtjzTW3AKZT+NHySk5Rf4Eg==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.3.tgz", + "integrity": "sha512-xXa+ftMPv6JxRgzkvPwZuDCafIdwDW3kyijGcfij1a2qBVScr2qli6MfgJzYm/AMYdbHq9I/4hdpKV0Thim2EA==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", @@ -63,50 +63,53 @@ } }, "node_modules/vscode-html-languageservice": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.3.2.tgz", - "integrity": "sha512-3MgFQqVG+iQVNG7QI/slaoL7lJpne0nssX082kjUF1yn/YJa8BWCLeCJjM0YpTlp8A7JT1+J22mk4qSPx3NjSQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.3.3.tgz", + "integrity": "sha512-AK/jJM0VIWRrlfqkDBMZxNMnxYT5I2uoMVRoNJ5ePSplnSaT9mbYjqJlxxeLvUrOW7MEH0vVIDzU48u44QZE0w==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.1.0" } }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.6", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.6.tgz", - "integrity": "sha512-KCSvUNsFiVciG9iqjJKBZOd66CN3ZKohDlYRmoOi+pd8l15MFLZ8wRG4c+wuzePGba/8WcCG2TM+C/GVlvuaeA==", + "version": "9.0.0-next.7", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.7.tgz", + "integrity": "sha512-7SgnbbbJfYr3off0T2KV/RCMYhVsuLeFPw8l3bkxSiavtoTLsOdu1jyxK3yWbdQuO8QOJC7+no0TXmYjRWSC+g==", + "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageserver": { - "version": "10.0.0-next.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.11.tgz", - "integrity": "sha512-cmobSrVDYhlh/t02vz/bV8nNpds8mus5HnILULae2iAvOjoaJPnTAp0jJWoYdUqTpIVzT9JV6JMKqLEvdqpeqg==", + "version": "10.0.0-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.12.tgz", + "integrity": "sha512-6lT2CJhH93YFmdDrFTwWvuG0/yzEN2Zbw/DfPaRF91sylZ3TSD0NkJU5jug6t/3NLoDh9VjfJZkgkKr6e3UmRw==", "license": "MIT", "dependencies": { - "vscode-languageserver-protocol": "3.17.6-next.11" + "vscode-languageserver-protocol": "3.17.6-next.12" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.11.tgz", - "integrity": "sha512-GeJxEp1TiLsp79f8WG5n10wLViXfgFKb99hU9K8m7KDWM95/QFEqWkm79f9LVm54tUK74I91a9EeiQLCS/FABQ==", + "version": "3.17.6-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.12.tgz", + "integrity": "sha512-EqrbwF0glTWD2HiDpFc32pJOr6/bJvyKSfCpRQrKy3XsfdloH4p3o/rNJYcpujM0OVLmPZgl1i9g57z9g2YRJA==", + "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.6", - "vscode-languageserver-types": "3.17.6-next.5" + "vscode-jsonrpc": "9.0.0-next.7", + "vscode-languageserver-types": "3.17.6-next.6" } }, "node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": { - "version": "3.17.6-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.5.tgz", - "integrity": "sha512-QFmf3Yl1tCgUQfA77N9Me/LXldJXkIVypQbty2rJ1DNHQkC+iwvm4Z2tXg9czSwlhvv0pD4pbF5mT7WhAglolw==" + "version": "3.17.6-next.6", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.6.tgz", + "integrity": "sha512-aiJY5/yW+xzw7KPNlwi3gQtddq/3EIn5z8X8nCgJfaiAij2R1APKePngv+MUdLdYJBVTLu+Qa0ODsT+pHgYguQ==", + "license": "MIT" }, "node_modules/vscode-languageserver-textdocument": { "version": "1.0.12", @@ -119,9 +122,10 @@ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" } } } diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 7017ee85..7eabd342 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -10,9 +10,9 @@ "main": "./out/node/htmlServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.2", - "vscode-html-languageservice": "^5.3.2", - "vscode-languageserver": "^10.0.0-next.11", + "vscode-css-languageservice": "^6.3.3", + "vscode-html-languageservice": "^5.3.3", + "vscode-languageserver": "^10.0.0-next.12", "vscode-languageserver-textdocument": "^1.0.12", "vscode-uri": "^3.0.8" }, @@ -23,7 +23,7 @@ "scripts": { "compile": "npx gulp compile-extension:html-language-features-server", "watch": "npx gulp watch-extension:html-language-features-server", - "install-service-next": "npm install vscode-css-languageservice@next && npm install vscode-html-languageservice@next", + "install-service-next": "npm install vscode-css-languageservice && npm install vscode-html-languageservice", "install-service-local": "npm install vscode-css-languageservice && npm install vscode-html-languageservice", "install-server-next": "npm install vscode-languageserver@next", "install-server-local": "npm install vscode-languageserver", diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index f7ee3c34..beb65118 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -74,7 +74,8 @@ { "command": "notebook.cellOutput.addToChat", "title": "%addCellOutputToChat.title%", - "category": "Notebook" + "category": "Notebook", + "enablement": "chatIsEnabled" }, { "command": "notebook.cellOutput.openInTextEditor", @@ -144,7 +145,7 @@ }, { "command": "notebook.cellOutput.addToChat", - "when": "webviewId == 'notebook.output' && webviewSection == 'image'", + "when": "webviewId == 'notebook.output' && (webviewSection == 'text' || webviewSection == 'image')", "group": "context@2" }, { diff --git a/extensions/ipynb/src/constants.ts b/extensions/ipynb/src/constants.ts index 9a82ccfa..b7218596 100644 --- a/extensions/ipynb/src/constants.ts +++ b/extensions/ipynb/src/constants.ts @@ -5,7 +5,7 @@ import type { DocumentSelector } from 'vscode'; -export const defaultNotebookFormat = { major: 4, minor: 2 }; +export const defaultNotebookFormat = { major: 4, minor: 5 }; export const ATTACHMENT_CLEANUP_COMMANDID = 'ipynb.cleanInvalidImageAttachment'; export const JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR: DocumentSelector = { notebookType: 'jupyter-notebook', language: 'markdown' }; diff --git a/extensions/ipynb/src/helper.ts b/extensions/ipynb/src/helper.ts index beab091f..6d67b7d5 100644 --- a/extensions/ipynb/src/helper.ts +++ b/extensions/ipynb/src/helper.ts @@ -147,13 +147,15 @@ export interface ITask { /** * Copied from src/vs/base/common/uuid.ts */ -export function generateUuid() { - // use `randomValues` if possible - function getRandomValues(bucket: Uint8Array): Uint8Array { - for (let i = 0; i < bucket.length; i++) { - bucket[i] = Math.floor(Math.random() * 256); - } - return bucket; +export function generateUuid(): string { + // use `randomUUID` if possible + if (typeof crypto.randomUUID === 'function') { + // see https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto + // > Although crypto is available on all windows, the returned Crypto object only has one + // > usable feature in insecure contexts: the getRandomValues() method. + // > In general, you should use this API only in secure contexts. + + return crypto.randomUUID.bind(crypto)(); } // prep-work @@ -164,7 +166,7 @@ export function generateUuid() { } // get data - getRandomValues(_data); + crypto.getRandomValues(_data); // set version bits _data[6] = (_data[6] & 0x0f) | 0x40; diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index e4fe302d..cc55d39e 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -8,6 +8,7 @@ import { activate as keepNotebookModelStoreInSync } from './notebookModelStoreSy import { notebookImagePasteSetup } from './notebookImagePaste'; import { AttachmentCleaner } from './notebookAttachmentCleaner'; import { serializeNotebookToString } from './serializers'; +import { defaultNotebookFormat } from './constants'; // From {nbformat.INotebookMetadata} in @jupyterlab/coreutils type NotebookMetadata = { @@ -86,8 +87,8 @@ export function activate(context: vscode.ExtensionContext, serializer: vscode.No data.metadata = { cells: [], metadata: {}, - nbformat: 4, - nbformat_minor: 2 + nbformat: defaultNotebookFormat.major, + nbformat_minor: defaultNotebookFormat.minor, }; const doc = await vscode.workspace.openNotebookDocument('jupyter-notebook', data); await vscode.window.showNotebookDocument(doc); diff --git a/extensions/ipynb/src/test/notebookModelStoreSync.test.ts b/extensions/ipynb/src/test/notebookModelStoreSync.test.ts index eab92167..7174678a 100644 --- a/extensions/ipynb/src/test/notebookModelStoreSync.test.ts +++ b/extensions/ipynb/src/test/notebookModelStoreSync.test.ts @@ -101,7 +101,8 @@ suite(`Notebook Model Store Sync`, () => { assert.strictEqual(editsApplied.length, 0); assert.strictEqual(cellMetadataUpdates.length, 0); }); - test('Adding cell will result in an update to the metadata', async () => { + test('Adding cell to nbformat 4.2 notebook will result in adding empty metadata', async () => { + sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 2 })); const cell: NotebookCell = { document: {} as any, executionSummary: {}, @@ -131,7 +132,7 @@ suite(`Notebook Model Store Sync`, () => { const newMetadata = cellMetadataUpdates[0].newCellMetadata; assert.deepStrictEqual(newMetadata, { execution_count: null, metadata: {} }); }); - test('Add cell id if nbformat is 4.5', async () => { + test('Added cell will have a cell id if nbformat is 4.5', async () => { sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 5 })); const cell: NotebookCell = { document: {} as any, diff --git a/extensions/javascript/javascript-language-configuration.json b/extensions/javascript/javascript-language-configuration.json index c2444c45..20827982 100644 --- a/extensions/javascript/javascript-language-configuration.json +++ b/extensions/javascript/javascript-language-configuration.json @@ -231,7 +231,7 @@ // Add // when pressing enter from inside line comment { "beforeText": { - "pattern": "(? = new RequestType('json/languageStatus'); } +namespace ValidateContentRequest { + export const type: RequestType<{ schemaUri: string; content: string }, LSPDiagnostic[], any> = new RequestType('json/validateContent'); +} interface SortOptions extends LSPFormattingOptions { } @@ -182,7 +186,7 @@ export async function startClient(context: ExtensionContext, newLanguageClient: }; } -async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { +async function startClientWithParticipants(_context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { const toDispose: Disposable[] = []; @@ -211,6 +215,10 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa window.showInformationMessage(l10n.t('JSON schema cache cleared.')); })); + toDispose.push(commands.registerCommand('json.validate', async (schemaUri: Uri, content: string) => { + const diagnostics: LSPDiagnostic[] = await client.sendRequest(ValidateContentRequest.type, { schemaUri: schemaUri.toString(), content }); + return diagnostics.map(client.protocol2CodeConverter.asDiagnostic); + })); toDispose.push(commands.registerCommand('json.sort', async () => { @@ -495,10 +503,19 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa toDispose.push(commands.registerCommand('_json.retryResolveSchema', handleRetryResolveSchemaCommand)); - client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); + client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations()); - toDispose.push(extensions.onDidChange(_ => { - client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); + toDispose.push(extensions.onDidChange(async _ => { + client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations()); + })); + + const associationWatcher = workspace.createFileSystemWatcher(new RelativePattern( + Uri.parse(`vscode://schemas-associations/`), + '**/schemas-associations.json') + ); + toDispose.push(associationWatcher); + toDispose.push(associationWatcher.onDidChange(async _e => { + client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations()); })); // manually register / deregister format provider based on the `json.format.enable` setting avoiding issues with late registration. See #71652. @@ -595,7 +612,12 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa }; } -function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] { +async function getSchemaAssociations(): Promise { + return getSchemaExtensionAssociations() + .concat(await getDynamicSchemaAssociations()); +} + +function getSchemaExtensionAssociations(): ISchemaAssociation[] { const associations: ISchemaAssociation[] = []; extensions.allAcrossExtensionHosts.forEach(extension => { const packageJSON = extension.packageJSON; @@ -631,6 +653,24 @@ function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] return associations; } +async function getDynamicSchemaAssociations(): Promise { + const result: ISchemaAssociation[] = []; + try { + const data = await workspace.fs.readFile(Uri.parse(`vscode://schemas-associations/schemas-associations.json`)); + const rawStr = new TextDecoder().decode(data); + const obj = >JSON.parse(rawStr); + for (const item of Object.keys(obj)) { + result.push({ + fileMatch: obj[item], + uri: item + }); + } + } catch { + // ignore + } + return result; +} + function getSettings(): Settings { const configuration = workspace.getConfiguration(); const httpSettings = workspace.getConfiguration('http'); @@ -735,3 +775,5 @@ function updateMarkdownString(h: MarkdownString): MarkdownString { function isSchemaResolveError(d: Diagnostic) { return d.code === /* SchemaResolveError */ 0x300; } + + diff --git a/extensions/json-language-features/client/src/languageStatus.ts b/extensions/json-language-features/client/src/languageStatus.ts index 6f3d7468..1064a0b5 100644 --- a/extensions/json-language-features/client/src/languageStatus.ts +++ b/extensions/json-language-features/client/src/languageStatus.ts @@ -185,13 +185,13 @@ export function createLanguageStatusItem(documentSelector: DocumentSelector, sta const schemas = (await statusRequest(document.uri.toString())).schemas; statusItem.detail = undefined; if (schemas.length === 0) { - statusItem.text = l10n.t('No Schema Validation'); + statusItem.text = l10n.t('No schema validation'); statusItem.detail = l10n.t('no JSON schema configured'); } else if (schemas.length === 1) { - statusItem.text = l10n.t('Schema Validated'); + statusItem.text = l10n.t('Schema validated'); statusItem.detail = l10n.t('JSON schema configured'); } else { - statusItem.text = l10n.t('Schema Validated'); + statusItem.text = l10n.t('Schema validated'); statusItem.detail = l10n.t('multiple JSON schemas configured'); } statusItem.command = { diff --git a/extensions/json-language-features/package-lock.json b/extensions/json-language-features/package-lock.json index bde80cbf..85f31440 100644 --- a/extensions/json-language-features/package-lock.json +++ b/extensions/json-language-features/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@vscode/extension-telemetry": "^0.9.8", "request-light": "^0.8.0", - "vscode-languageclient": "^10.0.0-next.13" + "vscode-languageclient": "^10.0.0-next.14" }, "devDependencies": { "@types/node": "20.x" @@ -168,38 +168,30 @@ } }, "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c= sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -211,12 +203,10 @@ "integrity": "sha512-bH6E4PMmsEXYrLX6Kr1vu+xI3HproB1vECAwaPSJeroLE1kpWE3HR27uB4icx+6YORu1ajqBJXxuedv8ZQg5Lw==" }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -231,45 +221,43 @@ "dev": true }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.6", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.6.tgz", - "integrity": "sha512-KCSvUNsFiVciG9iqjJKBZOd66CN3ZKohDlYRmoOi+pd8l15MFLZ8wRG4c+wuzePGba/8WcCG2TM+C/GVlvuaeA==", + "version": "9.0.0-next.7", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.7.tgz", + "integrity": "sha512-7SgnbbbJfYr3off0T2KV/RCMYhVsuLeFPw8l3bkxSiavtoTLsOdu1jyxK3yWbdQuO8QOJC7+no0TXmYjRWSC+g==", + "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "10.0.0-next.13", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.13.tgz", - "integrity": "sha512-KLsOMJoYpkk36PIgcOjyZ4AekOfzp4kdWdRRbVKeVvSIrwrn/4RSZr0NlD6EvUBBJSsJW4WDrYY7Y3znkqa6+w==", + "version": "10.0.0-next.14", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.14.tgz", + "integrity": "sha512-4m/cpNocRgrAkWc8IH4wd3zllAs16NvMmeGcQxFa6xt+mGXJASIeqp0NAFWKZERKg6ClVgBph+SDSZSVvNZ2oA==", "license": "MIT", "dependencies": { - "minimatch": "^9.0.3", - "semver": "^7.6.0", - "vscode-languageserver-protocol": "3.17.6-next.11" + "minimatch": "^10.0.1", + "semver": "^7.6.3", + "vscode-languageserver-protocol": "3.17.6-next.12" }, "engines": { "vscode": "^1.91.0" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.11.tgz", - "integrity": "sha512-GeJxEp1TiLsp79f8WG5n10wLViXfgFKb99hU9K8m7KDWM95/QFEqWkm79f9LVm54tUK74I91a9EeiQLCS/FABQ==", + "version": "3.17.6-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.12.tgz", + "integrity": "sha512-EqrbwF0glTWD2HiDpFc32pJOr6/bJvyKSfCpRQrKy3XsfdloH4p3o/rNJYcpujM0OVLmPZgl1i9g57z9g2YRJA==", + "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.6", - "vscode-languageserver-types": "3.17.6-next.5" + "vscode-jsonrpc": "9.0.0-next.7", + "vscode-languageserver-types": "3.17.6-next.6" } }, "node_modules/vscode-languageserver-types": { - "version": "3.17.6-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.5.tgz", - "integrity": "sha512-QFmf3Yl1tCgUQfA77N9Me/LXldJXkIVypQbty2rJ1DNHQkC+iwvm4Z2tXg9czSwlhvv0pD4pbF5mT7WhAglolw==" - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "3.17.6-next.6", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.6.tgz", + "integrity": "sha512-aiJY5/yW+xzw7KPNlwi3gQtddq/3EIn5z8X8nCgJfaiAij2R1APKePngv+MUdLdYJBVTLu+Qa0ODsT+pHgYguQ==", + "license": "MIT" } } } diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index cf4a7f16..375afed0 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -16,7 +16,8 @@ "activationEvents": [ "onLanguage:json", "onLanguage:jsonc", - "onLanguage:snippets" + "onLanguage:snippets", + "onCommand:json.validate" ], "main": "./client/out/node/jsonClientMain", "browser": "./client/dist/browser/jsonClientMain", @@ -169,7 +170,7 @@ "dependencies": { "@vscode/extension-telemetry": "^0.9.8", "request-light": "^0.8.0", - "vscode-languageclient": "^10.0.0-next.13" + "vscode-languageclient": "^10.0.0-next.14" }, "devDependencies": { "@types/node": "20.x" diff --git a/extensions/json-language-features/server/package-lock.json b/extensions/json-language-features/server/package-lock.json index 384ce045..00361def 100644 --- a/extensions/json-language-features/server/package-lock.json +++ b/extensions/json-language-features/server/package-lock.json @@ -12,8 +12,8 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "request-light": "^0.8.0", - "vscode-json-languageservice": "^5.4.3", - "vscode-languageserver": "^10.0.0-next.11", + "vscode-json-languageservice": "^5.4.4", + "vscode-languageserver": "^10.0.0-next.12", "vscode-uri": "^3.0.8" }, "bin": { @@ -64,51 +64,54 @@ "dev": true }, "node_modules/vscode-json-languageservice": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.4.3.tgz", - "integrity": "sha512-NVSEQDloP9NYccuqKg4eI46kutZpwucBY4csBB6FCxbM7AZVoBt0oxTItPVA+ZwhnG1bg/fmiBRAwcGJyNQoPA==", + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.4.4.tgz", + "integrity": "sha512-dgT16da8VznFv0IrEpBSKYvi29gxnMf5EOq+UfZSPaCiLZ65kgVOo3vMJSPNbZK8557YYbQH/fpMxxa4wRPAQw==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", - "vscode-uri": "^3.0.8" + "vscode-uri": "^3.1.0" } }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.6", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.6.tgz", - "integrity": "sha512-KCSvUNsFiVciG9iqjJKBZOd66CN3ZKohDlYRmoOi+pd8l15MFLZ8wRG4c+wuzePGba/8WcCG2TM+C/GVlvuaeA==", + "version": "9.0.0-next.7", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.7.tgz", + "integrity": "sha512-7SgnbbbJfYr3off0T2KV/RCMYhVsuLeFPw8l3bkxSiavtoTLsOdu1jyxK3yWbdQuO8QOJC7+no0TXmYjRWSC+g==", + "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageserver": { - "version": "10.0.0-next.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.11.tgz", - "integrity": "sha512-cmobSrVDYhlh/t02vz/bV8nNpds8mus5HnILULae2iAvOjoaJPnTAp0jJWoYdUqTpIVzT9JV6JMKqLEvdqpeqg==", + "version": "10.0.0-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.12.tgz", + "integrity": "sha512-6lT2CJhH93YFmdDrFTwWvuG0/yzEN2Zbw/DfPaRF91sylZ3TSD0NkJU5jug6t/3NLoDh9VjfJZkgkKr6e3UmRw==", "license": "MIT", "dependencies": { - "vscode-languageserver-protocol": "3.17.6-next.11" + "vscode-languageserver-protocol": "3.17.6-next.12" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.11", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.11.tgz", - "integrity": "sha512-GeJxEp1TiLsp79f8WG5n10wLViXfgFKb99hU9K8m7KDWM95/QFEqWkm79f9LVm54tUK74I91a9EeiQLCS/FABQ==", + "version": "3.17.6-next.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.12.tgz", + "integrity": "sha512-EqrbwF0glTWD2HiDpFc32pJOr6/bJvyKSfCpRQrKy3XsfdloH4p3o/rNJYcpujM0OVLmPZgl1i9g57z9g2YRJA==", + "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.6", - "vscode-languageserver-types": "3.17.6-next.5" + "vscode-jsonrpc": "9.0.0-next.7", + "vscode-languageserver-types": "3.17.6-next.6" } }, "node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": { - "version": "3.17.6-next.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.5.tgz", - "integrity": "sha512-QFmf3Yl1tCgUQfA77N9Me/LXldJXkIVypQbty2rJ1DNHQkC+iwvm4Z2tXg9czSwlhvv0pD4pbF5mT7WhAglolw==" + "version": "3.17.6-next.6", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.6.tgz", + "integrity": "sha512-aiJY5/yW+xzw7KPNlwi3gQtddq/3EIn5z8X8nCgJfaiAij2R1APKePngv+MUdLdYJBVTLu+Qa0ODsT+pHgYguQ==", + "license": "MIT" }, "node_modules/vscode-languageserver-textdocument": { "version": "1.0.12", @@ -121,9 +124,10 @@ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" } } } diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 6dcd8293..f503a7e1 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -15,8 +15,8 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "request-light": "^0.8.0", - "vscode-json-languageservice": "^5.4.3", - "vscode-languageserver": "^10.0.0-next.11", + "vscode-json-languageservice": "^5.4.4", + "vscode-languageserver": "^10.0.0-next.12", "vscode-uri": "^3.0.8" }, "devDependencies": { @@ -28,7 +28,7 @@ "compile": "npx gulp compile-extension:json-language-features-server", "watch": "npx gulp watch-extension:json-language-features-server", "clean": "../../../node_modules/.bin/rimraf out", - "install-service-next": "npm install vscode-json-languageservice@next", + "install-service-next": "npm install vscode-json-languageservice", "install-service-latest": "npm install vscode-json-languageservice", "install-service-local": "npm link vscode-json-languageservice", "install-server-next": "npm install vscode-languageserver@next", diff --git a/extensions/json-language-features/server/src/jsonServer.ts b/extensions/json-language-features/server/src/jsonServer.ts index 36ca0dc5..6a806b72 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -40,6 +40,10 @@ namespace LanguageStatusRequest { export const type: RequestType = new RequestType('json/languageStatus'); } +namespace ValidateContentRequest { + export const type: RequestType<{ schemaUri: string; content: string }, Diagnostic[], any> = new RequestType('json/validateContent'); +} + export interface DocumentSortingParams { /** * The uri of the document to sort. @@ -299,6 +303,14 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) return []; }); + connection.onRequest(ValidateContentRequest.type, async ({ schemaUri, content }) => { + const docURI = 'vscode://schemas/temp/' + new Date().getTime(); + const document = TextDocument.create(docURI, 'json', 1, content); + updateConfiguration([{ uri: schemaUri, fileMatch: [docURI] }]); + return await validateTextDocument(document); + }); + + connection.onRequest(LanguageStatusRequest.type, async uri => { const document = documents.get(uri); if (document) { @@ -319,7 +331,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) return []; }); - function updateConfiguration() { + function updateConfiguration(extraSchemas?: SchemaConfiguration[]) { const languageSettings = { validate: validateEnabled, allowComments: true, @@ -350,6 +362,10 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } }); } + if (extraSchemas) { + languageSettings.schemas.push(...extraSchemas); + } + languageService.configure(languageSettings); diagnosticsSupport?.requestRefresh(); @@ -529,3 +545,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) function getFullRange(document: TextDocument): Range { return Range.create(Position.create(0, 0), document.positionAt(document.getText().length)); } + + + + diff --git a/extensions/mangle-loader.js b/extensions/mangle-loader.js index 73d62576..016d0f69 100644 --- a/extensions/mangle-loader.js +++ b/extensions/mangle-loader.js @@ -37,7 +37,7 @@ function getMangledFileContents(projectPath) { * @type {webpack.LoaderDefinitionFunction} */ module.exports = async function (source, sourceMap, meta) { - if (true) { // Void - extensions-disable-mangler + if (this.mode !== 'production') { // Only enable mangling in production builds return source; } diff --git a/extensions/markdown-language-features/package-lock.json b/extensions/markdown-language-features/package-lock.json index 63d93c40..2b8430bb 100644 --- a/extensions/markdown-language-features/package-lock.json +++ b/extensions/markdown-language-features/package-lock.json @@ -14,12 +14,12 @@ "highlight.js": "^11.8.0", "markdown-it": "^12.3.2", "markdown-it-front-matter": "^0.2.4", - "morphdom": "^2.6.1", + "morphdom": "^2.7.4", "picomatch": "^2.3.1", "punycode": "^2.3.1", "vscode-languageclient": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.11", - "vscode-markdown-languageserver": "^0.5.0-alpha.9", + "vscode-markdown-languageserver": "^0.5.0-alpha.10", "vscode-uri": "^3.0.3" }, "devDependencies": { @@ -275,7 +275,8 @@ "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -305,6 +306,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -320,6 +322,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -331,6 +334,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -349,12 +353,14 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -375,9 +381,10 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -391,6 +398,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -402,6 +410,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", "bin": { "he": "bin/he" } @@ -501,14 +510,16 @@ } }, "node_modules/morphdom": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.6.1.tgz", - "integrity": "sha512-Y8YRbAEP3eKykroIBWrjcfMw7mmwJfjhqdpSvoqinu8Y702nAwikpXcNFDiIkyvfCLxLM9Wu95RZqo4a9jFBaA==" + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.4.tgz", + "integrity": "sha512-ATTbWMgGa+FaMU3FhnFYB6WgulCqwf6opOll4CBzmVDTLvPMmUPrEv8CudmLPK0MESa64+6B89fWOxP3+YIlxQ==", + "license": "MIT" }, "node_modules/node-html-parser": { "version": "6.1.13", "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "license": "MIT", "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" @@ -518,6 +529,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, @@ -637,15 +649,16 @@ "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, "node_modules/vscode-markdown-languageserver": { - "version": "0.5.0-alpha.9", - "resolved": "https://registry.npmjs.org/vscode-markdown-languageserver/-/vscode-markdown-languageserver-0.5.0-alpha.9.tgz", - "integrity": "sha512-60jiPHgkstgkyODCN8qCJ4xAOrP/EoKyISmEAcJ7ILT5k2kAJF9JFEl3LvVZ+11HGGMJ2lm1L+lT2/JHvu5Pgg==", + "version": "0.5.0-alpha.10", + "resolved": "https://registry.npmjs.org/vscode-markdown-languageserver/-/vscode-markdown-languageserver-0.5.0-alpha.10.tgz", + "integrity": "sha512-e/Vzd2M34CvmkFYAF+MVh/mF2jmZQ8lQoxMB9V14JUs5UXbyJO2avTv6XF3GEB0EfHlQQ7qVOlCRghzPyskB8A==", + "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.11", "vscode-languageserver": "^8.1.0", "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3", - "vscode-markdown-languageservice": "^0.5.0-alpha.8", + "vscode-markdown-languageservice": "^0.5.0-alpha.9", "vscode-uri": "^3.0.7" }, "engines": { @@ -658,9 +671,10 @@ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" }, "node_modules/vscode-markdown-languageserver/node_modules/vscode-markdown-languageservice": { - "version": "0.5.0-alpha.8", - "resolved": "https://registry.npmjs.org/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0-alpha.8.tgz", - "integrity": "sha512-b2NgVMZvzI/7hRL32Kcu9neAAPFQzkcf/Fqwlxbz9p1/Q7aIorGACOGGo00s72AJtwjkCJ29eVJwUlFMFbPKqA==", + "version": "0.5.0-alpha.9", + "resolved": "https://registry.npmjs.org/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0-alpha.9.tgz", + "integrity": "sha512-OrE8homBOuXX9FOUhqRXgx/Iw0qA94yj3FBRSMztn8VveeO1Y0Eqej/9HBb5ga4sYdlFtQRIZ19lie37TsI+cQ==", + "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.10", "node-html-parser": "^6.1.5", @@ -676,7 +690,8 @@ "node_modules/vscode-markdown-languageserver/node_modules/vscode-markdown-languageservice/node_modules/@vscode/l10n": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.10.tgz", - "integrity": "sha512-E1OCmDcDWa0Ya7vtSjp/XfHFGqYJfh+YPC1RkATU71fTac+j1JjCcB3qwSzmlKAighx2WxhLlfhS0RwAN++PFQ==" + "integrity": "sha512-E1OCmDcDWa0Ya7vtSjp/XfHFGqYJfh+YPC1RkATU71fTac+j1JjCcB3qwSzmlKAighx2WxhLlfhS0RwAN++PFQ==", + "license": "MIT" }, "node_modules/vscode-markdown-languageservice": { "version": "0.3.0-alpha.3", diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 51b9f80e..5d76a322 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -768,12 +768,12 @@ "highlight.js": "^11.8.0", "markdown-it": "^12.3.2", "markdown-it-front-matter": "^0.2.4", - "morphdom": "^2.6.1", + "morphdom": "^2.7.4", "picomatch": "^2.3.1", "punycode": "^2.3.1", "vscode-languageclient": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.11", - "vscode-markdown-languageserver": "^0.5.0-alpha.9", + "vscode-markdown-languageserver": "^0.5.0-alpha.10", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts index 08c6759f..6c70e58e 100644 --- a/extensions/markdown-language-features/preview-src/index.ts +++ b/extensions/markdown-language-features/preview-src/index.ts @@ -68,7 +68,14 @@ onceDocumentLoaded(() => { getRawData('data-initial-md-content'), 'text/html' ); - document.body.appendChild(markDownHtml.body); + + const newElements = [...markDownHtml.body.children]; + document.body.append(...newElements); + for (const el of newElements) { + if (el instanceof HTMLElement) { + domEval(el); + } + } // Restore const scrollProgress = state.scrollProgress; @@ -177,6 +184,17 @@ async function copyImage(image: HTMLImageElement, retries = 5) { })]); } catch (e) { console.error(e); + const selection = window.getSelection(); + if (!selection) { + await navigator.clipboard.writeText(image.getAttribute('data-src') ?? image.src); + return; + } + selection.removeAllRanges(); + const range = document.createRange(); + range.selectNode(image); + selection.addRange(range); + document.execCommand('copy'); + selection.removeAllRanges(); } } @@ -216,46 +234,11 @@ window.addEventListener('message', async event => { } if (data.source !== documentResource) { - root.replaceWith(newContent.querySelector('.markdown-body')!); documentResource = data.source; + const newBody = newContent.querySelector('.markdown-body')!; + root.replaceWith(newBody); + domEval(newBody); } else { - const skippedAttrs = [ - 'open', // for details - ]; - - // Compare two elements but some elements - const areEqual = (a: Element, b: Element): boolean => { - if (a.isEqualNode(b)) { - return true; - } - - if (a.tagName !== b.tagName || a.textContent !== b.textContent) { - return false; - } - - const aAttrs = [...a.attributes].filter(attr => !skippedAttrs.includes(attr.name)); - const bAttrs = [...b.attributes].filter(attr => !skippedAttrs.includes(attr.name)); - if (aAttrs.length !== bAttrs.length) { - return false; - } - - for (let i = 0; i < aAttrs.length; ++i) { - const aAttr = aAttrs[i]; - const bAttr = bAttrs[i]; - if (aAttr.name !== bAttr.name) { - return false; - } - if (aAttr.value !== bAttr.value && aAttr.name !== 'data-line') { - return false; - } - } - - const aChildren = Array.from(a.children); - const bChildren = Array.from(b.children); - - return aChildren.length === bChildren.length && aChildren.every((x, i) => areEqual(x, bChildren[i])); - }; - const newRoot = newContent.querySelector('.markdown-body')!; // Move styles to head @@ -265,10 +248,11 @@ window.addEventListener('message', async event => { style.remove(); } newRoot.prepend(...styles); + morphdom(root, newRoot, { childrenOnly: true, - onBeforeElUpdated: (fromEl, toEl) => { - if (areEqual(fromEl, toEl)) { + onBeforeElUpdated: (fromEl: Element, toEl: Element) => { + if (areNodesEqual(fromEl, toEl)) { // areEqual doesn't look at `data-line` so copy those over manually const fromLines = fromEl.querySelectorAll('[data-line]'); const toLines = toEl.querySelectorAll('[data-line]'); @@ -294,8 +278,14 @@ window.addEventListener('message', async event => { } return true; + }, + addChild: (parentNode: Node, childNode: Node) => { + parentNode.appendChild(childNode); + if (childNode instanceof HTMLElement) { + domEval(childNode); + } } - }); + } as any); } ++documentVersion; @@ -383,3 +373,72 @@ function updateScrollProgress() { vscode.setState(state); } + +/** + * Compares two nodes for morphdom to see if they are equal. + * + * This skips some attributes that should not cause equality to fail. + */ +function areNodesEqual(a: Element, b: Element): boolean { + const skippedAttrs = [ + 'open', // for details + ]; + + if (a.isEqualNode(b)) { + return true; + } + + if (a.tagName !== b.tagName || a.textContent !== b.textContent) { + return false; + } + + const aAttrs = [...a.attributes].filter(attr => !skippedAttrs.includes(attr.name)); + const bAttrs = [...b.attributes].filter(attr => !skippedAttrs.includes(attr.name)); + if (aAttrs.length !== bAttrs.length) { + return false; + } + + for (let i = 0; i < aAttrs.length; ++i) { + const aAttr = aAttrs[i]; + const bAttr = bAttrs[i]; + if (aAttr.name !== bAttr.name) { + return false; + } + if (aAttr.value !== bAttr.value && aAttr.name !== 'data-line') { + return false; + } + } + + const aChildren = Array.from(a.children); + const bChildren = Array.from(b.children); + + return aChildren.length === bChildren.length && aChildren.every((x, i) => areNodesEqual(x, bChildren[i])); +} + + +function domEval(el: Element): void { + const preservedScriptAttributes: (keyof HTMLScriptElement)[] = [ + 'type', 'src', 'nonce', 'noModule', 'async', + ]; + + const scriptNodes = el.tagName === 'SCRIPT' ? [el] : Array.from(el.getElementsByTagName('script')); + + for (const node of scriptNodes) { + if (!(node instanceof HTMLElement)) { + continue; + } + + const scriptTag = document.createElement('script'); + const trustedScript = node.innerText; + scriptTag.text = trustedScript as string; + for (const key of preservedScriptAttributes) { + const val = node.getAttribute && node.getAttribute(key); + if (val) { + scriptTag.setAttribute(key, val as any); + } + } + + node.insertAdjacentElement('afterend', scriptTag); + node.remove(); + } +} diff --git a/extensions/microsoft-authentication/package-lock.json b/extensions/microsoft-authentication/package-lock.json index 4aae40c5..5024815f 100644 --- a/extensions/microsoft-authentication/package-lock.json +++ b/extensions/microsoft-authentication/package-lock.json @@ -71,9 +71,9 @@ } }, "node_modules/@azure/msal-node-runtime": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.17.1.tgz", - "integrity": "sha512-qAfTg+iGJsg+XvD9nmknI63+XuoX32oT+SX4wJdFz7CS6ETVpSHoroHVaUmsTU1H7H0+q1/ZkP988gzPRMYRsg==", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.18.1.tgz", + "integrity": "sha512-vaUkpSiXD33/iDyZt1VZDEyOxvlNMT5o9D4ruIqkUmULyKgUik0y86DK2dsqZql/LU04T5siuq1AMTus+15SvA==", "hasInstallScript": true, "license": "MIT" }, diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 4170a778..f9eb8198 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -115,23 +115,6 @@ "tags": [ "onExP" ] - }, - "microsoft-authentication.clientIdVersion": { - "type": "string", - "default": "v1", - "enum": [ - "v2", - "v1" - ], - "enumDescriptions": [ - "%microsoft-authentication.clientIdVersion.enumDescriptions.v2%", - "%microsoft-authentication.clientIdVersion.enumDescriptions.v1%" - ], - "markdownDescription": "%microsoft-authentication.clientIdVersion.description%", - "tags": [ - "onExP", - "experimental" - ] } } } @@ -162,6 +145,9 @@ "keytar": "file:./packageMocks/keytar", "vscode-tas-client": "^0.1.84" }, + "overrides": { + "@azure/msal-node-runtime": "^0.18.1" + }, "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" diff --git a/extensions/microsoft-authentication/package.nls.json b/extensions/microsoft-authentication/package.nls.json index ece95ac7..c8e0189c 100644 --- a/extensions/microsoft-authentication/package.nls.json +++ b/extensions/microsoft-authentication/package.nls.json @@ -12,9 +12,6 @@ }, "microsoft-authentication.implementation.enumDescriptions.msal": "Use the Microsoft Authentication Library (MSAL) to sign in with a Microsoft account.", "microsoft-authentication.implementation.enumDescriptions.classic": "(deprecated) Use the classic authentication flow to sign in with a Microsoft account.", - "microsoft-authentication.clientIdVersion.description": "The version of the Microsoft Account client ID to use for signing in with a Microsoft account. Only change this if you have been asked to. The default is `v1`.", - "microsoft-authentication.clientIdVersion.enumDescriptions.v1": "Use the v1 Microsoft Account client ID to sign in with a Microsoft account.", - "microsoft-authentication.clientIdVersion.enumDescriptions.v2": "Use the v2 Microsoft Account client ID to sign in with a Microsoft account.", "microsoft-sovereign-cloud.environment.description": { "message": "The Sovereign Cloud to use for authentication. If you select `custom`, you must also set the `#microsoft-sovereign-cloud.customEnvironment#` setting.", "comment": [ diff --git a/extensions/microsoft-authentication/src/common/accountAccess.ts b/extensions/microsoft-authentication/src/common/accountAccess.ts index a8fdeefe..27d56b0b 100644 --- a/extensions/microsoft-authentication/src/common/accountAccess.ts +++ b/extensions/microsoft-authentication/src/common/accountAccess.ts @@ -3,35 +3,52 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, SecretStorage } from 'vscode'; +import { Disposable, Event, EventEmitter, LogOutputChannel, SecretStorage } from 'vscode'; import { AccountInfo } from '@azure/msal-node'; -interface IAccountAccess { +export interface IAccountAccess { onDidAccountAccessChange: Event; isAllowedAccess(account: AccountInfo): boolean; - setAllowedAccess(account: AccountInfo, allowed: boolean): void; + setAllowedAccess(account: AccountInfo, allowed: boolean): Promise; } -export class ScopedAccountAccess implements IAccountAccess { +export class ScopedAccountAccess implements IAccountAccess, Disposable { private readonly _onDidAccountAccessChangeEmitter = new EventEmitter(); readonly onDidAccountAccessChange = this._onDidAccountAccessChangeEmitter.event; - private readonly _accountAccessSecretStorage: AccountAccessSecretStorage; - private value = new Array(); - constructor( - private readonly _secretStorage: SecretStorage, - private readonly _cloudName: string, - private readonly _clientId: string, - private readonly _authority: string + private readonly _disposable: Disposable; + + private constructor( + private readonly _accountAccessSecretStorage: IAccountAccessSecretStorage, + disposables: Disposable[] = [] ) { - this._accountAccessSecretStorage = new AccountAccessSecretStorage(this._secretStorage, this._cloudName, this._clientId, this._authority); - this._accountAccessSecretStorage.onDidChange(() => this.update()); + this._disposable = Disposable.from( + ...disposables, + this._onDidAccountAccessChangeEmitter, + this._accountAccessSecretStorage.onDidChange(() => this.update()) + ); } - initialize() { - return this.update(); + static async create( + secretStorage: SecretStorage, + cloudName: string, + logger: LogOutputChannel, + migrations: { clientId: string; authority: string }[] | undefined, + ): Promise { + const storage = await AccountAccessSecretStorage.create(secretStorage, cloudName, logger, migrations); + const access = new ScopedAccountAccess(storage, [storage]); + await access.initialize(); + return access; + } + + dispose() { + this._disposable.dispose(); + } + + private async initialize(): Promise { + await this.update(); } isAllowedAccess(account: AccountInfo): boolean { @@ -60,19 +77,26 @@ export class ScopedAccountAccess implements IAccountAccess { } } -export class AccountAccessSecretStorage { +interface IAccountAccessSecretStorage { + get(): Promise; + store(value: string[]): Thenable; + delete(): Thenable; + onDidChange: Event; +} + +class AccountAccessSecretStorage implements IAccountAccessSecretStorage, Disposable { private _disposable: Disposable; - private readonly _onDidChangeEmitter = new EventEmitter; + private readonly _onDidChangeEmitter = new EventEmitter(); readonly onDidChange: Event = this._onDidChangeEmitter.event; - private readonly _key = `accounts-${this._cloudName}-${this._clientId}-${this._authority}`; + private readonly _key = `accounts-${this._cloudName}`; - constructor( + private constructor( private readonly _secretStorage: SecretStorage, private readonly _cloudName: string, - private readonly _clientId: string, - private readonly _authority: string + private readonly _logger: LogOutputChannel, + private readonly _migrations?: { clientId: string; authority: string }[], ) { this._disposable = Disposable.from( this._onDidChangeEmitter, @@ -84,6 +108,48 @@ export class AccountAccessSecretStorage { ); } + static async create( + secretStorage: SecretStorage, + cloudName: string, + logger: LogOutputChannel, + migrations?: { clientId: string; authority: string }[], + ): Promise { + const storage = new AccountAccessSecretStorage(secretStorage, cloudName, logger, migrations); + await storage.initialize(); + return storage; + } + + /** + * TODO: Remove this method after a release with the migration + */ + private async initialize(): Promise { + if (!this._migrations) { + return; + } + const current = await this.get(); + // If the secret storage already has the new key, we have already run the migration + if (current) { + return; + } + try { + const allValues = new Set(); + for (const { clientId, authority } of this._migrations) { + const oldKey = `accounts-${this._cloudName}-${clientId}-${authority}`; + const value = await this._secretStorage.get(oldKey); + if (value) { + const parsed = JSON.parse(value) as string[]; + parsed.forEach(v => allValues.add(v)); + } + } + if (allValues.size > 0) { + await this.store(Array.from(allValues)); + } + } catch (e) { + // Migration is best effort + this._logger.error(`Failed to migrate account access secret storage: ${e}`); + } + } + async get(): Promise { const value = await this._secretStorage.get(this._key); if (!value) { diff --git a/extensions/microsoft-authentication/src/common/cachePlugin.ts b/extensions/microsoft-authentication/src/common/cachePlugin.ts index 91b4f0ee..b87fdb78 100644 --- a/extensions/microsoft-authentication/src/common/cachePlugin.ts +++ b/extensions/microsoft-authentication/src/common/cachePlugin.ts @@ -6,7 +6,7 @@ import { ICachePlugin, TokenCacheContext } from '@azure/msal-node'; import { Disposable, EventEmitter, SecretStorage } from 'vscode'; -export class SecretStorageCachePlugin implements ICachePlugin { +export class SecretStorageCachePlugin implements ICachePlugin, Disposable { private readonly _onDidChange: EventEmitter = new EventEmitter(); readonly onDidChange = this._onDidChange.event; diff --git a/extensions/microsoft-authentication/src/common/publicClientCache.ts b/extensions/microsoft-authentication/src/common/publicClientCache.ts index 925a4d1a..acc8ba0d 100644 --- a/extensions/microsoft-authentication/src/common/publicClientCache.ts +++ b/extensions/microsoft-authentication/src/common/publicClientCache.ts @@ -2,23 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { AccountInfo, AuthenticationResult, InteractiveRequest, SilentFlowRequest } from '@azure/msal-node'; +import type { AccountInfo, AuthenticationResult, InteractiveRequest, RefreshTokenRequest, SilentFlowRequest } from '@azure/msal-node'; import type { Disposable, Event } from 'vscode'; -export interface ICachedPublicClientApplication extends Disposable { - initialize(): Promise; +export interface ICachedPublicClientApplication { onDidAccountsChange: Event<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; onDidRemoveLastAccount: Event; acquireTokenSilent(request: SilentFlowRequest): Promise; acquireTokenInteractive(request: InteractiveRequest): Promise; + acquireTokenByRefreshToken(request: RefreshTokenRequest): Promise; removeAccount(account: AccountInfo): Promise; accounts: AccountInfo[]; clientId: string; - authority: string; } export interface ICachedPublicClientApplicationManager { onDidAccountsChange: Event<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; - getOrCreate(clientId: string, authority: string): Promise; + getOrCreate(clientId: string, refreshTokensToMigrate?: string[]): Promise; getAll(): ICachedPublicClientApplication[]; } diff --git a/extensions/microsoft-authentication/src/common/scopeData.ts b/extensions/microsoft-authentication/src/common/scopeData.ts index a43f2c43..88a0aad6 100644 --- a/extensions/microsoft-authentication/src/common/scopeData.ts +++ b/extensions/microsoft-authentication/src/common/scopeData.ts @@ -3,21 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace } from 'vscode'; - -const DEFAULT_CLIENT_ID_V1 = 'aebc6443-996d-45c2-90f0-388ff96faa56'; -const DEFAULT_TENANT_V1 = 'organizations'; -const DEFAULT_CLIENT_ID_V2 = 'c27c220f-ce2f-4904-927d-333864217eeb'; -const DEFAULT_TENANT_V2 = 'common'; +const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56'; +const DEFAULT_TENANT = 'organizations'; const OIDC_SCOPES = ['openid', 'email', 'profile', 'offline_access']; const GRAPH_TACK_ON_SCOPE = 'User.Read'; export class ScopeData { - - private readonly _defaultClientId: string; - private readonly _defaultTenant: string; - /** * The full list of scopes including: * * the original scopes passed to the constructor @@ -42,47 +34,57 @@ export class ScopeData { readonly clientId: string; /** - * The tenant ID to use for the token request. This is the value of the `VSCODE_TENANT:...` scope if present, otherwise the default tenant ID. + * The tenant ID or `organizations`, `common`, `consumers` to use for the token request. This is the value of the `VSCODE_TENANT:...` scope if present, otherwise it's the default. */ readonly tenant: string; - constructor(readonly originalScopes: readonly string[] = []) { - if (workspace.getConfiguration('microsoft-authentication').get<'v1' | 'v2'>('clientIdVersion') === 'v2') { - this._defaultClientId = DEFAULT_CLIENT_ID_V2; - this._defaultTenant = DEFAULT_TENANT_V2; - } else { - this._defaultClientId = DEFAULT_CLIENT_ID_V1; - this._defaultTenant = DEFAULT_TENANT_V1; - } + /** + * The tenant ID to use for the token request. This will only ever be a GUID if one was specified via the `VSCODE_TENANT:...` scope, otherwise undefined. + */ + readonly tenantId: string | undefined; + constructor(readonly originalScopes: readonly string[] = []) { const modifiedScopes = [...originalScopes]; modifiedScopes.sort(); this.allScopes = modifiedScopes; this.scopeStr = modifiedScopes.join(' '); this.scopesToSend = this.getScopesToSend(modifiedScopes); this.clientId = this.getClientId(this.allScopes); - this.tenant = this.getTenantId(this.allScopes); + this.tenant = this.getTenant(this.allScopes); + this.tenantId = this.getTenantId(this.tenant); } - private getClientId(scopes: string[]) { + private getClientId(scopes: string[]): string { return scopes.reduce((prev, current) => { if (current.startsWith('VSCODE_CLIENT_ID:')) { return current.split('VSCODE_CLIENT_ID:')[1]; } return prev; - }, undefined) ?? this._defaultClientId; + }, undefined) ?? DEFAULT_CLIENT_ID; } - private getTenantId(scopes: string[]) { + private getTenant(scopes: string[]): string { return scopes.reduce((prev, current) => { if (current.startsWith('VSCODE_TENANT:')) { return current.split('VSCODE_TENANT:')[1]; } return prev; - }, undefined) ?? this._defaultTenant; + }, undefined) ?? DEFAULT_TENANT; } - private getScopesToSend(scopes: string[]) { + private getTenantId(tenant: string): string | undefined { + switch (tenant) { + case 'organizations': + case 'common': + case 'consumers': + // These are not valid tenant IDs, so we return undefined + return undefined; + default: + return this.tenant; + } + } + + private getScopesToSend(scopes: string[]): string[] { const scopesToSend = scopes.filter(s => !s.startsWith('VSCODE_')); const set = new Set(scopesToSend); diff --git a/extensions/microsoft-authentication/src/common/test/scopeData.test.ts b/extensions/microsoft-authentication/src/common/test/scopeData.test.ts index 9250d7ce..4c70e4fd 100644 --- a/extensions/microsoft-authentication/src/common/test/scopeData.test.ts +++ b/extensions/microsoft-authentication/src/common/test/scopeData.test.ts @@ -56,4 +56,21 @@ suite('ScopeData', () => { const scopeData = new ScopeData(['custom_scope', 'VSCODE_TENANT:some_tenant']); assert.strictEqual(scopeData.tenant, 'some_tenant'); }); + + test('should have tenantId be undefined if no VSCODE_TENANT scope is present', () => { + const scopeData = new ScopeData(['custom_scope']); + assert.strictEqual(scopeData.tenantId, undefined); + }); + + test('should have tenantId be undefined if typical tenant values are present', () => { + for (const element of ['common', 'organizations', 'consumers']) { + const scopeData = new ScopeData(['custom_scope', `VSCODE_TENANT:${element}`]); + assert.strictEqual(scopeData.tenantId, undefined); + } + }); + + test('should have tenantId be the value of VSCODE_TENANT scope if set to a specific value', () => { + const scopeData = new ScopeData(['custom_scope', 'VSCODE_TENANT:some_guid']); + assert.strictEqual(scopeData.tenantId, 'some_guid'); + }); }); diff --git a/extensions/microsoft-authentication/src/extensionV2.ts b/extensions/microsoft-authentication/src/extensionV2.ts index 9610af37..978603ad 100644 --- a/extensions/microsoft-authentication/src/extensionV2.ts +++ b/extensions/microsoft-authentication/src/extensionV2.ts @@ -49,14 +49,13 @@ async function initMicrosoftSovereignCloudAuthProvider( return undefined; } - const authProvider = new MsalAuthProvider( + const authProvider = await MsalAuthProvider.create( context, new MicrosoftSovereignCloudAuthenticationTelemetryReporter(context.extension.packageJSON.aiKey), window.createOutputChannel(l10n.t('Microsoft Sovereign Cloud Authentication'), { log: true }), uriHandler, env ); - await authProvider.initialize(); const disposable = authentication.registerAuthenticationProvider( 'microsoft-sovereign-cloud', authProviderName, @@ -70,13 +69,12 @@ async function initMicrosoftSovereignCloudAuthProvider( export async function activate(context: ExtensionContext, mainTelemetryReporter: MicrosoftAuthenticationTelemetryReporter) { const uriHandler = new UriEventHandler(); context.subscriptions.push(uriHandler); - const authProvider = new MsalAuthProvider( + const authProvider = await MsalAuthProvider.create( context, mainTelemetryReporter, Logger, uriHandler ); - await authProvider.initialize(); context.subscriptions.push(authentication.registerAuthenticationProvider( 'microsoft', 'Microsoft', diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index 40000e08..a26008fb 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -7,7 +7,7 @@ import { AuthenticationGetSessionOptions, AuthenticationProvider, Authentication import { Environment } from '@azure/ms-rest-azure-env'; import { CachedPublicClientApplicationManager } from './publicClientCache'; import { UriEventHandler } from '../UriEventHandler'; -import { ICachedPublicClientApplication } from '../common/publicClientCache'; +import { ICachedPublicClientApplication, ICachedPublicClientApplicationManager } from '../common/publicClientCache'; import { MicrosoftAccountType, MicrosoftAuthenticationTelemetryReporter } from '../common/telemetryReporter'; import { ScopeData } from '../common/scopeData'; import { EventBufferer } from '../common/event'; @@ -22,7 +22,6 @@ const MSA_PASSTHRU_TID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a'; export class MsalAuthProvider implements AuthenticationProvider { private readonly _disposables: { dispose(): void }[]; - private readonly _publicClientManager: CachedPublicClientApplicationManager; private readonly _eventBufferer = new EventBufferer(); /** @@ -43,20 +42,15 @@ export class MsalAuthProvider implements AuthenticationProvider { */ onDidChangeSessions = this._onDidChangeSessionsEmitter.event; - constructor( + private constructor( private readonly _context: ExtensionContext, private readonly _telemetryReporter: MicrosoftAuthenticationTelemetryReporter, private readonly _logger: LogOutputChannel, private readonly _uriHandler: UriEventHandler, + private readonly _publicClientManager: ICachedPublicClientApplicationManager, private readonly _env: Environment = Environment.AzureCloud ) { this._disposables = _context.subscriptions; - this._publicClientManager = new CachedPublicClientApplicationManager( - _context.globalState, - _context.secrets, - this._logger, - this._env.name - ); const accountChangeEvent = this._eventBufferer.wrapEvent( this._publicClientManager.onDidAccountsChange, (last, newEvent) => { @@ -81,11 +75,24 @@ export class MsalAuthProvider implements AuthenticationProvider { )(e => this._handleAccountChange(e)); this._disposables.push( this._onDidChangeSessionsEmitter, - this._publicClientManager, accountChangeEvent ); } + static async create( + context: ExtensionContext, + telemetryReporter: MicrosoftAuthenticationTelemetryReporter, + logger: LogOutputChannel, + uriHandler: UriEventHandler, + env: Environment = Environment.AzureCloud + ): Promise { + const publicClientManager = await CachedPublicClientApplicationManager.create(context.secrets, logger, env.name); + context.subscriptions.push(publicClientManager); + const authProvider = new MsalAuthProvider(context, telemetryReporter, logger, uriHandler, publicClientManager, env); + await authProvider.initialize(); + return authProvider; + } + /** * Migrate sessions from the old secret storage to MSAL. * TODO: MSAL Migration. Remove this when we remove the old flow. @@ -109,14 +116,12 @@ export class MsalAuthProvider implements AuthenticationProvider { clientTenantMap.get(key)!.refreshTokens.push(session.refreshToken); } - for (const { clientId, tenant, refreshTokens } of clientTenantMap.values()) { - await this.getOrCreatePublicClientApplication(clientId, tenant, refreshTokens); + for (const { clientId, refreshTokens } of clientTenantMap.values()) { + await this._publicClientManager.getOrCreate(clientId, refreshTokens); } } - async initialize(): Promise { - await this._eventBufferer.bufferEventsAsync(() => this._publicClientManager.initialize()); - + private async initialize(): Promise { if (!this._context.globalState.get('msalMigration', false)) { await this._migrateSessions(); } @@ -173,8 +178,8 @@ export class MsalAuthProvider implements AuthenticationProvider { return allSessions; } - const cachedPca = await this.getOrCreatePublicClientApplication(scopeData.clientId, scopeData.tenant); - const sessions = await this.getAllSessionsForPca(cachedPca, scopeData.originalScopes, scopeData.scopesToSend, options?.account); + const cachedPca = await this._publicClientManager.getOrCreate(scopeData.clientId); + const sessions = await this.getAllSessionsForPca(cachedPca, scopeData, options?.account); this._logger.info(`[getSessions] [${scopeData.scopeStr}] returned ${sessions.length} session(s)`); return sessions; @@ -185,7 +190,7 @@ export class MsalAuthProvider implements AuthenticationProvider { // Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead. this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'starting'); - const cachedPca = await this.getOrCreatePublicClientApplication(scopeData.clientId, scopeData.tenant); + const cachedPca = await this._publicClientManager.getOrCreate(scopeData.clientId); // Used for showing a friendlier message to the user when the explicitly cancel a flow. let userCancelled: boolean | undefined; @@ -211,6 +216,7 @@ export class MsalAuthProvider implements AuthenticationProvider { : ExtensionHost.WebWorker, }); + const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString(); let lastError: Error | undefined; for (const flow of flows) { if (flow !== flows[0]) { @@ -223,6 +229,7 @@ export class MsalAuthProvider implements AuthenticationProvider { try { const result = await flow.trigger({ cachedPca, + authority, scopes: scopeData.scopesToSend, loginHint: options.account?.label, windowHandle: window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined, @@ -260,7 +267,7 @@ export class MsalAuthProvider implements AuthenticationProvider { if (account.homeAccountId === sessionId) { this._telemetryReporter.sendLogoutEvent(); promises.push(cachedPca.removeAccount(account)); - this._logger.info(`[removeSession] [${sessionId}] [${cachedPca.clientId}] [${cachedPca.authority}] removing session...`); + this._logger.info(`[removeSession] [${sessionId}] [${cachedPca.clientId}] removing session...`); } } } @@ -281,26 +288,69 @@ export class MsalAuthProvider implements AuthenticationProvider { //#endregion - private async getOrCreatePublicClientApplication(clientId: string, tenant: string, refreshTokensToMigrate?: string[]): Promise { - const authority = new URL(tenant, this._env.activeDirectoryEndpointUrl).toString(); - return await this._publicClientManager.getOrCreate(clientId, authority, refreshTokensToMigrate); - } - private async getAllSessionsForPca( cachedPca: ICachedPublicClientApplication, - originalScopes: readonly string[], - scopesToSend: string[], + scopeData: ScopeData, accountFilter?: AuthenticationSessionAccountInformation ): Promise { - const accounts = accountFilter + let filteredAccounts = accountFilter ? cachedPca.accounts.filter(a => a.homeAccountId === accountFilter.id) : cachedPca.accounts; + + // Group accounts by homeAccountId + const accountGroups = new Map(); + for (const account of filteredAccounts) { + const existing = accountGroups.get(account.homeAccountId) || []; + existing.push(account); + accountGroups.set(account.homeAccountId, existing); + } + + // Filter to one account per homeAccountId + filteredAccounts = Array.from(accountGroups.values()).map(accounts => { + if (accounts.length === 1) { + return accounts[0]; + } + + // If we have a specific tenant to target, prefer that one + if (scopeData.tenantId) { + const matchingTenant = accounts.find(a => a.tenantId === scopeData.tenantId); + if (matchingTenant) { + return matchingTenant; + } + } + + // Otherwise prefer the home tenant + return accounts.find(a => a.tenantId === a.idTokenClaims?.tid) || accounts[0]; + }); + + const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString(); const sessions: AuthenticationSession[] = []; return this._eventBufferer.bufferEventsAsync(async () => { - for (const account of accounts) { + for (const account of filteredAccounts) { try { - const result = await cachedPca.acquireTokenSilent({ account, scopes: scopesToSend, redirectUri }); - sessions.push(this.sessionFromAuthenticationResult(result, originalScopes)); + let forceRefresh: true | undefined; + if (scopeData.tenantId) { + // If the tenants do not match, then we need to skip the cache + // to get a new token for the new tenant + if (account.tenantId !== scopeData.tenantId) { + forceRefresh = true; + } + } else { + // If we are requesting the home tenant and we don't yet have + // a token for the home tenant, we need to skip the cache + // to get a new token for the home tenant + if (account.tenantId !== account.idTokenClaims?.tid) { + forceRefresh = true; + } + } + const result = await cachedPca.acquireTokenSilent({ + account, + authority, + scopes: scopeData.scopesToSend, + redirectUri, + forceRefresh + }); + sessions.push(this.sessionFromAuthenticationResult(result, scopeData.originalScopes)); } catch (e) { // If we can't get a token silently, the account is probably in a bad state so we should skip it // MSAL will log this already, so we don't need to log it again diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index 8d081f1a..f0f4eb7b 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PublicClientApplication, AccountInfo, Configuration, SilentFlowRequest, AuthenticationResult, InteractiveRequest, LogLevel, RefreshTokenRequest } from '@azure/msal-node'; +import { PublicClientApplication, AccountInfo, SilentFlowRequest, AuthenticationResult, InteractiveRequest, LogLevel, RefreshTokenRequest } from '@azure/msal-node'; import { NativeBrokerPlugin } from '@azure/msal-node-extensions'; -import { Disposable, Memento, SecretStorage, LogOutputChannel, window, ProgressLocation, l10n, EventEmitter } from 'vscode'; +import { Disposable, SecretStorage, LogOutputChannel, window, ProgressLocation, l10n, EventEmitter } from 'vscode'; import { raceCancellationAndTimeoutError } from '../common/async'; import { SecretStorageCachePlugin } from '../common/cachePlugin'; import { MsalLoggerOptions } from '../common/loggerOptions'; import { ICachedPublicClientApplication } from '../common/publicClientCache'; -import { ScopedAccountAccess } from '../common/accountAccess'; +import { IAccountAccess } from '../common/accountAccess'; export class CachedPublicClientApplication implements ICachedPublicClientApplication { // Core properties @@ -23,11 +23,10 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica private readonly _secretStorageCachePlugin = new SecretStorageCachePlugin( this._secretStorage, // Include the prefix as a differentiator to other secrets - `pca:${JSON.stringify({ clientId: this._clientId, authority: this._authority })}` + `pca:${this._clientId}` ); // Broker properties - private readonly _accountAccess = new ScopedAccountAccess(this._secretStorage, this._cloudName, this._clientId, this._authority); private readonly _isBrokerAvailable: boolean; //#region Events @@ -40,24 +39,20 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica //#endregion - constructor( + private constructor( private readonly _clientId: string, - private readonly _authority: string, - private readonly _cloudName: string, - private readonly _globalMemento: Memento, private readonly _secretStorage: SecretStorage, - private readonly _logger: LogOutputChannel + private readonly _accountAccess: IAccountAccess, + private readonly _logger: LogOutputChannel, ) { - // TODO:@TylerLeonhardt clean up old use of memento. Remove this in an iteration - this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, undefined); const loggerOptions = new MsalLoggerOptions(_logger); const nativeBrokerPlugin = new NativeBrokerPlugin(); - this._isBrokerAvailable = nativeBrokerPlugin.isBrokerAvailable ?? false; + this._isBrokerAvailable = nativeBrokerPlugin.isBrokerAvailable; this._pca = new PublicClientApplication({ - auth: { clientId: _clientId, authority: _authority }, + auth: { clientId: _clientId }, system: { loggerOptions: { - correlationId: `${_clientId}] [${_authority}`, + correlationId: _clientId, loggerCallback: (level, message, containsPii) => loggerOptions.loggerCallback(level, message, containsPii), logLevel: LogLevel.Trace } @@ -68,18 +63,26 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica this._disposable = Disposable.from( this._registerOnSecretStorageChanged(), this._onDidAccountsChangeEmitter, - this._onDidRemoveLastAccountEmitter + this._onDidRemoveLastAccountEmitter, + this._secretStorageCachePlugin ); } get accounts(): AccountInfo[] { return this._accounts; } get clientId(): string { return this._clientId; } - get authority(): string { return this._authority; } - async initialize(): Promise { - if (this._isBrokerAvailable) { - await this._accountAccess.initialize(); - } + static async create( + clientId: string, + secretStorage: SecretStorage, + accountAccess: IAccountAccess, + logger: LogOutputChannel + ): Promise { + const app = new CachedPublicClientApplication(clientId, secretStorage, accountAccess, logger); + await app.initialize(); + return app; + } + + private async initialize(): Promise { await this._sequencer.queue(() => this._update()); } @@ -88,9 +91,9 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica } async acquireTokenSilent(request: SilentFlowRequest): Promise { - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] starting...`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] starting...`); let result = await this._sequencer.queue(() => this._pca.acquireTokenSilent(request)); - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got result`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] got result`); // Check expiration of id token and if it's 5min before expiration, force a refresh. // this is what MSAL does for access tokens already so we're just adding it for id tokens since we care about those. // NOTE: Once we stop depending on id tokens for some things we can remove all of this. @@ -101,13 +104,13 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica * 1000 // convert to milliseconds ); if (fiveMinutesBefore < new Date()) { - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is expired or about to expire. Forcing refresh...`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is expired or about to expire. Forcing refresh...`); const newRequest = this._isBrokerAvailable // HACK: Broker doesn't support forceRefresh so we need to pass in claims which will force a refresh ? { ...request, claims: '{ "id_token": {}}' } : { ...request, forceRefresh: true }; result = await this._sequencer.queue(() => this._pca.acquireTokenSilent(newRequest)); - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got forced result`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] got forced result`); } const newIdTokenExpirationInSecs = (result.idTokenClaims as { exp?: number }).exp; if (newIdTokenExpirationInSecs) { @@ -116,15 +119,15 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica * 1000 // convert to milliseconds ); if (fiveMinutesBefore < new Date()) { - this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); // HACK: Only for the Broker we try one more time with different claims to force a refresh. Why? We've seen the Broker caching tokens by the claims requested, thus // there has been a situation where both tokens are expired. if (this._isBrokerAvailable) { - this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] forcing refresh with different claims...`); + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] forcing refresh with different claims...`); const newRequest = { ...request, claims: '{ "access_token": {}}' }; result = await this._sequencer.queue(() => this._pca.acquireTokenSilent(newRequest)); - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] got forced result with different claims`); + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] got forced result with different claims`); const newIdTokenExpirationInSecs = (result.idTokenClaims as { exp?: number }).exp; if (newIdTokenExpirationInSecs) { const fiveMinutesBefore = new Date( @@ -132,7 +135,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica * 1000 // convert to milliseconds ); if (fiveMinutesBefore < new Date()) { - this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] id token is still expired.`); } } } @@ -140,15 +143,17 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica } } - if (result.account && !result.fromCache && this._verifyIfUsingBroker(result)) { - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}] [${request.account.username}] firing event due to change`); + if (!result.account) { + this._logger.error(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] no account found in result`); + } else if (!result.fromCache && this._verifyIfUsingBroker(result)) { + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}] [${request.account.username}] firing event due to change`); this._onDidAccountsChangeEmitter.fire({ added: [], changed: [result.account], deleted: [] }); } return result; } async acquireTokenInteractive(request: InteractiveRequest): Promise { - this._logger.debug(`[acquireTokenInteractive] [${this._clientId}] [${this._authority}] [${request.scopes?.join(' ')}] loopbackClientOverride: ${request.loopbackClient ? 'true' : 'false'}`); + this._logger.debug(`[acquireTokenInteractive] [${this._clientId}] [${request.authority}] [${request.scopes?.join(' ')}] loopbackClientOverride: ${request.loopbackClient ? 'true' : 'false'}`); return await window.withProgress( { location: ProgressLocation.Notification, @@ -180,9 +185,17 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica * @param request a {@link RefreshTokenRequest} object that contains the refresh token and other parameters. * @returns an {@link AuthenticationResult} object that contains the result of the token acquisition operation. */ - async acquireTokenByRefreshToken(request: RefreshTokenRequest) { - this._logger.debug(`[acquireTokenByRefreshToken] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}]`); - const result = await this._sequencer.queue(() => this._pca.acquireTokenByRefreshToken(request)); + async acquireTokenByRefreshToken(request: RefreshTokenRequest): Promise { + this._logger.debug(`[acquireTokenByRefreshToken] [${this._clientId}] [${request.authority}] [${request.scopes.join(' ')}]`); + const result = await this._sequencer.queue(async () => { + const result = await this._pca.acquireTokenByRefreshToken(request); + // Force an update so that the account cache is updated. + // TODO:@TylerLeonhardt The problem is, we use the sequencer for + // change events but we _don't_ use it for the accounts cache. + // We should probably use it for the accounts cache as well. + await this._update(); + return result; + }); if (result) { // this._setupRefresh(result); if (this._isBrokerAvailable && result.account) { @@ -213,7 +226,14 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica if (!result.fromNativeBroker) { return true; } - const key = result.account!.homeAccountId; + // The nativeAccountId is what the broker uses to differenciate all + // types of accounts. Even if the "account" is a duplicate of another because + // it's actaully a guest account in another tenant. + let key = result.account!.nativeAccountId; + if (!key) { + this._logger.error(`[verifyIfUsingBroker] [${this._clientId}] [${result.account!.username}] no nativeAccountId found. Using homeAccountId instead.`); + key = result.account!.homeAccountId; + } const lastSeen = this._lastSeen.get(key); const lastTimeAuthed = result.account!.idTokenClaims!.iat!; if (!lastSeen) { @@ -229,7 +249,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica private async _update() { const before = this._accounts; - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update before: ${before.length}`); + this._logger.debug(`[update] [${this._clientId}] CachedPublicClientApplication update before: ${before.length}`); // Clear in-memory cache so we know we're getting account data from the SecretStorage this._pca.clearCache(); let after = await this._pca.getAllAccounts(); @@ -237,7 +257,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica after = after.filter(a => this._accountAccess.isAllowedAccess(a)); } this._accounts = after; - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update after: ${after.length}`); + this._logger.debug(`[update] [${this._clientId}] CachedPublicClientApplication update after: ${after.length}`); const beforeSet = new Set(before.map(b => b.homeAccountId)); const afterSet = new Set(after.map(a => a.homeAccountId)); @@ -246,13 +266,13 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica const deleted = before.filter(b => !afterSet.has(b.homeAccountId)); if (added.length > 0 || deleted.length > 0) { this._onDidAccountsChangeEmitter.fire({ added, changed: [], deleted }); - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication accounts changed. added: ${added.length}, deleted: ${deleted.length}`); + this._logger.debug(`[update] [${this._clientId}] CachedPublicClientApplication accounts changed. added: ${added.length}, deleted: ${deleted.length}`); if (!after.length) { - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication final account deleted. Firing event.`); + this._logger.debug(`[update] [${this._clientId}] CachedPublicClientApplication final account deleted. Firing event.`); this._onDidRemoveLastAccountEmitter.fire(); } } - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update complete`); + this._logger.debug(`[update] [${this._clientId}] CachedPublicClientApplication update complete`); } } diff --git a/extensions/microsoft-authentication/src/node/flows.ts b/extensions/microsoft-authentication/src/node/flows.ts index c6f40a94..ac678d83 100644 --- a/extensions/microsoft-authentication/src/node/flows.ts +++ b/extensions/microsoft-authentication/src/node/flows.ts @@ -25,6 +25,7 @@ interface IMsalFlowOptions { interface IMsalFlowTriggerOptions { cachedPca: ICachedPublicClientApplication; + authority: string; scopes: string[]; loginHint?: string; windowHandle?: Buffer; @@ -45,11 +46,12 @@ class DefaultLoopbackFlow implements IMsalFlow { supportsWebWorkerExtensionHost: false }; - async trigger({ cachedPca, scopes, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise { + async trigger({ cachedPca, authority, scopes, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise { logger.info('Trying default msal flow...'); return await cachedPca.acquireTokenInteractive({ openBrowser: async (url: string) => { await env.openExternal(Uri.parse(url)); }, scopes, + authority, successTemplate: loopbackTemplate, errorTemplate: loopbackTemplate, loginHint, @@ -66,12 +68,13 @@ class UrlHandlerFlow implements IMsalFlow { supportsWebWorkerExtensionHost: false }; - async trigger({ cachedPca, scopes, loginHint, windowHandle, logger, uriHandler }: IMsalFlowTriggerOptions): Promise { + async trigger({ cachedPca, authority, scopes, loginHint, windowHandle, logger, uriHandler }: IMsalFlowTriggerOptions): Promise { logger.info('Trying protocol handler flow...'); const loopbackClient = new UriHandlerLoopbackClient(uriHandler, redirectUri, logger); return await cachedPca.acquireTokenInteractive({ openBrowser: (url: string) => loopbackClient.openBrowser(url), scopes, + authority, loopbackClient, loginHint, prompt: loginHint ? undefined : 'select_account', diff --git a/extensions/microsoft-authentication/src/node/publicClientCache.ts b/extensions/microsoft-authentication/src/node/publicClientCache.ts index f4f19ff8..16ccb803 100644 --- a/extensions/microsoft-authentication/src/node/publicClientCache.ts +++ b/extensions/microsoft-authentication/src/node/publicClientCache.ts @@ -7,6 +7,7 @@ import { AccountInfo } from '@azure/msal-node'; import { SecretStorage, LogOutputChannel, Disposable, EventEmitter, Memento, Event } from 'vscode'; import { ICachedPublicClientApplication, ICachedPublicClientApplicationManager } from '../common/publicClientCache'; import { CachedPublicClientApplication } from './cachedPublicClientApplication'; +import { IAccountAccess, ScopedAccountAccess } from '../common/accountAccess'; export interface IPublicClientApplicationInfo { clientId: string; @@ -14,56 +15,68 @@ export interface IPublicClientApplicationInfo { } export class CachedPublicClientApplicationManager implements ICachedPublicClientApplicationManager { - // The key is the clientId and authority JSON stringified - private readonly _pcas = new Map(); + // The key is the clientId + private readonly _pcas = new Map(); private readonly _pcaDisposables = new Map(); private _disposable: Disposable; - private _pcasSecretStorage: PublicClientApplicationsSecretStorage; private readonly _onDidAccountsChangeEmitter = new EventEmitter<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>(); readonly onDidAccountsChange = this._onDidAccountsChangeEmitter.event; - constructor( - private readonly _globalMemento: Memento, + private constructor( + private readonly _pcasSecretStorage: IPublicClientApplicationSecretStorage, + private readonly _accountAccess: IAccountAccess, private readonly _secretStorage: SecretStorage, private readonly _logger: LogOutputChannel, - private readonly _cloudName: string + disposables: Disposable[] ) { - this._pcasSecretStorage = new PublicClientApplicationsSecretStorage(_secretStorage, _cloudName); this._disposable = Disposable.from( - this._pcasSecretStorage, + ...disposables, this._registerSecretStorageHandler(), this._onDidAccountsChangeEmitter ); } + static async create( + secretStorage: SecretStorage, + logger: LogOutputChannel, + cloudName: string + ): Promise { + const pcasSecretStorage = await PublicClientApplicationsSecretStorage.create(secretStorage, cloudName); + // TODO: Remove the migrations in a version + const migrations = await pcasSecretStorage.getOldValue(); + const accountAccess = await ScopedAccountAccess.create(secretStorage, cloudName, logger, migrations); + const manager = new CachedPublicClientApplicationManager(pcasSecretStorage, accountAccess, secretStorage, logger, [pcasSecretStorage, accountAccess]); + await manager.initialize(); + return manager; + } + private _registerSecretStorageHandler() { return this._pcasSecretStorage.onDidChange(() => this._handleSecretStorageChange()); } - async initialize() { + private async initialize() { this._logger.debug('[initialize] Initializing PublicClientApplicationManager'); - let keys: string[] | undefined; + let clientIds: string[] | undefined; try { - keys = await this._pcasSecretStorage.get(); + clientIds = await this._pcasSecretStorage.get(); } catch (e) { // data is corrupted this._logger.error('[initialize] Error initializing PublicClientApplicationManager:', e); await this._pcasSecretStorage.delete(); } - if (!keys) { + if (!clientIds) { return; } const promises = new Array>(); - for (const key of keys) { + for (const clientId of clientIds) { try { - const { clientId, authority } = JSON.parse(key) as IPublicClientApplicationInfo; // Load the PCA in memory - promises.push(this._doCreatePublicClientApplication(clientId, authority, key)); + promises.push(this._doCreatePublicClientApplication(clientId)); } catch (e) { - this._logger.error('[initialize] Error intitializing PCA:', key); + this._logger.error('[initialize] Error intitializing PCA:', clientId); } } @@ -75,11 +88,11 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient } else { if (!result.value.accounts.length) { pcasChanged = true; - const pcaKey = JSON.stringify({ clientId: result.value.clientId, authority: result.value.authority }); - this._pcaDisposables.get(pcaKey)?.dispose(); - this._pcaDisposables.delete(pcaKey); - this._pcas.delete(pcaKey); - this._logger.debug(`[initialize] [${result.value.clientId}] [${result.value.authority}] PCA disposed because it's empty.`); + const clientId = result.value.clientId; + this._pcaDisposables.get(clientId)?.dispose(); + this._pcaDisposables.delete(clientId); + this._pcas.delete(clientId); + this._logger.debug(`[initialize] [${clientId}] PCA disposed because it's empty.`); } } } @@ -94,43 +107,39 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient Disposable.from(...this._pcaDisposables.values()).dispose(); } - async getOrCreate(clientId: string, authority: string, refreshTokensToMigrate?: string[]): Promise { - // Use the clientId and authority as the key - const pcasKey = JSON.stringify({ clientId, authority }); - let pca = this._pcas.get(pcasKey); + async getOrCreate(clientId: string, refreshTokensToMigrate?: string[]): Promise { + let pca = this._pcas.get(clientId); if (pca) { - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PublicClientApplicationManager cache hit`); + this._logger.debug(`[getOrCreate] [${clientId}] PublicClientApplicationManager cache hit`); } else { - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PublicClientApplicationManager cache miss, creating new PCA...`); - pca = await this._doCreatePublicClientApplication(clientId, authority, pcasKey); + this._logger.debug(`[getOrCreate] [${clientId}] PublicClientApplicationManager cache miss, creating new PCA...`); + pca = await this._doCreatePublicClientApplication(clientId); await this._storePublicClientApplications(); - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PCA created.`); + this._logger.debug(`[getOrCreate] [${clientId}] PCA created.`); } // TODO: MSAL Migration. Remove this when we remove the old flow. if (refreshTokensToMigrate?.length) { - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] Migrating refresh tokens to PCA...`); + this._logger.debug(`[getOrCreate] [${clientId}] Migrating refresh tokens to PCA...`); for (const refreshToken of refreshTokensToMigrate) { try { // Use the refresh token to acquire a result. This will cache the refresh token for future operations. // The scopes don't matter here since we can create any token from the refresh token. const result = await pca.acquireTokenByRefreshToken({ refreshToken, forceCache: true, scopes: [] }); if (result?.account) { - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] Refresh token migrated to PCA.`); + this._logger.debug(`[getOrCreate] [${clientId}] Refresh token migrated to PCA.`); } } catch (e) { - this._logger.error(`[getOrCreate] [${clientId}] [${authority}] Error migrating refresh token:`, e); + this._logger.error(`[getOrCreate] [${clientId}] Error migrating refresh token:`, e); } } - // reinitialize the PCA so the account is properly cached - await pca.initialize(); } return pca; } - private async _doCreatePublicClientApplication(clientId: string, authority: string, pcasKey: string) { - const pca = new CachedPublicClientApplication(clientId, authority, this._cloudName, this._globalMemento, this._secretStorage, this._logger); - this._pcas.set(pcasKey, pca); + private async _doCreatePublicClientApplication(clientId: string): Promise { + const pca = await CachedPublicClientApplication.create(clientId, this._secretStorage, this._accountAccess, this._logger); + this._pcas.set(clientId, pca); const disposable = Disposable.from( pca, pca.onDidAccountsChange(e => this._onDidAccountsChangeEmitter.fire(e)), @@ -138,15 +147,17 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient // The PCA has no more accounts, so we can dispose it so we're not keeping it // around forever. disposable.dispose(); - this._pcaDisposables.delete(pcasKey); - this._pcas.delete(pcasKey); - this._logger.debug(`[_doCreatePublicClientApplication] [${clientId}] [${authority}] PCA disposed. Firing off storing of PCAs...`); + this._pcaDisposables.delete(clientId); + this._pcas.delete(clientId); + this._logger.debug(`[_doCreatePublicClientApplication] [${clientId}] PCA disposed. Firing off storing of PCAs...`); void this._storePublicClientApplications(); }) ); - this._pcaDisposables.set(pcasKey, disposable); - // Intialize the PCA after the `onDidAccountsChange` is set so we get initial state. - await pca.initialize(); + this._pcaDisposables.set(clientId, disposable); + // Fire for the initial state and only if accounts exist + if (pca.accounts.length > 0) { + this._onDidAccountsChangeEmitter.fire({ added: pca.accounts, changed: [], deleted: [] }); + } return pca; } @@ -183,15 +194,14 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient } // Handle the new ones - for (const newPca of pcaKeysFromStorage) { + for (const clientId of pcaKeysFromStorage) { try { - const { clientId, authority } = JSON.parse(newPca); - this._logger.debug(`[_handleSecretStorageChange] [${clientId}] [${authority}] Creating new PCA that was created in another window...`); - await this._doCreatePublicClientApplication(clientId, authority, newPca); - this._logger.debug(`[_handleSecretStorageChange] [${clientId}] [${authority}] PCA created.`); + this._logger.debug(`[_handleSecretStorageChange] [${clientId}] Creating new PCA that was created in another window...`); + await this._doCreatePublicClientApplication(clientId); + this._logger.debug(`[_handleSecretStorageChange] [${clientId}] PCA created.`); } catch (_e) { // This really shouldn't happen, but should we do something about this? - this._logger.error(`Failed to parse new PublicClientApplication: ${newPca}`); + this._logger.error(`Failed to create new PublicClientApplication: ${clientId}`); continue; } } @@ -204,15 +214,24 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient } } -class PublicClientApplicationsSecretStorage { +interface IPublicClientApplicationSecretStorage { + get(): Promise; + getOldValue(): Promise<{ clientId: string; authority: string }[] | undefined>; + store(value: string[]): Thenable; + delete(): Thenable; + onDidChange: Event; +} + +class PublicClientApplicationsSecretStorage implements IPublicClientApplicationSecretStorage, Disposable { private _disposable: Disposable; private readonly _onDidChangeEmitter = new EventEmitter; readonly onDidChange: Event = this._onDidChangeEmitter.event; - private readonly _key = `publicClientApplications-${this._cloudName}`; + private readonly _oldKey = `publicClientApplications-${this._cloudName}`; + private readonly _key = `publicClients-${this._cloudName}`; - constructor(private readonly _secretStorage: SecretStorage, private readonly _cloudName: string) { + private constructor(private readonly _secretStorage: SecretStorage, private readonly _cloudName: string) { this._disposable = Disposable.from( this._onDidChangeEmitter, this._secretStorage.onDidChange(e => { @@ -223,6 +242,30 @@ class PublicClientApplicationsSecretStorage { ); } + static async create(secretStorage: SecretStorage, cloudName: string): Promise { + const storage = new PublicClientApplicationsSecretStorage(secretStorage, cloudName); + await storage.initialize(); + return storage; + } + + /** + * Runs the migration. + * TODO: Remove this after a version. + */ + private async initialize() { + const oldValue = await this.getOldValue(); + if (!oldValue) { + return; + } + const newValue = await this.get() ?? []; + for (const { clientId } of oldValue) { + if (!newValue.includes(clientId)) { + newValue.push(clientId); + } + } + await this.store(newValue); + } + async get(): Promise { const value = await this._secretStorage.get(this._key); if (!value) { @@ -231,6 +274,25 @@ class PublicClientApplicationsSecretStorage { return JSON.parse(value); } + /** + * Old representation of data that included the authority. This should be removed in a version or 2. + * @returns An array of objects with clientId and authority + */ + async getOldValue(): Promise<{ clientId: string; authority: string }[] | undefined> { + const value = await this._secretStorage.get(this._oldKey); + if (!value) { + return undefined; + } + const result: { clientId: string; authority: string }[] = []; + for (const stringifiedObj of JSON.parse(value)) { + const obj = JSON.parse(stringifiedObj); + if (obj.clientId && obj.authority) { + result.push(obj); + } + } + return result; + } + store(value: string[]): Thenable { return this._secretStorage.store(this._key, JSON.stringify(value)); } diff --git a/extensions/open-remote-ssh/package.json b/extensions/open-remote-ssh/package.json index 04bd4ad8..16cef436 100644 --- a/extensions/open-remote-ssh/package.json +++ b/extensions/open-remote-ssh/package.json @@ -1,7 +1,7 @@ { "name": "open-remote-ssh", "displayName": "Open Remote - SSH", - "publisher": "voideditor", + "publisher": "voideditor-test", "description": "Use any remote machine with a SSH server as your development environment.", "version": "0.0.48", "icon": "resources/icon.png", @@ -71,7 +71,7 @@ "type": "string", "description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number", "scope": "application", - "default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz" + "default": "https://github.com/voideditor-test/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz" }, "remote.SSH.remotePlatform": { "type": "object", diff --git a/extensions/open-remote-ssh/src/serverConfig.ts b/extensions/open-remote-ssh/src/serverConfig.ts index 2478df5d..2405b2a6 100644 --- a/extensions/open-remote-ssh/src/serverConfig.ts +++ b/extensions/open-remote-ssh/src/serverConfig.ts @@ -32,14 +32,12 @@ export async function getVSCodeServerConfig(): Promise { const customServerBinaryName = vscode.workspace.getConfiguration('remote.SSH.experimental').get('serverBinaryName', ''); return { - // version: vscode.version.replace('-insider', ''), + version: vscode.version.replace('-insider', ''), commit: productJson.commit, quality: productJson.quality, release: productJson.release, serverApplicationName: customServerBinaryName || productJson.serverApplicationName, serverDataFolderName: productJson.serverDataFolderName, serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate, - // Void changed this - version: productJson.voidVersion }; } diff --git a/extensions/open-remote-ssh/src/serverSetup.ts b/extensions/open-remote-ssh/src/serverSetup.ts index 31830223..772910fe 100644 --- a/extensions/open-remote-ssh/src/serverSetup.ts +++ b/extensions/open-remote-ssh/src/serverSetup.ts @@ -39,7 +39,7 @@ export class ServerInstallError extends Error { } } -const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz'; +const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor-test/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz'; export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise { let shell = 'powershell'; diff --git a/extensions/open-remote-wsl/package.json b/extensions/open-remote-wsl/package.json index e913bdd9..3414b9f4 100644 --- a/extensions/open-remote-wsl/package.json +++ b/extensions/open-remote-wsl/package.json @@ -38,7 +38,7 @@ "type": "string", "description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number", "scope": "application", - "default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz" + "default": "https://github.com/voideditor-test/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz" } } }, diff --git a/extensions/open-remote-wsl/src/serverConfig.ts b/extensions/open-remote-wsl/src/serverConfig.ts index 83a73643..476350db 100644 --- a/extensions/open-remote-wsl/src/serverConfig.ts +++ b/extensions/open-remote-wsl/src/serverConfig.ts @@ -31,14 +31,12 @@ export async function getVSCodeServerConfig(): Promise { const productJson = await getVSCodeProductJson(); return { - // version: vscode.version.replace('-insider', ''), + version: vscode.version.replace('-insider', ''), commit: productJson.commit, quality: productJson.quality, release: productJson.release, serverApplicationName: productJson.serverApplicationName, serverDataFolderName: productJson.serverDataFolderName, serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate, - // Void changed this - version: productJson.voidVersion }; } diff --git a/extensions/open-remote-wsl/src/serverSetup.ts b/extensions/open-remote-wsl/src/serverSetup.ts index 3d254313..4c24da38 100644 --- a/extensions/open-remote-wsl/src/serverSetup.ts +++ b/extensions/open-remote-wsl/src/serverSetup.ts @@ -39,7 +39,7 @@ export class ServerInstallError extends Error { } } -const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz'; +const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor-test/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz'; export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise { const scriptId = crypto.randomBytes(12).toString('hex'); diff --git a/extensions/terminal-suggest/.vscodeignore b/extensions/terminal-suggest/.vscodeignore index f05a7941..21df0d1f 100644 --- a/extensions/terminal-suggest/.vscodeignore +++ b/extensions/terminal-suggest/.vscodeignore @@ -5,3 +5,6 @@ tsconfig.json extension.webpack.config.js extension-browser.webpack.config.js package-lock.json +fixtures/** +scripts/** +testWorkspace/** diff --git a/extensions/terminal-suggest/ThirdPartyNotices.txt b/extensions/terminal-suggest/ThirdPartyNotices.txt index 761d5b01..f6de57f6 100644 --- a/extensions/terminal-suggest/ThirdPartyNotices.txt +++ b/extensions/terminal-suggest/ThirdPartyNotices.txt @@ -28,5 +28,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - diff --git a/extensions/terminal-suggest/cgmanifest.json b/extensions/terminal-suggest/cgmanifest.json index f39b1fae..ead3479e 100644 --- a/extensions/terminal-suggest/cgmanifest.json +++ b/extensions/terminal-suggest/cgmanifest.json @@ -82,6 +82,63 @@ "SOFTWARE." ], "version": "1.1.2" + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/zsh-users/zsh", + "commitHash": "435cb1b748ce1f2f5c38edc1d64f4ee2424f9b3a" + } + }, + "version": "5.9", + "licenseDetail": [ + "Unless otherwise noted in the header of specific files, files in this distribution have the licence shown below.", + "", + "However, note that certain shell functions are licensed under versions of the GNU General Public Licence. Anyone distributing the shell as a binary including those files needs to take account of this. Search shell functions for \"Copyright\" for specific copyright information. None of the core functions are affected by this, so those files may simply be omitted.", + "", + "--", + "", + "The Z Shell is copyright (c) 1992-2017 Paul Falstad, Richard Coleman, Zoltán Hidvégi, Andrew Main, Peter Stephenson, Sven Wischnowsky, and others. All rights reserved. Individual authors, whether or not specifically named, retain copyright in all changes; in what follows, they are referred to as `the Zsh Development Group'. This is for convenience only and this body has no legal status. The Z shell is distributed under the following licence; any provisions made in individual files take precedence.", + "", + "Permission is hereby granted, without written agreement and without licence or royalty fees, to use, copy, modify, and distribute this software and to distribute modified versions of this software for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software.", + "", + "In no event shall the Zsh Development Group be liable to any party for direct, indirect, special, incidental, or consequential damages arising out of the use of this software and its documentation, even if the Zsh Development Group have been advised of the possibility of such damage.", + "", + "The Zsh Development Group specifically disclaim any warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. The software provided hereunder is on an \"as is\" basis, and the Zsh Development Group have no obligation to provide maintenance, support, updates, enhancements, or modifications." + ] + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/fish-shell/fish-shell", + "commitHash": "6627d403d33b4e74b49aa4db2a4f17709628cdc8" + } + }, + "version": "3.7.1", + "licenseDetail": [ + "Fish is a smart and user-friendly command line shell.", + "", + "Copyright (C) 2005-2009 Axel Liljencrantz", + "Copyright (C) 2009- fish-shell contributors", + "", + "fish is free software.", + "", + "Most of fish is licensed under the GNU General Public License version 2, and", + "you can redistribute it and/or modify it under the terms of the GNU GPL as", + "published by the Free Software Foundation.", + "", + "fish also includes software licensed under the Python Software Foundation License version 2, the MIT", + "license, and the GNU Library General Public License version 2.", + "", + "Full licensing information is contained in doc_src/license.rst.", + "", + "This program is distributed in the hope that it will be useful, but WITHOUT", + "ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or", + "FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for", + "more details." + ] } ], "version": 1 diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index dcc02bff..5ce429f4 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -15,13 +15,13 @@ ], "enabledApiProposals": [ "terminalCompletionProvider", - "terminalShellEnv", - "terminalShellType" + "terminalShellEnv" ], "scripts": { "compile": "npx gulp compile-extension:terminal-suggest", "watch": "npx gulp watch-extension:terminal-suggest", - "pull-zshbuiltins": "ts-node ./scripts/pullZshBuiltins.ts" + "pull-zshbuiltins": "ts-node ./scripts/pullZshBuiltins.ts", + "pull-fishbuiltins": "ts-node ./scripts/pullFishBuiltins.ts" }, "main": "./out/terminalSuggestMain", "activationEvents": [ diff --git a/extensions/terminal-suggest/scripts/pullFishBuiltins.ts b/extensions/terminal-suggest/scripts/pullFishBuiltins.ts new file mode 100644 index 00000000..8ac6b870 --- /dev/null +++ b/extensions/terminal-suggest/scripts/pullFishBuiltins.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { cleanupText, checkWindows, execAsync, copyright } from './terminalScriptHelpers'; + +checkWindows(); + +interface ICommandDetails { + description: string; + args: string | undefined; + shortDescription?: string; +} + +let fishBuiltinsCommandDescriptionsCache = new Map(); + +// Fallback descriptions for commands that don't return proper help information +const fallbackDescriptions: Record = { + '[': { + shortDescription: 'Test if a statement is true', + description: 'Evaluate an expression and return a status of true (0) or false (non-zero). Unlike the `test` command, the `[` command requires a closing `]`.', + args: 'EXPRESSION ]' + }, + 'break': { + shortDescription: 'Exit the current loop', + description: 'Terminate the execution of the nearest enclosing `while` or `for` loop and proceed with the next command after the loop.', + args: undefined + }, + 'breakpoint': { + shortDescription: 'Launch debug mode', + description: 'Pause execution and launch an interactive debug prompt. This is useful for inspecting the state of a script at a specific point.', + args: undefined + }, + 'case': { + shortDescription: 'Match a value against patterns', + description: 'Within a `switch` block, the `case` command specifies patterns to match against the given value, executing the associated block if a match is found.', + args: 'PATTERN...' + }, + 'continue': { + shortDescription: 'Skip to the next iteration of a loop', + description: 'Within a `while` or `for` loop, `continue` skips the remaining commands in the current iteration and proceeds to the next iteration of the loop.', + args: undefined + }, + 'else': { + shortDescription: 'Execute commands if the previous condition was false', + description: 'In an `if` block, the `else` section contains commands that execute if none of the preceding `if` or `else if` conditions were true.', + args: undefined + }, + 'end': { + shortDescription: 'Terminate a block of code', + description: 'Conclude a block of code initiated by constructs like `if`, `switch`, `while`, `for`, or `function`.', + args: undefined + }, + 'eval': { + shortDescription: 'Execute arguments as a command', + description: 'Concatenate all arguments into a single command and execute it. This allows for dynamic construction and execution of commands.', + args: 'COMMAND...' + }, + 'false': { + shortDescription: 'Return an unsuccessful result', + description: 'A command that returns a non-zero exit status, indicating failure. It is often used in scripts to represent a false condition.', + args: undefined + }, + 'realpath': { + shortDescription: 'Resolve and print the absolute path', + description: 'Convert each provided path to its absolute, canonical form by resolving symbolic links and relative path components.', + args: 'PATH...' + }, + ':': { + shortDescription: 'No operation command', + description: 'The `:` command is a no-op (no operation) command that returns a successful (zero) exit status. It can be used as a placeholder in scripts where a command is syntactically required but no action is desired.', + args: undefined + }, + 'test': { + shortDescription: 'Evaluate conditional expressions', + description: 'The `test` command evaluates conditional expressions and sets the exit status to 0 if the expression is true, and 1 if it is false. It supports various operators to evaluate expressions related to strings, numbers, and file attributes.', + args: 'EXPRESSION' + }, + 'true': { + shortDescription: 'Return a successful result', + description: 'The `true` command always returns a successful (zero) exit status. It is often used in scripts and conditional statements where an unconditional success result is needed.', + args: undefined + }, + 'printf': { + shortDescription: 'Display formatted text', + description: 'The `printf` command formats and prints text according to a specified format string. Unlike `echo`, `printf` does not append a newline unless explicitly included in the format.', + args: 'FORMAT [ARGUMENT...]' + } +}; + + +async function createCommandDescriptionsCache(): Promise { + const cachedCommandDescriptions: Map = new Map(); + + try { + // Get list of all builtins + const builtinsOutput = await execAsync('fish -c "builtin -n"').then(r => r.stdout.trim()); + const builtins = builtinsOutput.split('\n'); + + console.log(`Found ${builtins.length} Fish builtin commands`); + + for (const cmd of builtins) { + try { + // Get help info for each builtin + const helpOutput = await execAsync(`fish -c "${cmd} --help 2>&1"`).then(r => r.stdout); + let set = false; + if (helpOutput && !helpOutput.includes('No help for function') && !helpOutput.includes('See the web documentation')) { + const cleanHelpText = cleanupText(helpOutput); + + // Split the text into lines to process + const lines = cleanHelpText.split('\n'); + + + // Extract the short description, args, and full description + const { shortDescription, args, description } = extractHelpContent(cmd, lines); + + cachedCommandDescriptions.set(cmd, { + shortDescription, + description, + args + }); + set = description !== ''; + } + if (!set) { + // Use fallback descriptions for commands that don't return proper help + if (fallbackDescriptions[cmd]) { + console.info(`Using fallback description for ${cmd}`); + cachedCommandDescriptions.set(cmd, fallbackDescriptions[cmd]); + } else { + console.info(`No fallback description exists for ${cmd}`); + } + } + } catch { + // Use fallback descriptions for commands that throw an error + if (fallbackDescriptions[cmd]) { + console.info('Using fallback description for', cmd); + cachedCommandDescriptions.set(cmd, fallbackDescriptions[cmd]); + } else { + console.info(`Error getting help for ${cmd}`); + } + } + } + } catch (e) { + console.error('Error creating Fish builtins cache:', e); + process.exit(1); + } + + fishBuiltinsCommandDescriptionsCache = cachedCommandDescriptions; +} + +/** + * Extracts short description, args, and full description from help text lines + */ +function extractHelpContent(cmd: string, lines: string[]): { shortDescription: string; args: string | undefined; description: string } { + let shortDescription = ''; + let args: string | undefined; + let description = ''; + + // Skip the first line (usually just command name and basic usage) + let i = 1; + + // Skip any leading empty lines + while (i < lines.length && lines[i].trim().length === 0) { + i++; + } + + // The next non-empty line after the command name is typically + // either the short description or additional usage info + const startLine = i; + + // Find where the short description starts + if (i < lines.length) { + // First, check if the line has a command prefix and remove it + let firstContentLine = lines[i].trim(); + const cmdPrefixRegex = new RegExp(`^${cmd}\\s*-\\s*`, 'i'); + firstContentLine = firstContentLine.replace(cmdPrefixRegex, ''); + + // First non-empty line is the short description + shortDescription = firstContentLine; + i++; + + // Next non-empty line (after short description) is typically args + while (i < lines.length && lines[i].trim().length === 0) { + i++; + } + + if (i < lines.length) { + // Found a line after the short description - that's our args + args = lines[i].trim(); + i++; + } + } + + // Find the DESCRIPTION marker which marks the end of args section + let descriptionIndex = -1; + for (let j = i; j < lines.length; j++) { + if (lines[j].trim() === 'DESCRIPTION') { + descriptionIndex = j; + break; + } + } + + // If DESCRIPTION marker is found, consider everything between i and descriptionIndex as part of args + if (descriptionIndex > i) { + // Combine lines from i up to (but not including) descriptionIndex + const additionalArgs = lines.slice(i, descriptionIndex).join('\n').trim(); + if (additionalArgs) { + args = args ? `${args}\n${additionalArgs}` : additionalArgs; + } + i = descriptionIndex + 1; // Move past the DESCRIPTION line + } + + // The rest is the full description (skipping any empty lines after args) + while (i < lines.length && lines[i].trim().length === 0) { + i++; + } + + // Combine the remaining lines into the full description + description = lines.slice(Math.max(i, startLine)).join('\n').trim(); + + // If description is empty, use the short description + if (!description && shortDescription) { + description = shortDescription; + } + + // Extract just the first sentence for short description + const firstPeriodIndex = shortDescription.indexOf('.'); + if (firstPeriodIndex > 0) { + shortDescription = shortDescription.substring(0, firstPeriodIndex + 1).trim(); + } else if (shortDescription.length > 100) { + shortDescription = shortDescription.substring(0, 100) + '...'; + } + + return { + shortDescription, + args, + description + }; +} + +const main = async () => { + try { + await createCommandDescriptionsCache(); + console.log('Created Fish command descriptions cache with', fishBuiltinsCommandDescriptionsCache.size, 'entries'); + + // Save the cache to a TypeScript file + const cacheFilePath = path.join(__dirname, '../src/shell/fishBuiltinsCache.ts'); + const cacheObject = Object.fromEntries(fishBuiltinsCommandDescriptionsCache); + const tsContent = `${copyright}\n\nexport const fishBuiltinsCommandDescriptionsCache = ${JSON.stringify(cacheObject, null, 2)} as const;`; + await fs.writeFile(cacheFilePath, tsContent, 'utf8'); + console.log('Saved Fish command descriptions cache to fishBuiltinsCache.ts with', Object.keys(cacheObject).length, 'entries'); + } catch (error) { + console.error('Error:', error); + } +}; + +main(); diff --git a/extensions/terminal-suggest/scripts/pullZshBuiltins.ts b/extensions/terminal-suggest/scripts/pullZshBuiltins.ts index cedfc85c..a05866b1 100644 --- a/extensions/terminal-suggest/scripts/pullZshBuiltins.ts +++ b/extensions/terminal-suggest/scripts/pullZshBuiltins.ts @@ -3,16 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { exec } from 'child_process'; -import { promisify } from 'util'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { platform } from 'os'; +import { checkWindows, execAsync, copyright } from './terminalScriptHelpers'; -if (platform() === 'win32') { - console.error('\x1b[31mThis command is not supported on Windows\x1b[0m'); - process.exit(1); -} +checkWindows(); const latestZshVersion = 5.9; @@ -126,14 +121,14 @@ const shortDescriptions: Map = new Map([ ['ztcp', 'Manipulate TCP sockets'], ]); -const execAsync = promisify(exec); - interface ICommandDetails { description: string; args: string | undefined; shortDescription?: string; } + let zshBuiltinsCommandDescriptionsCache = new Map(); + async function createCommandDescriptionsCache(): Promise { const cachedCommandDescriptions: Map = new Map(); let output = ''; @@ -259,10 +254,4 @@ const main = async () => { } }; -const copyright = ` -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/`; - main(); diff --git a/extensions/terminal-suggest/scripts/terminalScriptHelpers.ts b/extensions/terminal-suggest/scripts/terminalScriptHelpers.ts new file mode 100644 index 00000000..402e6d5d --- /dev/null +++ b/extensions/terminal-suggest/scripts/terminalScriptHelpers.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { platform } from 'os'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +export const execAsync = promisify(exec); + +/** + * Cleans up text from terminal control sequences and formatting artifacts + */ +export function cleanupText(text: string): string { + // Remove ANSI escape codes + let cleanedText = text.replace(/\x1b\[\d+m/g, ''); + + // Remove backspace sequences (like a\bb which tries to print a, move back, print b) + // This regex looks for a character followed by a backspace and another character + const backspaceRegex = /.\x08./g; + while (backspaceRegex.test(cleanedText)) { + cleanedText = cleanedText.replace(backspaceRegex, match => match.charAt(2)); + } + + // Remove any remaining backspaces and their preceding characters + cleanedText = cleanedText.replace(/.\x08/g, ''); + + // Remove underscores that are used for formatting in some fish help output + cleanedText = cleanedText.replace(/_\b/g, ''); + + return cleanedText; +} + +/** + * Copyright notice for generated files + */ +export const copyright = `/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/`; + +/** + * Checks if the script is running on Windows and exits if so + */ +export function checkWindows(): void { + if (platform() === 'win32') { + console.error('\x1b[31mThis command is not supported on Windows\x1b[0m'); + process.exit(1); + } +} diff --git a/extensions/terminal-suggest/scripts/update-specs.js b/extensions/terminal-suggest/scripts/update-specs.js index 5f6e0ec5..775c5dd3 100644 --- a/extensions/terminal-suggest/scripts/update-specs.js +++ b/extensions/terminal-suggest/scripts/update-specs.js @@ -14,8 +14,25 @@ const replaceStrings = [ [ 'import { filepaths } from "@fig/autocomplete-generators";', 'import { filepaths } from \'../../helpers/filepaths\';' - ] -] + ], +]; +const indentSearch = [20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1].map(e => new RegExp('^' + ' '.repeat(e * 2), 'gm')); +const indentReplaceValue = [20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1].map(e => '\t'.repeat(e)); + +const specSpecificReplaceStrings = new Map([ + ['git', [ + [ + 'import { ai } from "@fig/autocomplete-generators";', + 'function ai(...args: any[]): undefined { return undefined; }' + ], [ + 'prompt: async ({ executeCommand }) => {', + 'prompt: async ({ executeCommand }: any) => {' + ], [ + 'message: async ({ executeCommand }) =>', + 'message: async ({ executeCommand }: any) =>' + ] + ]], +]); for (const spec of upstreamSpecs) { const source = path.join(extRoot, `third_party/autocomplete/src/${spec}.ts`); @@ -26,5 +43,15 @@ for (const spec of upstreamSpecs) { for (const replaceString of replaceStrings) { content = content.replaceAll(replaceString[0], replaceString[1]); } + for (let i = 0; i < indentSearch.length; i++) { + content = content.replaceAll(indentSearch[i], indentReplaceValue[i]); + } + const thisSpecReplaceStrings = specSpecificReplaceStrings.get(spec); + if (thisSpecReplaceStrings) { + for (const replaceString of thisSpecReplaceStrings) { + content = content.replaceAll(replaceString[0], replaceString[1]); + } + } + fs.writeFileSync(destination, content); } diff --git a/extensions/terminal-suggest/src/completions/code-insiders.ts b/extensions/terminal-suggest/src/completions/code-insiders.ts index 89d01dc5..1eb4f04a 100644 --- a/extensions/terminal-suggest/src/completions/code-insiders.ts +++ b/extensions/terminal-suggest/src/completions/code-insiders.ts @@ -2,12 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import code from './code'; +import code, { commonOptions, extensionManagementOptions, troubleshootingOptions } from './code'; const codeInsidersCompletionSpec: Fig.Spec = { ...code, name: 'code-insiders', description: 'Visual Studio Code Insiders', + options: [ + ...commonOptions, + ...extensionManagementOptions('code-insiders'), + ...troubleshootingOptions('code-insiders'), + ], }; export default codeInsidersCompletionSpec; diff --git a/extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts b/extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts new file mode 100644 index 00000000..dd54a243 --- /dev/null +++ b/extensions/terminal-suggest/src/completions/code-tunnel-insiders.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { commonOptions, extensionManagementOptions, troubleshootingOptions, globalTunnelOptions, codeTunnelSubcommands, extTunnelSubcommand, codeTunnelOptions } from './code'; +import codeTunnelCompletionSpec, { codeTunnelSpecOptions } from './code-tunnel'; + +const codeTunnelInsidersCompletionSpec: Fig.Spec = { + ...codeTunnelCompletionSpec, + name: 'code-tunnel-insiders', + description: 'Create a tunnel that\'s accessible on vscode.dev from anywhere, with insider features.', + subcommands: [...codeTunnelSubcommands, extTunnelSubcommand], + options: [ + ...commonOptions, + ...extensionManagementOptions('code-tunnel-insiders'), + ...troubleshootingOptions('code-tunnel-insiders'), + ...globalTunnelOptions, + ...codeTunnelOptions, + ...codeTunnelSpecOptions + ] +}; + +export default codeTunnelInsidersCompletionSpec; diff --git a/extensions/terminal-suggest/src/completions/code-tunnel.ts b/extensions/terminal-suggest/src/completions/code-tunnel.ts new file mode 100644 index 00000000..a53edd52 --- /dev/null +++ b/extensions/terminal-suggest/src/completions/code-tunnel.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import code, { codeTunnelSubcommands, commonOptions, extensionManagementOptions, troubleshootingOptions, globalTunnelOptions, extTunnelSubcommand, codeTunnelOptions } from './code'; + + +export const codeTunnelSpecOptions: Fig.Option[] = [ + { + name: '--cli-data-dir', + description: 'Directory where CLI metadata should be stored', + isRepeatable: true, + args: { + name: 'cli_data_dir', + isOptional: true, + }, + }, + { + name: '--log-to-file', + description: 'Log to a file in addition to stdout. Used when running as a service', + hidden: true, + isRepeatable: true, + args: { + name: 'log_to_file', + isOptional: true, + template: 'filepaths', + }, + }, + { + name: '--log', + description: 'Log level to use', + isRepeatable: true, + args: { + name: 'log', + isOptional: true, + suggestions: [ + 'trace', + 'debug', + 'info', + 'warn', + 'error', + 'critical', + 'off', + ], + }, + }, + { + name: '--telemetry-level', + description: 'Sets the initial telemetry level', + hidden: true, + isRepeatable: true, + args: { + name: 'telemetry_level', + isOptional: true, + suggestions: [ + 'off', + 'crash', + 'error', + 'all', + ], + }, + }, + { + name: '--verbose', + description: 'Print verbose output (implies --wait)', + }, + { + name: '--disable-telemetry', + description: 'Disable telemetry for the current command, even if it was previously accepted as part of the license prompt or specified in \'--telemetry-level\'', + }, + { + name: ['-h', '--help'], + description: 'Print help', + }, +]; + +const codeTunnelCompletionSpec: Fig.Spec = { + ...code, + name: 'code-tunnel', + description: 'Create a tunnel that\'s accessible on vscode.dev from anywhere.', + subcommands: [ + ...codeTunnelSubcommands, + extTunnelSubcommand + ], + options: [ + ...commonOptions, + ...extensionManagementOptions('code-tunnel'), + ...troubleshootingOptions('code-tunnel'), + ...globalTunnelOptions, + ...codeTunnelOptions + ] +}; + +export default codeTunnelCompletionSpec; diff --git a/extensions/terminal-suggest/src/completions/code.ts b/extensions/terminal-suggest/src/completions/code.ts index c514a32e..1ae5957a 100644 --- a/extensions/terminal-suggest/src/completions/code.ts +++ b/extensions/terminal-suggest/src/completions/code.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const commonOptions: Fig.Option[] = [ +import { filepaths } from '../helpers/filepaths'; + +export const commonOptions: Fig.Option[] = [ { name: '-', description: `Read from stdin (e.g. 'ps aux | grep code | code -')`, @@ -60,7 +62,6 @@ const commonOptions: Fig.Option[] = [ 'Open a file at the path on the specified line and character position', args: { name: 'file:line[:character]', - // TODO: Support :line[:character] completion? template: 'filepaths', }, }, @@ -135,9 +136,25 @@ const commonOptions: Fig.Option[] = [ name: ['-h', '--help'], description: 'Print usage', }, + { + name: '--locate-shell-integration-path', + description: + 'Print the path to the shell integration script for the provided shell', + args: { + isOptional: false, + name: 'shell', + description: 'The shell to locate the integration script for', + suggestions: [ + 'bash', + 'fish', + 'pwsh', + 'zsh', + ] + } + } ]; -const extensionManagementOptions: Fig.Option[] = [ +export const extensionManagementOptions = (cliName: string): Fig.Option[] => [ { name: '--extensions-dir', description: 'Set the root path for extensions', @@ -188,8 +205,13 @@ const extensionManagementOptions: Fig.Option[] = [ description: `Installs or updates an extension. The argument is either an extension id or a path to a VSIX. The identifier of an extension is '\${ publisher }.\${ name }'. Use '--force' argument to update to latest version. To install a specific version provide '@\${version}'. For example: 'vscode.csharp@1.2.3'`, args: { - // TODO: Create extension ID generator name: 'extension-id[@version] | path-to-vsix', + generators: [ + createCodeGenerators(cliName), + filepaths({ + extensions: ['vsix'], + }), + ], }, }, { @@ -201,8 +223,8 @@ const extensionManagementOptions: Fig.Option[] = [ name: '--uninstall-extension', description: 'Uninstalls an extension', args: { - // TODO: Create extension ID generator name: 'extension-id', + generators: createCodeGenerators(cliName) }, }, { @@ -212,7 +234,7 @@ const extensionManagementOptions: Fig.Option[] = [ }, ]; -const troubleshootingOptions: Fig.Option[] = [ +export const troubleshootingOptions = (cliName: string): Fig.Option[] => [ { name: ['-v', '--version'], description: 'Print version', @@ -254,8 +276,8 @@ const troubleshootingOptions: Fig.Option[] = [ name: '--disable-extension', description: 'Disable an extension', args: { - // TODO: Create extension ID generator name: 'extension-id', + generators: createCodeGenerators(cliName) }, }, { @@ -301,6 +323,644 @@ const troubleshootingOptions: Fig.Option[] = [ }, ]; +export function createCodeGenerators(cliName: string): Fig.Generator { + return { + script: [cliName, '--list-extensions', '--show-versions'], + postProcess: parseInstalledExtensions + }; +} + +export function parseInstalledExtensions(out: string): Fig.Suggestion[] | undefined { + const extensions = out.split('\n').filter(Boolean).map((line) => { + const [id, version] = line.split('@'); + return { + name: id, + type: 'option' as Fig.SuggestionType, + description: `Version: ${version}` + }; + }); + return extensions; +} + +export const commonAuthOptions: Fig.Option[] = [ + { + name: '--access-token', + description: 'An access token to store for authentication', + isRepeatable: true, + args: { + name: 'access_token', + isOptional: true, + }, + }, + { + name: '--refresh-token', + description: 'An access token to store for authentication', + isRepeatable: true, + args: { + name: 'refresh_token', + isOptional: true, + }, + }, + { + name: '--provider', + description: 'The auth provider to use. If not provided, a prompt will be shown', + isRepeatable: true, + args: { + name: 'provider', + isOptional: true, + suggestions: [ + 'microsoft', + 'github', + ], + }, + } +]; + +export const tunnelHelpOptions: Fig.Option[] = [ + { + name: ['-h', '--help'], + description: 'Print help', + }, +]; + +export const globalTunnelOptions: Fig.Option[] = [ + { + name: '--cli-data-dir', + description: 'Directory where CLI metadata should be stored', + args: { + name: 'cli_data_dir', + }, + }, + { + name: '--verbose', + description: 'Print verbose output (implies --wait)', + }, + { + name: '--log', + description: 'Log level to use', + isRepeatable: true, + args: { + name: 'log', + isOptional: true, + suggestions: [ + 'trace', + 'debug', + 'info', + 'warn', + 'error', + 'critical', + 'off', + ], + }, + }, +]; + + +export const codeTunnelOptions = [ + { + name: '--extensions-dir', + description: 'Set the root path for extensions', + isRepeatable: true, + args: { + name: 'extensions_dir', + isOptional: true, + }, + }, + { + name: '--user-data-dir', + description: 'Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of the editor', + isRepeatable: true, + args: { + name: 'user_data_dir', + isOptional: true, + }, + }, + { + name: '--use-version', + description: 'Sets the editor version to use for this command. The preferred version can be persisted with `code version use `. Can be \'stable\', \'insiders\', a version number, or an absolute path to an existing install', + isRepeatable: true, + args: { + name: 'use_version', + isOptional: true, + }, + }, +]; + +export const extTunnelSubcommand = { + name: 'ext', + description: 'Manage editor extensions', + subcommands: [ + { + name: 'list', + description: 'List installed extensions', + options: [...globalTunnelOptions, ...tunnelHelpOptions, + { + name: '--category', + description: 'Filters installed extensions by provided category, when using --list-extensions', + isRepeatable: true, + args: { + name: 'category', + isOptional: true, + }, + }, + { + name: '--show-versions', + description: 'Show versions of installed extensions, when using --list-extensions', + }, + ] + }, + { + name: 'install', + description: 'Install an extension', + options: [...globalTunnelOptions, ...tunnelHelpOptions, + { + name: '--pre-release', + description: 'Installs the pre-release version of the extension', + }, + { + name: '--donot-include-pack-and-dependencies', + description: `Don't include installing pack and dependencies of the extension`, + }, + { + name: '--force', + description: `Update to the latest version of the extension if it's already installed`, + }, + ], + args: { + name: 'ext-id | id', + isVariadic: true, + isOptional: true, + }, + }, + { + name: 'uninstall', + description: 'Uninstall an extension', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + args: { + name: 'ext-id | id', + isVariadic: true, + isOptional: true, + }, + }, + { + name: 'update', + description: 'Update the installed extensions', + options: [...globalTunnelOptions, ...tunnelHelpOptions] + }, + ], + ...globalTunnelOptions, + ...codeTunnelOptions +}; + + +export const codeTunnelSubcommands = [ + { + name: 'tunnel', + description: 'Create a tunnel that\'s accessible on vscode.dev from anywhere. Run`code tunnel --help` for more usage info', + subcommands: [ + { + name: 'prune', + description: 'Delete all servers which are currently not running', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'kill', + description: 'Stops any running tunnel on the system', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'restart', + description: 'Restarts any running tunnel on the system', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'status', + description: 'Gets whether there is a tunnel running on the current machine', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'rename', + description: 'Rename the name of this machine associated with port forwarding service', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + args: { + name: 'name', + }, + }, + { + name: 'status', + description: 'Print process usage and diagnostics information', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'unregister', + description: 'Remove this machine\'s association with the port forwarding service', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'user', + subcommands: [ + { + name: 'login', + description: 'Log in to port forwarding service', + options: [...globalTunnelOptions, ...tunnelHelpOptions, ...commonAuthOptions], + }, + { + name: 'logout', + description: 'Log out of port forwarding service', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'show', + description: 'Show the account that\'s logged into port forwarding service', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'help', + description: 'Print this message or the help of the given subcommand(s)', + subcommands: [ + { name: 'login', description: 'Log in to port forwarding service' }, + { name: 'logout', description: 'Log out of port forwarding service' }, + { name: 'show', description: 'Show the account that\'s logged into port forwarding service' }, + { name: 'help', description: 'Print this message or the help of the given subcommand(s)' }, + ], + }, + ], + }, + { + name: 'service', + description: '(Preview) Manages the tunnel when installed as a system service,', + subcommands: [ + { + name: 'install', + description: 'Installs or re-installs the tunnel service on the machine', + options: [ + { + name: '--name', + description: 'Sets the machine name for port forwarding service', + + args: { + name: 'name', + + }, + }, + { + name: '--accept-server-license-terms', + description: 'If set, the user accepts the server license terms and the server will be started without a user prompt', + }, + ...globalTunnelOptions, ...tunnelHelpOptions + ], + }, + { + name: 'uninstall', + description: 'Uninstalls and stops the tunnel service', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'log', + description: 'Shows logs for the running service', + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'help', + description: 'Print this message or the help of the given subcommand(s)', + subcommands: [ + { name: 'install', description: 'Installs or re-installs the tunnel service on the machine' }, + { name: 'uninstall', description: 'Uninstalls and stops the tunnel service' }, + { name: 'log', description: 'Shows logs for the running service' }, + { name: 'help', description: 'Print this message or the help of the given subcommand(s)' }, + ], + }, + ], + options: [...globalTunnelOptions, ...tunnelHelpOptions], + }, + { + name: 'help', + description: 'Print this message or the help of the given subcommand(s)', + subcommands: [ + { name: 'prune', description: 'Delete all servers which are currently not running' }, + { name: 'kill', description: 'Stops any running tunnel on the system' }, + { name: 'restart', description: 'Restarts any running tunnel on the system' }, + { name: 'status', description: 'Gets whether there is a tunnel running on the current machine' }, + { name: 'rename', description: 'Rename the name of this machine associated with port forwarding service' }, + { name: 'unregister', description: 'Remove this machine\'s association with the port forwarding service' }, + { + name: 'user', + subcommands: [ + { name: 'login', description: 'Log in to port forwarding service' }, + { name: 'logout', description: 'Log out of port forwarding service' }, + { name: 'show', description: 'Show the account that\'s logged into port forwarding service' }, + ], + }, + { + name: 'service', + description: '(Preview) Manages the tunnel when installed as a system service,', + subcommands: [ + { name: 'install', description: 'Installs or re-installs the tunnel service on the machine' }, + { name: 'uninstall', description: 'Uninstalls and stops the tunnel service' }, + { name: 'log', description: 'Shows logs for the running service' }, + ], + }, + { name: 'help', description: 'Print this message or the help of the given subcommand(s)' }, + ], + }, + ], + options: [ + { + name: '--install-extension', + description: 'Requests that extensions be preloaded and installed on connecting servers', + isRepeatable: true, + args: { + name: 'install_extension', + isOptional: true, + }, + }, + { + name: '--server-data-dir', + description: 'Specifies the directory that server data is kept in', + isRepeatable: true, + args: { + name: 'server_data_dir', + isOptional: true, + }, + }, + { + name: '--extensions-dir', + description: 'Set the root path for extensions', + isRepeatable: true, + args: { + name: 'extensions_dir', + isOptional: true, + }, + }, + { + name: '--user-data-dir', + description: 'Specifies the directory that user data is kept in. Can be used to open multiple distinct instances of the editor', + isRepeatable: true, + args: { + name: 'user_data_dir', + isOptional: true, + }, + }, + { + name: '--use-version', + description: 'Sets the editor version to use for this command. The preferred version can be persisted with `code version use `. Can be \'stable\', \'insiders\', a version number, or an absolute path to an existing install', + isRepeatable: true, + args: { + name: 'use_version', + isOptional: true, + }, + }, + { + name: '--random-name', + description: 'Randomly name machine for port forwarding service', + }, + { + name: '--no-sleep', + description: 'Prevents the machine going to sleep while this command runs', + }, + { + name: '--accept-server-license-terms', + description: 'If set, the user accepts the server license terms and the server will be started without a user prompt', + }, + { + name: '--name', + description: 'Sets the machine name for port forwarding service', + isRepeatable: true, + args: { + name: 'name', + isOptional: true, + }, + }, + { + name: ['-h', '--help'], + description: 'Print help', + }, + { + name: '--log', + description: 'Log level to use', + isRepeatable: true, + args: { + name: 'log', + isOptional: true, + suggestions: [ + 'trace', + 'debug', + 'info', + 'warn', + 'error', + 'critical', + 'off', + ], + }, + }, + { + name: '--verbose', + description: 'Print verbose output (implies --wait)', + }, + { + name: '--cli-data-dir', + description: 'Directory where CLI metadata should be stored', + args: { + name: 'cli_data_dir', + }, + }, + ], + }, + { + name: 'status', + description: 'Print process usage and diagnostics information', + }, + { + name: 'version', + description: `Changes the version of the editor you're using`, + options: globalTunnelOptions + }, + { + name: 'serve-web', + description: 'Runs a local web version of Code - OSS', + options: [ + { + name: '--host', + description: 'Host to listen on, defaults to \'localhost\'', + isRepeatable: true, + args: { + name: 'host', + isOptional: true, + }, + }, + { + name: '--socket-path', + isRepeatable: true, + args: { + name: 'socket_path', + isOptional: true, + }, + }, + { + name: '--port', + description: 'Port to listen on. If 0 is passed a random free port is picked', + isRepeatable: true, + args: { + name: 'port', + isOptional: true, + }, + }, + { + name: '--connection-token', + description: 'A secret that must be included with all requests', + isRepeatable: true, + args: { + name: 'connection_token', + isOptional: true, + }, + }, + { + name: '--connection-token-file', + description: 'A file containing a secret that must be included with all requests', + isRepeatable: true, + args: { + name: 'connection_token_file', + isOptional: true, + }, + }, + { + name: '--server-base-path', + description: 'Specifies the path under which the web UI and the code server is provided', + isRepeatable: true, + args: { + name: 'server_base_path', + isOptional: true, + }, + }, + { + name: '--server-data-dir', + description: 'Specifies the directory that server data is kept in', + isRepeatable: true, + args: { + name: 'server_data_dir', + isOptional: true, + }, + }, + { + name: '--without-connection-token', + description: 'Run without a connection token. Only use this if the connection is secured by other means', + }, + { + name: '--accept-server-license-terms', + description: 'If set, the user accepts the server license terms and the server will be started without a user prompt', + }, + ...globalTunnelOptions, ...tunnelHelpOptions, + ] + }, + { + name: 'help', + description: 'Print this message or the help of the given subcommand(s)', + subcommands: [ + { + name: 'tunnel', + description: 'Create a tunnel that\'s accessible on vscode.dev from anywhere. Run`code tunnel --help` for more usage info', + subcommands: [ + { + name: 'prune', + description: 'Delete all servers which are currently not running', + }, + { + name: 'kill', + description: 'Stops any running tunnel on the system', + }, + { + name: 'restart', + description: 'Restarts any running tunnel on the system', + }, + { + name: 'status', + description: 'Gets whether there is a tunnel running on the current machine', + }, + { + name: 'rename', + description: 'Rename the name of this machine associated with port forwarding service', + }, + { + name: 'unregister', + description: `Remove this machine's association with the port forwarding service`, + }, + { + name: 'user', + subcommands: [ + { + name: 'login', + description: 'Log in to port forwarding service', + }, + { + name: 'logout', + description: 'Log out of port forwarding service', + }, + { + name: 'show', + description: 'Show the account that\'s logged into port forwarding service', + }, + ], + }, + { + name: 'service', + description: '(Preview) Manages the tunnel when installed as a system service,', + subcommands: [ + { + name: 'install', + description: 'Installs or re-installs the tunnel service on the machine', + }, + { + name: 'uninstall', + description: 'Uninstalls and stops the tunnel service', + }, + { + name: 'log', + description: 'Shows logs for the running service', + }, + ], + } + ], + }, + extTunnelSubcommand, + { + name: 'status', + description: 'Print process usage and diagnostics information', + }, + { + name: 'version', + description: `Changes the version of the editor you're using`, + subcommands: [ + { + name: 'use', + description: 'Switches the version of the editor in use', + }, + { + name: 'show', + description: 'Shows the currently configured editor version', + }, + ], + }, + { + name: 'serve-web', + description: 'Runs a local web version of Code - OSS', + }, + { + name: 'command-shell', + description: 'Runs the control server on process stdin/stdout', + hidden: true, + }, + { + name: 'update', + description: 'Updates the CLI', + }, + { + name: 'help', + description: 'Print this message or the help of the given subcommand(s)', + }, + ], + }, +]; + const codeCompletionSpec: Fig.Spec = { name: 'code', description: 'Visual Studio Code', @@ -308,11 +968,13 @@ const codeCompletionSpec: Fig.Spec = { template: ['filepaths', 'folders'], isVariadic: true, }, + subcommands: codeTunnelSubcommands, options: [ ...commonOptions, - ...extensionManagementOptions, - ...troubleshootingOptions, + ...extensionManagementOptions('code'), + ...troubleshootingOptions('code'), ], }; export default codeCompletionSpec; + diff --git a/extensions/terminal-suggest/src/completions/npx.ts b/extensions/terminal-suggest/src/completions/npx.ts new file mode 100644 index 00000000..36010391 --- /dev/null +++ b/extensions/terminal-suggest/src/completions/npx.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const completionSpec: Fig.Spec = { + name: 'npx', + description: 'Execute binaries from npm packages', + args: { + name: 'command', + isCommand: true, + generators: { + script: [ + 'bash', + '-c', + 'until [[ -d node_modules/ ]] || [[ $PWD = \'/\' ]]; do cd ..; done; ls -1 node_modules/.bin/', + ], + postProcess: function (out) { + return out + .split('\n') + .map((name) => ({ + name, + icon: 'fig://icon?type=command', + loadSpec: name, + })); + }, + }, + isOptional: true, + }, + + options: [ + { + name: ['--package', '-p'], + description: 'Package to be installed', + args: { + name: 'package', + }, + }, + { + name: '--cache', + args: { + name: 'path', + template: 'filepaths', + }, + description: 'Location of the npm cache', + }, + { + name: '--always-spawn', + description: 'Always spawn a child process to execute the command', + }, + { + name: '-y', + description: 'Execute npx command without prompting for confirmation', + }, + { + description: 'Skip installation if a package is missing', + name: '--no-install', + }, + { + args: { + name: 'path', + template: 'filepaths', + }, + description: 'Path to user npmrc', + name: '--userconfig', + }, + { + name: ['--call', '-c'], + args: { + name: 'script', + }, + description: 'Execute string as if inside `npm run-script`', + }, + { + name: ['--shell', '-s'], + description: 'Shell to execute the command with, if any', + args: { + name: 'shell', + suggestions: [ + { + name: 'bash', + }, + { + name: 'fish', + }, + { + name: 'zsh', + }, + ], + }, + }, + { + args: { + name: 'shell-fallback', + suggestions: [ + { + name: 'bash', + }, + { + name: 'fish', + }, + { + name: 'zsh', + }, + ], + }, + name: '--shell-auto-fallback', + description: + 'Generate shell code to use npx as the "command not found" fallback', + }, + { + name: '--ignore-existing', + description: + 'Ignores existing binaries in $PATH, or in the localproject. This forces npx to do a temporary install and use the latest version', + }, + { + name: ['--quiet', '-q'], + description: + 'Suppress output from npx itself. Subcommands will not be affected', + }, + { + name: '--npm', + args: { + name: 'path to binary', + template: 'filepaths', + }, + description: 'Npm binary to use for internal operations', + }, + { + args: {}, + description: 'Extra node argument when calling a node binary', + name: ['--node-arg', '-n'], + }, + { + description: 'Show version number', + name: ['--version', '-v'], + }, + { + description: 'Show help', + name: ['--help', '-h'], + }, + ], +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/apt.ts b/extensions/terminal-suggest/src/completions/upstream/apt.ts index e3ebbb89..2d44332b 100644 --- a/extensions/terminal-suggest/src/completions/upstream/apt.ts +++ b/extensions/terminal-suggest/src/completions/upstream/apt.ts @@ -134,10 +134,7 @@ const completionSpec: Fig.Spec = { name: "package", description: "The package you want to install", isVariadic: true, - generators: [ - packages, - filepaths({ extensions: ["deb"] }) - ], + generators: [packages, filepaths({ extensions: ["deb"] })], }, options: [ ...installationOptions, diff --git a/extensions/terminal-suggest/src/completions/upstream/brew.ts b/extensions/terminal-suggest/src/completions/upstream/brew.ts index 99b87c45..4e0f0cbe 100644 --- a/extensions/terminal-suggest/src/completions/upstream/brew.ts +++ b/extensions/terminal-suggest/src/completions/upstream/brew.ts @@ -1,1675 +1,1675 @@ const servicesGenerator = (action: string): Fig.Generator => ({ - script: ["bash", "-c", "brew services list | sed -e 's/ .*//' | tail -n +2"], - postProcess: function (out) { - return out - .split("\n") - .filter((line) => !line.includes("unbound")) - .map((line) => ({ - name: line, - icon: "fig://icon?type=package", - description: `${action} ${line}`, - })); - }, + script: ["bash", "-c", "brew services list | sed -e 's/ .*//' | tail -n +2"], + postProcess: function (out) { + return out + .split("\n") + .filter((line) => !line.includes("unbound")) + .map((line) => ({ + name: line, + icon: "fig://icon?type=package", + description: `${action} ${line}`, + })); + }, }); const repositoriesGenerator = (): Fig.Generator => ({ - script: ["brew", "tap"], - postProcess: (out) => { - return out.split("\n").map((line) => ({ name: line })); - }, + script: ["brew", "tap"], + postProcess: (out) => { + return out.split("\n").map((line) => ({ name: line })); + }, }); const formulaeGenerator: Fig.Generator = { - script: ["brew", "list", "-1"], - postProcess: function (out) { - return out - .split("\n") - .filter((line) => !line.includes("=")) - .map((formula) => ({ - name: formula, - icon: "🍺", - description: "Installed formula", - })); - }, + script: ["brew", "list", "-1"], + postProcess: function (out) { + return out + .split("\n") + .filter((line) => !line.includes("=")) + .map((formula) => ({ + name: formula, + icon: "🍺", + description: "Installed formula", + })); + }, }; const outdatedformulaeGenerator: Fig.Generator = { - script: ["brew", "outdated", "-q"], - postProcess: function (out) { - return out.split("\n").map((formula) => ({ - name: formula, - icon: "🍺", - description: "Outdated formula", - })); - }, + script: ["brew", "outdated", "-q"], + postProcess: function (out) { + return out.split("\n").map((formula) => ({ + name: formula, + icon: "🍺", + description: "Outdated formula", + })); + }, }; const generateAllFormulae: Fig.Generator = { - script: ["brew", "formulae"], - postProcess: function (out) { - return out.split("\n").map((formula) => ({ - name: formula, - icon: "🍺", - description: "Formula", - priority: 51, - })); - }, + script: ["brew", "formulae"], + postProcess: function (out) { + return out.split("\n").map((formula) => ({ + name: formula, + icon: "🍺", + description: "Formula", + priority: 51, + })); + }, }; const generateAllCasks: Fig.Generator = { - script: ["brew", "casks"], - postProcess: function (out) { - return out.split("\n").map((cask) => ({ - name: cask, - icon: "🍺", - description: "Cask", - priority: 52, - })); - }, + script: ["brew", "casks"], + postProcess: function (out) { + return out.split("\n").map((cask) => ({ + name: cask, + icon: "🍺", + description: "Cask", + priority: 52, + })); + }, }; const generateAliases: Fig.Generator = { - script: [ - "bash", - "-c", - 'find ~/.brew-aliases/ -type f ! -name "*.*" -d 1 | sed "s/.*\\///"', - ], - postProcess: function (out) { - return out - .split("\n") - .filter((line) => line && line.trim() !== "") - .map((line) => ({ - name: line, - icon: "fig://icon?type=command", - description: `Execute alias ${line}`, - })); - }, + script: [ + "bash", + "-c", + 'find ~/.brew-aliases/ -type f ! -name "*.*" -d 1 | sed "s/.*\\///"', + ], + postProcess: function (out) { + return out + .split("\n") + .filter((line) => line && line.trim() !== "") + .map((line) => ({ + name: line, + icon: "fig://icon?type=command", + description: `Execute alias ${line}`, + })); + }, }; const commonOptions: Fig.Option[] = [ - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { - name: ["-v", "--verbose"], - description: "Make some output more verbose", - }, - { name: ["-h", "--help"], description: "Show help message" }, + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { + name: ["-v", "--verbose"], + description: "Make some output more verbose", + }, + { name: ["-h", "--help"], description: "Show help message" }, ]; const completionSpec: Fig.Spec = { - name: "brew", - description: "Package manager for macOS", - subcommands: [ - { - name: "list", - description: "List all installed formulae", - options: [ - ...commonOptions, - { - name: ["--formula", "--formulae"], - description: - "List only formulae, or treat all named arguments as formulae", - }, - { - name: ["--cask", "--casks"], - description: "List only casks, or treat all named arguments as casks", - }, - { - name: "--unbrewed", - description: - "List files in Homebrew's prefix not installed by Homebrew. (disabled; replaced by brew --prefix --unbrewed)", - }, - { - name: "--full-name", - description: - "Print formulae with fully-qualified names. Unless --full-name, --versions or", - }, - { - name: "--pinned", - description: - "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", - }, - { - name: "--versions", - description: - "Show the version number for installed formulae, or only the specified formulae if formula are provided", - }, - { - name: "--multiple", - description: "Only show formulae with multiple versions installed", - }, - { - name: "--pinned", - description: - "List only pinned formulae, or only the specified (pinned) formulae if formula are provided. See also pin, unpin", - }, - { - name: "-1", - description: - "Force output to be one entry per line. This is the default when output is not to a terminal", - }, - { - name: "-l", - description: - "List formulae and/or casks in long format. Has no effect when a formula or cask name is passed as an argument", - }, - { - name: "-r", - description: - "Reverse the order of the formulae and/or casks sort to list the oldest entries first. Has no effect when a formula or cask name is passed as an argument", - }, - { - name: "-t", - description: - "Sort formulae and/or casks by time modified, listing most recently modified first. Has no effect when a formula or cask name is passed as an argument", - }, - ], - args: { - isOptional: true, - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: "ls", - description: "List all installed formulae", - options: [ - ...commonOptions, - { - name: "--formula", - description: - "List only formulae, or treat all named arguments as formulae", - }, - { - name: "--cask", - description: "List only casks, or treat all named arguments as casks", - }, - { - name: "--unbrewed", - description: - "List files in Homebrew's prefix not installed by Homebrew. (disabled; replaced by brew --prefix --unbrewed)", - }, - { - name: "--full-name", - description: - "Print formulae with fully-qualified names. Unless --full-name, --versions or", - }, - { - name: "--pinned", - description: - "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", - }, - { - name: "--versions", - description: - "Show the version number for installed formulae, or only the specified formulae if formula are provided", - }, - { - name: "--multiple", - description: "Only show formulae with multiple versions installed", - }, - { - name: "--pinned", - description: - "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", - }, - { - name: "-1", - description: - "Force output to be one entry per line. This is the default when output is not to a terminal", - }, - { - name: "-l", - description: - "List formulae and/or casks in long format. Has no effect when a formula or cask name is passed as an argument", - }, - { - name: "-r", - description: - "Reverse the order of the formulae and/or casks sort to list the oldest entries first. Has no effect when a formula or cask name is passed as an argument", - }, - { - name: "-t", - description: - "Sort formulae and/or casks by time modified, listing most recently modified first. Has no effect when a formula or cask name is passed as an argument", - }, - ], - args: { - isOptional: true, - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: "leaves", - description: - "List installed formulae that are not dependencies of another installed formula", - options: [ - { - name: ["-r", "--installed-on-request"], - description: "Show manually installed formula", - }, - { - name: ["-p", "--installed-as-dependency"], - description: "Show installed formula as dependencies", - }, - ], - }, - { - name: "doctor", - description: "Check your system for potential problems", - options: [ - ...commonOptions, - { - name: "--list-checks", - description: "List all audit methods", - }, - { - name: ["-D", "--audit-debug"], - description: "Enable debugging and profiling of audit methods", - }, - ], - }, - { - name: ["abv", "info"], - description: "Display brief statistics for your Homebrew installation", - args: { - isVariadic: true, - isOptional: true, - name: "formula", - description: "Formula or cask to summarize", - generators: [generateAllFormulae, generateAllCasks], - }, - options: [ - { - name: ["--cask", "--casks"], - description: "List only casks, or treat all named arguments as casks", - }, - { - name: "--analytics", - description: - "List global Homebrew analytics data or, if specified, installation and build error data for formula", - }, - { - name: "--days", - description: "How many days of analytics data to retrieve", - exclusiveOn: ["--analytics"], - args: { - name: "days", - description: "Number of days of data to retrieve", - suggestions: ["30", "90", "365"], - }, - }, - { - name: "--category", - description: "Which type of analytics data to retrieve", - exclusiveOn: ["--analytics"], - args: { - generators: { - custom: async (ctx) => { - // if anything provided after the subcommand does not begin with '-' - // then a formula has been provided and we should provide info on it - if ( - ctx.slice(2, ctx.length - 1).some((token) => token[0] !== "-") - ) { - return ["install", "install-on-request", "build-error"].map( - (sugg) => ({ - name: sugg, - }) - ); - } + name: "brew", + description: "Package manager for macOS", + subcommands: [ + { + name: "list", + description: "List all installed formulae", + options: [ + ...commonOptions, + { + name: ["--formula", "--formulae"], + description: + "List only formulae, or treat all named arguments as formulae", + }, + { + name: ["--cask", "--casks"], + description: "List only casks, or treat all named arguments as casks", + }, + { + name: "--unbrewed", + description: + "List files in Homebrew's prefix not installed by Homebrew. (disabled; replaced by brew --prefix --unbrewed)", + }, + { + name: "--full-name", + description: + "Print formulae with fully-qualified names. Unless --full-name, --versions or", + }, + { + name: "--pinned", + description: + "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", + }, + { + name: "--versions", + description: + "Show the version number for installed formulae, or only the specified formulae if formula are provided", + }, + { + name: "--multiple", + description: "Only show formulae with multiple versions installed", + }, + { + name: "--pinned", + description: + "List only pinned formulae, or only the specified (pinned) formulae if formula are provided. See also pin, unpin", + }, + { + name: "-1", + description: + "Force output to be one entry per line. This is the default when output is not to a terminal", + }, + { + name: "-l", + description: + "List formulae and/or casks in long format. Has no effect when a formula or cask name is passed as an argument", + }, + { + name: "-r", + description: + "Reverse the order of the formulae and/or casks sort to list the oldest entries first. Has no effect when a formula or cask name is passed as an argument", + }, + { + name: "-t", + description: + "Sort formulae and/or casks by time modified, listing most recently modified first. Has no effect when a formula or cask name is passed as an argument", + }, + ], + args: { + isOptional: true, + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: "ls", + description: "List all installed formulae", + options: [ + ...commonOptions, + { + name: "--formula", + description: + "List only formulae, or treat all named arguments as formulae", + }, + { + name: "--cask", + description: "List only casks, or treat all named arguments as casks", + }, + { + name: "--unbrewed", + description: + "List files in Homebrew's prefix not installed by Homebrew. (disabled; replaced by brew --prefix --unbrewed)", + }, + { + name: "--full-name", + description: + "Print formulae with fully-qualified names. Unless --full-name, --versions or", + }, + { + name: "--pinned", + description: + "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", + }, + { + name: "--versions", + description: + "Show the version number for installed formulae, or only the specified formulae if formula are provided", + }, + { + name: "--multiple", + description: "Only show formulae with multiple versions installed", + }, + { + name: "--pinned", + description: + "List only pinned formulae, or only the specified (pinned) formulae if formula are provided", + }, + { + name: "-1", + description: + "Force output to be one entry per line. This is the default when output is not to a terminal", + }, + { + name: "-l", + description: + "List formulae and/or casks in long format. Has no effect when a formula or cask name is passed as an argument", + }, + { + name: "-r", + description: + "Reverse the order of the formulae and/or casks sort to list the oldest entries first. Has no effect when a formula or cask name is passed as an argument", + }, + { + name: "-t", + description: + "Sort formulae and/or casks by time modified, listing most recently modified first. Has no effect when a formula or cask name is passed as an argument", + }, + ], + args: { + isOptional: true, + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: "leaves", + description: + "List installed formulae that are not dependencies of another installed formula", + options: [ + { + name: ["-r", "--installed-on-request"], + description: "Show manually installed formula", + }, + { + name: ["-p", "--installed-as-dependency"], + description: "Show installed formula as dependencies", + }, + ], + }, + { + name: "doctor", + description: "Check your system for potential problems", + options: [ + ...commonOptions, + { + name: "--list-checks", + description: "List all audit methods", + }, + { + name: ["-D", "--audit-debug"], + description: "Enable debugging and profiling of audit methods", + }, + ], + }, + { + name: ["abv", "info"], + description: "Display brief statistics for your Homebrew installation", + args: { + isVariadic: true, + isOptional: true, + name: "formula", + description: "Formula or cask to summarize", + generators: [generateAllFormulae, generateAllCasks], + }, + options: [ + { + name: ["--cask", "--casks"], + description: "List only casks, or treat all named arguments as casks", + }, + { + name: "--analytics", + description: + "List global Homebrew analytics data or, if specified, installation and build error data for formula", + }, + { + name: "--days", + description: "How many days of analytics data to retrieve", + exclusiveOn: ["--analytics"], + args: { + name: "days", + description: "Number of days of data to retrieve", + suggestions: ["30", "90", "365"], + }, + }, + { + name: "--category", + description: "Which type of analytics data to retrieve", + exclusiveOn: ["--analytics"], + args: { + generators: { + custom: async (ctx) => { + // if anything provided after the subcommand does not begin with '-' + // then a formula has been provided and we should provide info on it + if ( + ctx.slice(2, ctx.length - 1).some((token) => token[0] !== "-") + ) { + return ["install", "install-on-request", "build-error"].map( + (sugg) => ({ + name: sugg, + }) + ); + } - // if no formulas are specified, then we should provide system info - return ["cask-install", "os-version"].map((sugg) => ({ - name: sugg, - })); - }, - }, - }, - }, - { - name: "--github", - description: "Open the GitHub source page for formula in a browser", - }, - { - name: "--json", - description: "Print a JSON representation", - }, - { - name: "--installed", - exclusiveOn: ["--json"], - description: "Print JSON of formulae that are currently installed", - }, - { - name: "--all", - exclusiveOn: ["--json"], - description: "Print JSON of all available formulae", - }, - { - name: ["-v", "--verbose"], - description: "Show more verbose analytics data for formulae", - }, - { - name: "--formula", - description: "Treat all named arguments as formulae", - }, - { - name: "--cash", - description: "Treat all named arguments as casks", - }, - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-q", "--quiet"], - description: "List only the names of outdated kegs", - }, - { - name: ["-h", "--help"], - description: "Get help with services command", - }, - ], - }, - { - name: "update", - description: "Fetch the newest version of Homebrew and all formulae", - options: [ - { - name: ["-f", "--force"], - description: "Always do a slower, full update check", - }, - { - name: ["-v", "--verbose"], - description: - "Print the directories checked and git operations performed", - }, - { - name: ["-d", "--debug"], - description: - "Display a trace of all shell commands as they are executed", - }, - { name: ["-h", "--help"], description: "Show help message" }, - { - name: "--merge", - description: - "Use git merge to apply updates (rather than git rebase)", - }, - { - name: "--preinstall", - description: - "Run on auto-updates (e.g. before brew install). Skips some slower steps", - }, - ], - }, - { - name: "outdated", - description: - "List installed casks and formulae that have an updated version available", - options: [ - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-q", "--quiet"], - description: "List only the names of outdated kegs", - }, - { - name: ["-v", "--verbose"], - description: "Include detailed version information", - }, - { - name: ["-h", "--help"], - description: "Show help message for the outdated command", - }, - { name: "--cask", description: "List only outdated casks" }, - { - name: "--fetch-HEAD", - description: - "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated", - }, - { name: "--formula", description: "List only outdated formulae" }, - { - name: "--greedy", - description: - "Print outdated casks with auto_updates or version :latest", - }, - { - name: "--greedy-latest", - description: - "Print outdated casks including those with version :latest", - }, - { - name: "--greedy-auto-updates", - description: - "Print outdated casks including those with auto_updates true", - }, - { name: "--json", description: "Print output in JSON format" }, - ], - }, - { - name: "pin", - description: "Pin formula, preventing them from being upgraded", - options: commonOptions, - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: "unpin", - description: "Unpin formula, allowing them to be upgraded", - options: commonOptions, - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: "upgrade", - description: - "Upgrade outdated casks and outdated, unpinned formulae using the same options they were originally installed with, plus any appended brew formula options", - options: [ - { - name: ["-d", "--debug"], - description: - "If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory", - }, - { - name: ["-f", "--force"], - description: - "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks, overwrite existing files (binaries and symlinks are excluded, unless originally from the same cask)", - }, - { - name: ["-v", "--verbose"], - description: "Print the verification and postinstall steps", - }, - { - name: ["-n", "--dry-run"], - description: - "Show what would be upgraded, but do not actually upgrade anything", - }, - { - name: ["-s", "--build-from-source"], - description: - "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", - }, - { - name: ["-i", "--interactive"], - description: "Download and patch formula, then open a shell", - }, - { name: ["-g", "--git"], description: "Create a Git repository" }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { name: ["-h", "--help"], description: "Show this message" }, - { - name: ["--formula", "--formulae"], - description: - "Treat all named arguments as formulae. If no named arguments are specified, upgrade only outdated formulae", - }, - { - name: "--env", - description: "Disabled other than for internal Homebrew use", - }, - { - name: "--ignore-dependencies", - description: - "An unsupported Homebrew development flag to skip installing any dependencies of any kind. If the dependencies are not already present, the formula will have issues. If you're not developing Homebrew, consider adjusting your PATH rather than using this flag", - }, - { - name: "--only-dependencies", - description: - "Install the dependencies with specified options but do not install the formula itself", - }, - { - name: "--cc", - description: - "Attempt to compile using the specified compiler, which should be the name of the compiler's executable", - args: { - name: "compiler", - suggestions: ["gcc-7", "llvm_clang", "clang"], - }, - }, - { - name: "--force-bottle", - description: - "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", - }, - { - name: "--include-test", - description: - "Install testing dependencies required to run brew test formula", - }, - { - name: "--HEAD", - description: - "If formula defines it, install the HEAD version, aka. main, trunk, unstable, master", - }, - { - name: "--fetch-HEAD", - description: - "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated. Otherwise, the repository's HEAD will only be checked for updates when a new stable or development version has been released", - }, - { - name: "--ignore-pinned", - description: - "Set a successful exit status even if pinned formulae are not upgraded", - }, - { - name: "--keep-tmp", - description: "Retain the temporary files created during installation", - }, - { - name: "--build-bottle", - description: - "Prepare the formula for eventual bottling during installation, skipping any post-install steps", - }, - { - name: "--bottle-arch", - description: - "Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on", - }, - { - name: "--display-times", - description: - "Print install times for each formula at the end of the run", - }, - { - name: ["--cask", "--casks"], - description: - "Treat all named arguments as casks. If no named arguments are specified, upgrade only outdated casks", - }, - { - name: "--binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - exclusiveOn: ["--no-binaries"], - }, - { - name: "--no-binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - exclusiveOn: ["--binaries"], - }, - { - name: "--require-sha", - description: "Require all casks to have a checksum", - }, - { - name: "--quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - exclusiveOn: ["--no-quarantine"], - }, - { - name: "--no-quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - exclusiveOn: ["--quarantine"], - }, - { - name: "--skip-cask-deps", - description: "Skip installing cask dependencies", - }, - { - name: "--greedy", - description: - "Also include casks with auto_updates true or version :latest", - exclusiveOn: ["--greedy-latest", "--greedy-auto-updates"], - }, - { - name: "--greedy-latest", - description: "Also include casks with version :latest", - }, - { - name: "--greedy-auto-updates", - description: "Also include casks with auto_updates true", - }, - { - name: "--appdir", - description: - "Target location for Applications (default: /Applications)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--colorpickerdir", - description: - "Target location for Color Pickers (default: ~/Library/ColorPickers)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--prefpanedir", - description: - "Target location for Preference Panes (default: ~/Library/PreferencePanes)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--qlplugindir", - description: - "Target location for QuickLook Plugins (default: ~/Library/QuickLook)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--mdimporterdir", - description: - "Target location for Spotlight Plugins (default: ~/Library/Spotlight)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--dictionarydir", - description: - "Target location for Dictionaries (default: ~/Library/Dictionaries)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--fontdir", - description: "Target location for Fonts (default: ~/Library/Fonts)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--servicedir", - description: - "Target location for Services (default: ~/Library/Services)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--input-methoddir", - description: - "Target location for Input Methods (default: ~/Library/Input Methods)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--internet-plugindir", - description: - "Target location for Internet Plugins (default: ~/Library/Internet Plug-Ins)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--audio-unit-plugindir", - description: - "Target location for Audio Unit Plugins (default: ~/Library/Audio/Plug-Ins/Components)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--vst-plugindir", - description: - "Target location for VST Plugins (default: ~/Library/Audio/Plug-Ins/VST)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--vst3-plugindir", - description: - "Target location for VST3 Plugins (default: ~/Library/Audio/Plug-Ins/VST3)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--screen-saverdir", - description: - "Target location for Screen Savers (default: ~/Library/Screen Savers)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--language", - description: - "Comma-separated list of language codes to prefer for cask installation. The first matching language is used, otherwise it reverts to the cask's default language. The default value is the language of your system", - }, - ], - args: { - isVariadic: true, - isOptional: true, - name: "outdated_formula|outdated_cask", - generators: outdatedformulaeGenerator, - }, - }, - { - name: "search", - description: - "Perform a substring search of cask tokens and formula names", - options: [ - ...commonOptions, - { - name: "--formula", - description: "Search online and locally for formulae", - }, - { - name: "--cask", - description: "Search online and locally for casks", - }, - { - name: "--desc", - description: - "Search for formulae with a description matching text and casks with a name matching text", - }, - { - name: "--pull-request", - description: "Search for GitHub pull requests containing text", - }, - { - name: "--open", - description: "Search for only open GitHub pull requests", - }, - { - name: "--closed", - description: "Search for only closed GitHub pull requests", - }, - { - name: ["--repology", "--macports"], - description: "Search for text in the given database", - }, - { - name: ["--fink", "--opensuse"], - description: "Search for text in the given database", - }, - { - name: ["--fedora", "--debian"], - description: "Search for text in the given database", - }, - { - name: "--ubuntu", - description: "Search for text in the given database", - }, - ], - }, - { - name: "config", - description: "Show Homebrew and system configuration info", - }, - { - name: "postinstall", - description: "Rerun the post install step for formula", - options: [ - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-v", "--verbose"], - description: "Make some output more verbose", - }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { name: ["-h", "--help"], description: "Show this message" }, - ], - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: "install", - description: "Install ", - options: [ - { - name: ["-f", "--force"], - description: - "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks", - }, - { - name: ["-v", "--verbose"], - description: "Print the verification and postinstall steps", - }, - { - name: ["-s", "--build-from-source"], - description: - "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", - }, - { - name: ["-i", "--interactive"], - description: "Download and patch formula", - }, - { name: ["-g", "--git"], description: "Create a Git repository" }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { name: ["-h", "--help"], description: "Show this message" }, - { - name: "--formula", - description: "Treat all named arguments as formulae", - }, - { - name: "--env", - description: "Disabled other than for internal Homebrew use", - }, - { - name: "--ignore-dependencies", - description: - "An unsupported Homebrew development flag to skip installing any dependencies of any kind. If the dependencies are not already present, the formula will have issues. If you're not developing Homebrew, consider adjusting your PATH rather than using this flag", - }, - { - name: "--only-dependencies", - description: - "Install the dependencies with specified options but do not install the formula itself", - }, - { - name: "--cc", - description: - "Attempt to compile using the specified compiler, which should be the name of the compiler's executable", - args: { - name: "compiler", - suggestions: ["gcc-7", "llvm_clang", "clang"], - }, - }, - { - name: "--force-bottle", - description: - "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", - }, - { - name: "--include-test", - description: - "Install testing dependencies required to run brew test formula", - }, - { - name: "--HEAD", - description: - "If formula defines it, install the HEAD version, aka. main, trunk, unstable, master", - }, - { - name: "--fetch-HEAD", - description: - "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated. Otherwise, the repository's HEAD will only be checked for updates when a new stable or development version has been released", - }, - { - name: "--keep-tmp", - description: "Retain the temporary files created during installation", - }, - { - name: "--build-bottle", - description: - "Prepare the formula for eventual bottling during installation, skipping any post-install steps", - }, - { - name: "--bottle-arch", - description: - "Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on", - }, - { - name: "--display-times", - description: - "Print install times for each formula at the end of the run", - }, - { - name: "--cask", - description: "--casks Treat all named arguments as casks", - }, - { - name: "--binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - }, - { - name: "--no-binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - }, - { - name: "--require-sha", - description: "Require all casks to have a checksum", - }, - { - name: "--quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - }, - { - name: "--no-quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - }, - { - name: "--skip-cask-deps", - description: "Skip installing cask dependencies", - }, - { - name: "--appdir", - description: - "Target location for Applications (default: /Applications)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--colorpickerdir", - description: - "Target location for Color Pickers (default: ~/Library/ColorPickers)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--prefpanedir", - description: - "Target location for Preference Panes (default: ~/Library/PreferencePanes)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--qlplugindir", - description: - "Target location for QuickLook Plugins (default: ~/Library/QuickLook)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--mdimporterdir", - description: - "Target location for Spotlight Plugins (default: ~/Library/Spotlight)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--dictionarydir", - description: - "Target location for Dictionaries (default: ~/Library/Dictionaries)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--fontdir", - description: "Target location for Fonts (default: ~/Library/Fonts)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--servicedir", - description: - "Target location for Services (default: ~/Library/Services)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--input-methoddir", - description: - "Target location for Input Methods (default: ~/Library/Input Methods)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--internet-plugindir", - description: - "Target location for Internet Plugins (default: ~/Library/Internet Plug-Ins)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--audio-unit-plugindir", - description: - "Target location for Audio Unit Plugins (default: ~/Library/Audio/Plug-Ins/Components)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--vst-plugindir", - description: - "Target location for VST Plugins (default: ~/Library/Audio/Plug-Ins/VST)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--vst3-plugindir", - description: - "Target location for VST3 Plugins (default: ~/Library/Audio/Plug-Ins/VST3)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--screen-saverdir", - description: - "Target location for Screen Savers (default: ~/Library/Screen Savers)", - args: { - name: "location", - template: "folders", - }, - }, - { - name: "--language", - description: - "Comma-separated list of language codes to prefer for cask installation. The first matching language is used, otherwise it reverts to the cask's default language. The default value is the language of your system", - }, - ], - args: { - isVariadic: true, - name: "formula", - description: "Formula or cask to install", - generators: [generateAllFormulae, generateAllCasks], - }, - }, - { - name: "reinstall", - description: - "Uninstall and then reinstall a formula or cask using the same options it was originally installed with, plus any appended options specific to a formula", - options: [ - { - name: ["-d", "--debug"], - description: - "If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory", - }, - { - name: ["-f", "--force"], - description: - "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks", - }, - { - name: ["-v", "--verbose"], - description: "Print the verification and postinstall steps", - }, - { - name: ["-s", "--build-from-source"], - description: - "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", - }, - { - name: ["-i", "--interactive"], - description: "Download and patch formula", - }, - { name: ["-g", "--git"], description: "Create a Git repository" }, - { - name: "--formula", - description: "Treat all named arguments as formulae", - }, - { - name: "--force-bottle", - description: - "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", - }, - { - name: "--keep-tmp", - description: "Retain the temporary files created during installation", - }, - { - name: "--display-times", - description: - "Print install times for each formula at the end of the run", - }, - { - name: "--cask", - description: "--casks Treat all named arguments as casks", - }, - { - name: "--binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - exclusiveOn: ["--no-binaries"], - }, - { - name: "--no-binaries", - description: - "Disable/enable linking of helper executables (default: enabled)", - exclusiveOn: ["--binaries"], - }, - { - name: "--require-sha", - description: "Require all casks to have a checksum", - }, - { - name: "--quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - exclusiveOn: ["--no-quarantine"], - }, - { - name: "--no-quarantine", - description: - "Disable/enable quarantining of downloads (default: enabled)", - exclusiveOn: ["--quarantine"], - }, - { - name: "--skip-cask-deps", - description: "Skip installing cask dependencies", - }, - ], - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - name: ["uninstall", "remove", "rm"], - description: "Uninstall a formula or cask", - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - }, - { - // NOTE: this is actually a command even if it has the double dash in the front - name: "--prefix", - description: "Prefix of ", - args: { - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - options: [ - { - name: "--unbrewed", - description: - "List files in Homebrew's prefix not installed by Homebrew", - }, - { - name: "--installed", - description: - "Outputs nothing and returns a failing status code if formula is not installed", - }, - ], - }, - { - name: "cask", - description: - "Homebrew Cask provides a friendly CLI workflow for the administration of macOS applications distributed as binaries", - subcommands: [ - { - name: "install", - description: "Installs the given cask", - args: { - name: "cask", - description: "Cask to install", - }, - }, - { - name: "uninstall", - description: "Uninstalls the given cask", - options: [ - ...commonOptions, - { - name: "--zap", - description: - "Remove all files associated with a cask. May remove files which are shared between applications", - }, - { - name: "--ignore-dependencies", - description: - "Don't fail uninstall, even if formula is a dependency of any installed formulae", - }, - { - name: "--formula", - description: "Treat all named arguments as formulae", - }, - { - name: "--cask", - description: "Treat all named arguments as casks", - }, - ], - args: { - isVariadic: true, + // if no formulas are specified, then we should provide system info + return ["cask-install", "os-version"].map((sugg) => ({ + name: sugg, + })); + }, + }, + }, + }, + { + name: "--github", + description: "Open the GitHub source page for formula in a browser", + }, + { + name: "--json", + description: "Print a JSON representation", + }, + { + name: "--installed", + exclusiveOn: ["--json"], + description: "Print JSON of formulae that are currently installed", + }, + { + name: "--all", + exclusiveOn: ["--json"], + description: "Print JSON of all available formulae", + }, + { + name: ["-v", "--verbose"], + description: "Show more verbose analytics data for formulae", + }, + { + name: "--formula", + description: "Treat all named arguments as formulae", + }, + { + name: "--cash", + description: "Treat all named arguments as casks", + }, + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-q", "--quiet"], + description: "List only the names of outdated kegs", + }, + { + name: ["-h", "--help"], + description: "Get help with services command", + }, + ], + }, + { + name: "update", + description: "Fetch the newest version of Homebrew and all formulae", + options: [ + { + name: ["-f", "--force"], + description: "Always do a slower, full update check", + }, + { + name: ["-v", "--verbose"], + description: + "Print the directories checked and git operations performed", + }, + { + name: ["-d", "--debug"], + description: + "Display a trace of all shell commands as they are executed", + }, + { name: ["-h", "--help"], description: "Show help message" }, + { + name: "--merge", + description: + "Use git merge to apply updates (rather than git rebase)", + }, + { + name: "--preinstall", + description: + "Run on auto-updates (e.g. before brew install). Skips some slower steps", + }, + ], + }, + { + name: "outdated", + description: + "List installed casks and formulae that have an updated version available", + options: [ + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-q", "--quiet"], + description: "List only the names of outdated kegs", + }, + { + name: ["-v", "--verbose"], + description: "Include detailed version information", + }, + { + name: ["-h", "--help"], + description: "Show help message for the outdated command", + }, + { name: "--cask", description: "List only outdated casks" }, + { + name: "--fetch-HEAD", + description: + "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated", + }, + { name: "--formula", description: "List only outdated formulae" }, + { + name: "--greedy", + description: + "Print outdated casks with auto_updates or version :latest", + }, + { + name: "--greedy-latest", + description: + "Print outdated casks including those with version :latest", + }, + { + name: "--greedy-auto-updates", + description: + "Print outdated casks including those with auto_updates true", + }, + { name: "--json", description: "Print output in JSON format" }, + ], + }, + { + name: "pin", + description: "Pin formula, preventing them from being upgraded", + options: commonOptions, + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: "unpin", + description: "Unpin formula, allowing them to be upgraded", + options: commonOptions, + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: "upgrade", + description: + "Upgrade outdated casks and outdated, unpinned formulae using the same options they were originally installed with, plus any appended brew formula options", + options: [ + { + name: ["-d", "--debug"], + description: + "If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory", + }, + { + name: ["-f", "--force"], + description: + "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks, overwrite existing files (binaries and symlinks are excluded, unless originally from the same cask)", + }, + { + name: ["-v", "--verbose"], + description: "Print the verification and postinstall steps", + }, + { + name: ["-n", "--dry-run"], + description: + "Show what would be upgraded, but do not actually upgrade anything", + }, + { + name: ["-s", "--build-from-source"], + description: + "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", + }, + { + name: ["-i", "--interactive"], + description: "Download and patch formula, then open a shell", + }, + { name: ["-g", "--git"], description: "Create a Git repository" }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { name: ["-h", "--help"], description: "Show this message" }, + { + name: ["--formula", "--formulae"], + description: + "Treat all named arguments as formulae. If no named arguments are specified, upgrade only outdated formulae", + }, + { + name: "--env", + description: "Disabled other than for internal Homebrew use", + }, + { + name: "--ignore-dependencies", + description: + "An unsupported Homebrew development flag to skip installing any dependencies of any kind. If the dependencies are not already present, the formula will have issues. If you're not developing Homebrew, consider adjusting your PATH rather than using this flag", + }, + { + name: "--only-dependencies", + description: + "Install the dependencies with specified options but do not install the formula itself", + }, + { + name: "--cc", + description: + "Attempt to compile using the specified compiler, which should be the name of the compiler's executable", + args: { + name: "compiler", + suggestions: ["gcc-7", "llvm_clang", "clang"], + }, + }, + { + name: "--force-bottle", + description: + "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", + }, + { + name: "--include-test", + description: + "Install testing dependencies required to run brew test formula", + }, + { + name: "--HEAD", + description: + "If formula defines it, install the HEAD version, aka. main, trunk, unstable, master", + }, + { + name: "--fetch-HEAD", + description: + "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated. Otherwise, the repository's HEAD will only be checked for updates when a new stable or development version has been released", + }, + { + name: "--ignore-pinned", + description: + "Set a successful exit status even if pinned formulae are not upgraded", + }, + { + name: "--keep-tmp", + description: "Retain the temporary files created during installation", + }, + { + name: "--build-bottle", + description: + "Prepare the formula for eventual bottling during installation, skipping any post-install steps", + }, + { + name: "--bottle-arch", + description: + "Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on", + }, + { + name: "--display-times", + description: + "Print install times for each formula at the end of the run", + }, + { + name: ["--cask", "--casks"], + description: + "Treat all named arguments as casks. If no named arguments are specified, upgrade only outdated casks", + }, + { + name: "--binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + exclusiveOn: ["--no-binaries"], + }, + { + name: "--no-binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + exclusiveOn: ["--binaries"], + }, + { + name: "--require-sha", + description: "Require all casks to have a checksum", + }, + { + name: "--quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + exclusiveOn: ["--no-quarantine"], + }, + { + name: "--no-quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + exclusiveOn: ["--quarantine"], + }, + { + name: "--skip-cask-deps", + description: "Skip installing cask dependencies", + }, + { + name: "--greedy", + description: + "Also include casks with auto_updates true or version :latest", + exclusiveOn: ["--greedy-latest", "--greedy-auto-updates"], + }, + { + name: "--greedy-latest", + description: "Also include casks with version :latest", + }, + { + name: "--greedy-auto-updates", + description: "Also include casks with auto_updates true", + }, + { + name: "--appdir", + description: + "Target location for Applications (default: /Applications)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--colorpickerdir", + description: + "Target location for Color Pickers (default: ~/Library/ColorPickers)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--prefpanedir", + description: + "Target location for Preference Panes (default: ~/Library/PreferencePanes)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--qlplugindir", + description: + "Target location for QuickLook Plugins (default: ~/Library/QuickLook)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--mdimporterdir", + description: + "Target location for Spotlight Plugins (default: ~/Library/Spotlight)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--dictionarydir", + description: + "Target location for Dictionaries (default: ~/Library/Dictionaries)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--fontdir", + description: "Target location for Fonts (default: ~/Library/Fonts)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--servicedir", + description: + "Target location for Services (default: ~/Library/Services)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--input-methoddir", + description: + "Target location for Input Methods (default: ~/Library/Input Methods)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--internet-plugindir", + description: + "Target location for Internet Plugins (default: ~/Library/Internet Plug-Ins)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--audio-unit-plugindir", + description: + "Target location for Audio Unit Plugins (default: ~/Library/Audio/Plug-Ins/Components)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--vst-plugindir", + description: + "Target location for VST Plugins (default: ~/Library/Audio/Plug-Ins/VST)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--vst3-plugindir", + description: + "Target location for VST3 Plugins (default: ~/Library/Audio/Plug-Ins/VST3)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--screen-saverdir", + description: + "Target location for Screen Savers (default: ~/Library/Screen Savers)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--language", + description: + "Comma-separated list of language codes to prefer for cask installation. The first matching language is used, otherwise it reverts to the cask's default language. The default value is the language of your system", + }, + ], + args: { + isVariadic: true, + isOptional: true, + name: "outdated_formula|outdated_cask", + generators: outdatedformulaeGenerator, + }, + }, + { + name: "search", + description: + "Perform a substring search of cask tokens and formula names", + options: [ + ...commonOptions, + { + name: "--formula", + description: "Search online and locally for formulae", + }, + { + name: "--cask", + description: "Search online and locally for casks", + }, + { + name: "--desc", + description: + "Search for formulae with a description matching text and casks with a name matching text", + }, + { + name: "--pull-request", + description: "Search for GitHub pull requests containing text", + }, + { + name: "--open", + description: "Search for only open GitHub pull requests", + }, + { + name: "--closed", + description: "Search for only closed GitHub pull requests", + }, + { + name: ["--repology", "--macports"], + description: "Search for text in the given database", + }, + { + name: ["--fink", "--opensuse"], + description: "Search for text in the given database", + }, + { + name: ["--fedora", "--debian"], + description: "Search for text in the given database", + }, + { + name: "--ubuntu", + description: "Search for text in the given database", + }, + ], + }, + { + name: "config", + description: "Show Homebrew and system configuration info", + }, + { + name: "postinstall", + description: "Rerun the post install step for formula", + options: [ + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-v", "--verbose"], + description: "Make some output more verbose", + }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { name: ["-h", "--help"], description: "Show this message" }, + ], + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: "install", + description: "Install ", + options: [ + { + name: ["-f", "--force"], + description: + "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks", + }, + { + name: ["-v", "--verbose"], + description: "Print the verification and postinstall steps", + }, + { + name: ["-s", "--build-from-source"], + description: + "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", + }, + { + name: ["-i", "--interactive"], + description: "Download and patch formula", + }, + { name: ["-g", "--git"], description: "Create a Git repository" }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { name: ["-h", "--help"], description: "Show this message" }, + { + name: "--formula", + description: "Treat all named arguments as formulae", + }, + { + name: "--env", + description: "Disabled other than for internal Homebrew use", + }, + { + name: "--ignore-dependencies", + description: + "An unsupported Homebrew development flag to skip installing any dependencies of any kind. If the dependencies are not already present, the formula will have issues. If you're not developing Homebrew, consider adjusting your PATH rather than using this flag", + }, + { + name: "--only-dependencies", + description: + "Install the dependencies with specified options but do not install the formula itself", + }, + { + name: "--cc", + description: + "Attempt to compile using the specified compiler, which should be the name of the compiler's executable", + args: { + name: "compiler", + suggestions: ["gcc-7", "llvm_clang", "clang"], + }, + }, + { + name: "--force-bottle", + description: + "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", + }, + { + name: "--include-test", + description: + "Install testing dependencies required to run brew test formula", + }, + { + name: "--HEAD", + description: + "If formula defines it, install the HEAD version, aka. main, trunk, unstable, master", + }, + { + name: "--fetch-HEAD", + description: + "Fetch the upstream repository to detect if the HEAD installation of the formula is outdated. Otherwise, the repository's HEAD will only be checked for updates when a new stable or development version has been released", + }, + { + name: "--keep-tmp", + description: "Retain the temporary files created during installation", + }, + { + name: "--build-bottle", + description: + "Prepare the formula for eventual bottling during installation, skipping any post-install steps", + }, + { + name: "--bottle-arch", + description: + "Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on", + }, + { + name: "--display-times", + description: + "Print install times for each formula at the end of the run", + }, + { + name: "--cask", + description: "--casks Treat all named arguments as casks", + }, + { + name: "--binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + }, + { + name: "--no-binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + }, + { + name: "--require-sha", + description: "Require all casks to have a checksum", + }, + { + name: "--quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + }, + { + name: "--no-quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + }, + { + name: "--skip-cask-deps", + description: "Skip installing cask dependencies", + }, + { + name: "--appdir", + description: + "Target location for Applications (default: /Applications)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--colorpickerdir", + description: + "Target location for Color Pickers (default: ~/Library/ColorPickers)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--prefpanedir", + description: + "Target location for Preference Panes (default: ~/Library/PreferencePanes)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--qlplugindir", + description: + "Target location for QuickLook Plugins (default: ~/Library/QuickLook)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--mdimporterdir", + description: + "Target location for Spotlight Plugins (default: ~/Library/Spotlight)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--dictionarydir", + description: + "Target location for Dictionaries (default: ~/Library/Dictionaries)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--fontdir", + description: "Target location for Fonts (default: ~/Library/Fonts)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--servicedir", + description: + "Target location for Services (default: ~/Library/Services)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--input-methoddir", + description: + "Target location for Input Methods (default: ~/Library/Input Methods)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--internet-plugindir", + description: + "Target location for Internet Plugins (default: ~/Library/Internet Plug-Ins)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--audio-unit-plugindir", + description: + "Target location for Audio Unit Plugins (default: ~/Library/Audio/Plug-Ins/Components)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--vst-plugindir", + description: + "Target location for VST Plugins (default: ~/Library/Audio/Plug-Ins/VST)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--vst3-plugindir", + description: + "Target location for VST3 Plugins (default: ~/Library/Audio/Plug-Ins/VST3)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--screen-saverdir", + description: + "Target location for Screen Savers (default: ~/Library/Screen Savers)", + args: { + name: "location", + template: "folders", + }, + }, + { + name: "--language", + description: + "Comma-separated list of language codes to prefer for cask installation. The first matching language is used, otherwise it reverts to the cask's default language. The default value is the language of your system", + }, + ], + args: { + isVariadic: true, + name: "formula", + description: "Formula or cask to install", + generators: [generateAllFormulae, generateAllCasks], + }, + }, + { + name: "reinstall", + description: + "Uninstall and then reinstall a formula or cask using the same options it was originally installed with, plus any appended options specific to a formula", + options: [ + { + name: ["-d", "--debug"], + description: + "If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory", + }, + { + name: ["-f", "--force"], + description: + "Install formulae without checking for previously installed keg-only or non-migrated versions. When installing casks", + }, + { + name: ["-v", "--verbose"], + description: "Print the verification and postinstall steps", + }, + { + name: ["-s", "--build-from-source"], + description: + "Compile formula from source even if a bottle is provided. Dependencies will still be installed from bottles if they are available", + }, + { + name: ["-i", "--interactive"], + description: "Download and patch formula", + }, + { name: ["-g", "--git"], description: "Create a Git repository" }, + { + name: "--formula", + description: "Treat all named arguments as formulae", + }, + { + name: "--force-bottle", + description: + "Install from a bottle if it exists for the current or newest version of macOS, even if it would not normally be used for installation", + }, + { + name: "--keep-tmp", + description: "Retain the temporary files created during installation", + }, + { + name: "--display-times", + description: + "Print install times for each formula at the end of the run", + }, + { + name: "--cask", + description: "--casks Treat all named arguments as casks", + }, + { + name: "--binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + exclusiveOn: ["--no-binaries"], + }, + { + name: "--no-binaries", + description: + "Disable/enable linking of helper executables (default: enabled)", + exclusiveOn: ["--binaries"], + }, + { + name: "--require-sha", + description: "Require all casks to have a checksum", + }, + { + name: "--quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + exclusiveOn: ["--no-quarantine"], + }, + { + name: "--no-quarantine", + description: + "Disable/enable quarantining of downloads (default: enabled)", + exclusiveOn: ["--quarantine"], + }, + { + name: "--skip-cask-deps", + description: "Skip installing cask dependencies", + }, + ], + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + name: ["uninstall", "remove", "rm"], + description: "Uninstall a formula or cask", + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + }, + { + // NOTE: this is actually a command even if it has the double dash in the front + name: "--prefix", + description: "Prefix of ", + args: { + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + options: [ + { + name: "--unbrewed", + description: + "List files in Homebrew's prefix not installed by Homebrew", + }, + { + name: "--installed", + description: + "Outputs nothing and returns a failing status code if formula is not installed", + }, + ], + }, + { + name: "cask", + description: + "Homebrew Cask provides a friendly CLI workflow for the administration of macOS applications distributed as binaries", + subcommands: [ + { + name: "install", + description: "Installs the given cask", + args: { + name: "cask", + description: "Cask to install", + }, + }, + { + name: "uninstall", + description: "Uninstalls the given cask", + options: [ + ...commonOptions, + { + name: "--zap", + description: + "Remove all files associated with a cask. May remove files which are shared between applications", + }, + { + name: "--ignore-dependencies", + description: + "Don't fail uninstall, even if formula is a dependency of any installed formulae", + }, + { + name: "--formula", + description: "Treat all named arguments as formulae", + }, + { + name: "--cask", + description: "Treat all named arguments as casks", + }, + ], + args: { + isVariadic: true, - generators: { - script: ["brew", "list", "-1", "--cask"], - postProcess: function (out) { - return out.split("\n").map((formula) => { - return { - name: formula, - icon: "🍺", - description: "Installed formula", - }; - }); - }, - }, - }, - }, - ], - }, - { - name: "cleanup", - description: - "Remove stale lock files and outdated downloads for all formulae and casks and remove old versions of installed formulae", - options: [ - ...commonOptions, - { - name: ["--prune", "--prune=all"], - description: "Remove all cache files older than specified days", - }, - { - name: ["-n", "--dry-run"], - description: - "Show what would be removed, but do not actually remove anything", - }, - { - name: "-s", - description: - "Scrub the cache, including downloads for even the latest versions", - }, - { - name: "--prune-prefix", - description: - "Only prune the symlinks and directories from the prefix and remove no other files", - }, - ], - args: { - isVariadic: true, - isOptional: true, - generators: servicesGenerator("Cleanup"), - }, - }, - { - name: "services", - description: - "Manage background services with macOS' launchctl(1) daemon manager", - options: [ - ...commonOptions, - { - name: "--file", - description: - "Use the plist file from this location to start or run the service", - }, - { - name: "--all", - description: "Run subcommand on all services", - }, - { - name: ["-v", "--verbose"], - description: "Make some output more verbose", - }, - { - name: ["-h", "--help"], - description: "Get help with services command", - }, - ], - subcommands: [ - { - name: "cleanup", - description: "Remove all unused services", - }, - { - name: "list", - description: "List all services", - }, - { - name: "run", - description: - "Run the service formula without registering to launch at login (or boot)", - options: [ - { - name: "--all", - description: "Start all services", - }, - ], - args: { - isVariadic: true, - generators: servicesGenerator("Run"), - }, - }, - { - name: "start", - description: - "Start the service formula immediately and register it to launch at login", - options: [ - { - name: "--all", - description: "Start all services", - }, - ], - args: { - isVariadic: true, - generators: servicesGenerator("Start"), - }, - }, - { - name: "stop", - description: - "Stop the service formula immediately and unregister it from launching at", - options: [ - { - name: "--all", - description: "Start all services", - }, - ], - args: { - isVariadic: true, - generators: servicesGenerator("Stop"), - }, - }, - { - name: "restart", - description: - "Stop (if necessary) and start the service formula immediately and register it to launch at login (or boot)", - options: [ - { - name: "--all", - description: "Start all services", - }, - ], - args: { - isVariadic: true, - generators: servicesGenerator("Restart"), - }, - }, - ], - }, - { - name: "analytics", - description: "Manages analytics preferences", - subcommands: [ - { - name: "on", - description: "Turns on analytics", - }, - { - name: "off", - description: "Turns off analytics", - }, - { - name: "regenerate-uuid", - description: "Regenerate the UUID used for analytics", - }, - ], - }, - { - name: "autoremove", - description: - "Uninstall formulae that were only installed as a dependency of another formula and are now no longer needed", - options: [ - { - name: ["-n", "--dry-run"], - description: - "List what would be uninstalled, but do not actually uninstall anything", - }, - ], - }, - { - name: "tap", - description: "Tap a formula repository", - options: [ - ...commonOptions, - { - name: "--full", - description: - "Convert a shallow clone to a full clone without untapping", - }, - { - name: "--shallow", - description: "Fetch tap as a shallow clone rather than a full clone", - }, - { - name: "--force-auto-update", - description: "Auto-update tap even if it is not hosted on GitHub", - }, - { - name: "--repair", - description: - "Migrate tapped formulae from symlink-based to directory-based structure", - }, - { - name: "--list-pinned", - description: "List all pinned taps", - }, - ], - args: { - name: "user/repo or URL", - }, - }, - { - name: "untap", - description: "Remove a tapped formula repository", - args: { - name: "repository", - generators: repositoriesGenerator(), - }, - options: [ - { - name: ["-f", "--force"], - description: - "Untap even if formulae or casks from this tap are currently installed", - }, - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { - name: ["-v", "--verbose"], - description: "Make some output more verbose", - }, - { - name: ["-h", "--help"], - description: "Show help message", - }, - ], - }, - { - name: "link", - description: - "Symlink all of formula's installed files into Homebrew's prefix", - args: { - isOptional: true, - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - options: [ - { - name: "--overwrite", - description: - "Delete files that already exist in the prefix while linking", - }, - { - name: ["-n", "--dry-run"], - description: - "List files which would be linked or deleted by brew link --overwrite without actually linking or deleting any files", - }, - { - name: ["-f", "--force"], - description: "Allow keg-only formulae to be linked", - }, - { - name: "--HEAD", - description: - "Link the HEAD version of the formula if it is installed", - }, - ], - }, - { - name: "unlink", - description: "Remove symlinks for formula from Homebrew's prefix", - args: { - isOptional: true, - isVariadic: true, - name: "formula", - generators: formulaeGenerator, - }, - options: [ - { - name: ["-n", "--dry-run"], - description: - "List files which would be unlinked without actually unlinking or deleting any files", - }, - ], - }, - { - name: "formulae", - description: "List all available formulae", - }, - { - name: "casks", - description: "List all available casks", - }, - { - name: "edit", - description: "", - args: { - isVariadic: true, - isOptional: true, - name: "formula", - description: "Formula or cask to install", - generators: [generateAllFormulae, generateAllCasks], - }, - options: [ - ...commonOptions, - { - name: ["--formula", "--formulae"], - description: "Treat all named arguments as formulae", - }, - { - name: ["--cask", "--casks"], - description: "Treat all named arguments as casks", - }, - ], - }, - { - name: ["home", "homepage"], - description: - "Open a formula, cask's homepage in a browser, or open Homebrew's own homepage if no argument is provided", - args: { - isVariadic: true, - isOptional: true, - name: "formula", - description: "Formula or cask to open homepage for", - generators: [generateAllFormulae, generateAllCasks], - }, - options: [ - ...commonOptions, - { - name: ["--formula", "--formulae"], - description: "Treat all named arguments as formulae", - }, - { - name: ["--cask", "--casks"], - description: "Treat all named arguments as casks", - }, - ], - }, - { - name: "alias", - description: "Manage custom user created brew aliases", - options: [ - { - name: "--edit", - description: "Edit aliases in a text editor", - }, - { - name: ["-d", "--debug"], - description: "Display any debugging information", - }, - { - name: ["-q", "--quiet"], - description: "Make some output more quiet", - }, - { - name: ["-v", "--verbose"], - description: "Make some output more verbose", - }, - { - name: ["-h", "--help"], - description: "Show help message", - }, - ], - args: { - name: "alias", - generators: generateAliases, - description: "Display the alias command", - isOptional: true, - }, - }, - { - name: "developer", - description: "Display the current state of Homebrew's developer mode", - args: { - name: "state", - description: "Turn Homebrew's developer mode on or off respectively", - suggestions: ["on", "off"], - isOptional: true, - }, - }, - ], - options: [ - { - name: "--version", - description: "The current Homebrew version", - }, - ], - args: { - name: "alias", - generators: generateAliases, - description: "Custom user defined brew alias", - isOptional: true, - }, + generators: { + script: ["brew", "list", "-1", "--cask"], + postProcess: function (out) { + return out.split("\n").map((formula) => { + return { + name: formula, + icon: "🍺", + description: "Installed formula", + }; + }); + }, + }, + }, + }, + ], + }, + { + name: "cleanup", + description: + "Remove stale lock files and outdated downloads for all formulae and casks and remove old versions of installed formulae", + options: [ + ...commonOptions, + { + name: ["--prune", "--prune=all"], + description: "Remove all cache files older than specified days", + }, + { + name: ["-n", "--dry-run"], + description: + "Show what would be removed, but do not actually remove anything", + }, + { + name: "-s", + description: + "Scrub the cache, including downloads for even the latest versions", + }, + { + name: "--prune-prefix", + description: + "Only prune the symlinks and directories from the prefix and remove no other files", + }, + ], + args: { + isVariadic: true, + isOptional: true, + generators: servicesGenerator("Cleanup"), + }, + }, + { + name: "services", + description: + "Manage background services with macOS' launchctl(1) daemon manager", + options: [ + ...commonOptions, + { + name: "--file", + description: + "Use the plist file from this location to start or run the service", + }, + { + name: "--all", + description: "Run subcommand on all services", + }, + { + name: ["-v", "--verbose"], + description: "Make some output more verbose", + }, + { + name: ["-h", "--help"], + description: "Get help with services command", + }, + ], + subcommands: [ + { + name: "cleanup", + description: "Remove all unused services", + }, + { + name: "list", + description: "List all services", + }, + { + name: "run", + description: + "Run the service formula without registering to launch at login (or boot)", + options: [ + { + name: "--all", + description: "Start all services", + }, + ], + args: { + isVariadic: true, + generators: servicesGenerator("Run"), + }, + }, + { + name: "start", + description: + "Start the service formula immediately and register it to launch at login", + options: [ + { + name: "--all", + description: "Start all services", + }, + ], + args: { + isVariadic: true, + generators: servicesGenerator("Start"), + }, + }, + { + name: "stop", + description: + "Stop the service formula immediately and unregister it from launching at", + options: [ + { + name: "--all", + description: "Start all services", + }, + ], + args: { + isVariadic: true, + generators: servicesGenerator("Stop"), + }, + }, + { + name: "restart", + description: + "Stop (if necessary) and start the service formula immediately and register it to launch at login (or boot)", + options: [ + { + name: "--all", + description: "Start all services", + }, + ], + args: { + isVariadic: true, + generators: servicesGenerator("Restart"), + }, + }, + ], + }, + { + name: "analytics", + description: "Manages analytics preferences", + subcommands: [ + { + name: "on", + description: "Turns on analytics", + }, + { + name: "off", + description: "Turns off analytics", + }, + { + name: "regenerate-uuid", + description: "Regenerate the UUID used for analytics", + }, + ], + }, + { + name: "autoremove", + description: + "Uninstall formulae that were only installed as a dependency of another formula and are now no longer needed", + options: [ + { + name: ["-n", "--dry-run"], + description: + "List what would be uninstalled, but do not actually uninstall anything", + }, + ], + }, + { + name: "tap", + description: "Tap a formula repository", + options: [ + ...commonOptions, + { + name: "--full", + description: + "Convert a shallow clone to a full clone without untapping", + }, + { + name: "--shallow", + description: "Fetch tap as a shallow clone rather than a full clone", + }, + { + name: "--force-auto-update", + description: "Auto-update tap even if it is not hosted on GitHub", + }, + { + name: "--repair", + description: + "Migrate tapped formulae from symlink-based to directory-based structure", + }, + { + name: "--list-pinned", + description: "List all pinned taps", + }, + ], + args: { + name: "user/repo or URL", + }, + }, + { + name: "untap", + description: "Remove a tapped formula repository", + args: { + name: "repository", + generators: repositoriesGenerator(), + }, + options: [ + { + name: ["-f", "--force"], + description: + "Untap even if formulae or casks from this tap are currently installed", + }, + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { + name: ["-v", "--verbose"], + description: "Make some output more verbose", + }, + { + name: ["-h", "--help"], + description: "Show help message", + }, + ], + }, + { + name: "link", + description: + "Symlink all of formula's installed files into Homebrew's prefix", + args: { + isOptional: true, + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + options: [ + { + name: "--overwrite", + description: + "Delete files that already exist in the prefix while linking", + }, + { + name: ["-n", "--dry-run"], + description: + "List files which would be linked or deleted by brew link --overwrite without actually linking or deleting any files", + }, + { + name: ["-f", "--force"], + description: "Allow keg-only formulae to be linked", + }, + { + name: "--HEAD", + description: + "Link the HEAD version of the formula if it is installed", + }, + ], + }, + { + name: "unlink", + description: "Remove symlinks for formula from Homebrew's prefix", + args: { + isOptional: true, + isVariadic: true, + name: "formula", + generators: formulaeGenerator, + }, + options: [ + { + name: ["-n", "--dry-run"], + description: + "List files which would be unlinked without actually unlinking or deleting any files", + }, + ], + }, + { + name: "formulae", + description: "List all available formulae", + }, + { + name: "casks", + description: "List all available casks", + }, + { + name: "edit", + description: "", + args: { + isVariadic: true, + isOptional: true, + name: "formula", + description: "Formula or cask to install", + generators: [generateAllFormulae, generateAllCasks], + }, + options: [ + ...commonOptions, + { + name: ["--formula", "--formulae"], + description: "Treat all named arguments as formulae", + }, + { + name: ["--cask", "--casks"], + description: "Treat all named arguments as casks", + }, + ], + }, + { + name: ["home", "homepage"], + description: + "Open a formula, cask's homepage in a browser, or open Homebrew's own homepage if no argument is provided", + args: { + isVariadic: true, + isOptional: true, + name: "formula", + description: "Formula or cask to open homepage for", + generators: [generateAllFormulae, generateAllCasks], + }, + options: [ + ...commonOptions, + { + name: ["--formula", "--formulae"], + description: "Treat all named arguments as formulae", + }, + { + name: ["--cask", "--casks"], + description: "Treat all named arguments as casks", + }, + ], + }, + { + name: "alias", + description: "Manage custom user created brew aliases", + options: [ + { + name: "--edit", + description: "Edit aliases in a text editor", + }, + { + name: ["-d", "--debug"], + description: "Display any debugging information", + }, + { + name: ["-q", "--quiet"], + description: "Make some output more quiet", + }, + { + name: ["-v", "--verbose"], + description: "Make some output more verbose", + }, + { + name: ["-h", "--help"], + description: "Show help message", + }, + ], + args: { + name: "alias", + generators: generateAliases, + description: "Display the alias command", + isOptional: true, + }, + }, + { + name: "developer", + description: "Display the current state of Homebrew's developer mode", + args: { + name: "state", + description: "Turn Homebrew's developer mode on or off respectively", + suggestions: ["on", "off"], + isOptional: true, + }, + }, + ], + options: [ + { + name: "--version", + description: "The current Homebrew version", + }, + ], + args: { + name: "alias", + generators: generateAliases, + description: "Custom user defined brew alias", + isOptional: true, + }, }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/cat.ts b/extensions/terminal-suggest/src/completions/upstream/cat.ts index 4e06dacd..b67e19b9 100644 --- a/extensions/terminal-suggest/src/completions/upstream/cat.ts +++ b/extensions/terminal-suggest/src/completions/upstream/cat.ts @@ -1,50 +1,50 @@ const completionSpec: Fig.Spec = { - name: "cat", - description: "Concatenate and print files", - args: { - isVariadic: true, - template: "filepaths", - }, - options: [ - { - name: "-b", - description: "Number the non-blank output lines, starting at 1", - }, + name: "cat", + description: "Concatenate and print files", + args: { + isVariadic: true, + template: "filepaths", + }, + options: [ + { + name: "-b", + description: "Number the non-blank output lines, starting at 1", + }, - { - name: "-e", - description: - "Display non-printing characters (see the -v option), and display a dollar sign (‘$’) at the end of each line", - }, + { + name: "-e", + description: + "Display non-printing characters (see the -v option), and display a dollar sign (‘$’) at the end of each line", + }, - { - name: "-l", - description: - "Set an exclusive advisory lock on the standard output file descriptor. This lock is set using fcntl(2) with the F_SETLKW command. If the output file is already locked, cat will block until the lock is acquired", - }, + { + name: "-l", + description: + "Set an exclusive advisory lock on the standard output file descriptor. This lock is set using fcntl(2) with the F_SETLKW command. If the output file is already locked, cat will block until the lock is acquired", + }, - { name: "-n", description: "Number the output lines, starting at 1" }, + { name: "-n", description: "Number the output lines, starting at 1" }, - { - name: "-s", - description: - "Squeeze multiple adjacent empty lines, causing the output to be single spaced", - }, + { + name: "-s", + description: + "Squeeze multiple adjacent empty lines, causing the output to be single spaced", + }, - { - name: "-t", - description: - "Display non-printing characters (see the -v option), and display tab characters as ‘^I’", - }, + { + name: "-t", + description: + "Display non-printing characters (see the -v option), and display tab characters as ‘^I’", + }, - { name: "-u", description: "Disable output buffering" }, + { name: "-u", description: "Disable output buffering" }, - { - name: "-v", - description: - "Display non-printing characters so they are visible. Control characters print as ‘^X’ for control-X; the delete character (octal 0177) prints as ‘^?’. Non-ASCII characters (with the high bit set) are printed as ‘M-’ (for meta) followed by the character for the low 7 bits", - }, - ], + { + name: "-v", + description: + "Display non-printing characters so they are visible. Control characters print as ‘^X’ for control-X; the delete character (octal 0177) prints as ‘^?’. Non-ASCII characters (with the high bit set) are printed as ‘M-’ (for meta) followed by the character for the low 7 bits", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/chmod.ts b/extensions/terminal-suggest/src/completions/upstream/chmod.ts index 93bc7264..c3b72a01 100644 --- a/extensions/terminal-suggest/src/completions/upstream/chmod.ts +++ b/extensions/terminal-suggest/src/completions/upstream/chmod.ts @@ -1,88 +1,88 @@ const completionSpec: Fig.Spec = { - name: "chmod", - description: "Change file modes or Access Control Lists", - args: [ - { - name: "mode", - suggestions: [ - // Some of the most common chmod's (non-exhaustive) - { - name: "u+x", - type: "arg", - description: "Give execute permission for the user", - icon: "🔐", - }, - { - name: "a+rx", - type: "arg", - description: "Adds read and execute permissions for all classes", - icon: "🔐", - }, - { - name: "744", - type: "arg", - description: - "Sets read, write, and execute permissions for user, and sets read permission for Group and Others", - icon: "🔐", - }, - { - name: "664", - type: "arg", - description: - "Sets read and write permissions for user and Group, and provides read to Others", - icon: "🔐", - }, - { - name: "777", - type: "arg", - description: "⚠️ allows all actions for all users", - icon: "🔐", - }, - ], - }, - { - // Modifying - template: "filepaths", - }, - ], + name: "chmod", + description: "Change file modes or Access Control Lists", + args: [ + { + name: "mode", + suggestions: [ + // Some of the most common chmod's (non-exhaustive) + { + name: "u+x", + type: "arg", + description: "Give execute permission for the user", + icon: "🔐", + }, + { + name: "a+rx", + type: "arg", + description: "Adds read and execute permissions for all classes", + icon: "🔐", + }, + { + name: "744", + type: "arg", + description: + "Sets read, write, and execute permissions for user, and sets read permission for Group and Others", + icon: "🔐", + }, + { + name: "664", + type: "arg", + description: + "Sets read and write permissions for user and Group, and provides read to Others", + icon: "🔐", + }, + { + name: "777", + type: "arg", + description: "⚠️ allows all actions for all users", + icon: "🔐", + }, + ], + }, + { + // Modifying + template: "filepaths", + }, + ], - options: [ - { - name: "-f", - description: - "Do not display a diagnostic message if chmod could not modify the mode for file, nor modify the exit status to reflect such failures", - }, - { - name: "-H", - description: - "If the -R option is specified, symbolic links on the command line are followed and hence unaffected by the command. (Symbolic links encountered during tree traversal are not followed.)", - }, - { - name: "-h", - description: - "If the file is a symbolic link, change the mode of the link itself rather than the file that the link points to", - }, - { - name: "-L", - description: - "If the -R option is specified, all symbolic links are followed", - }, - { - name: "-P", - description: - "If the -R option is specified, no symbolic links are followed. This is the default", - }, - { - name: "-R", - description: - "Change the modes of the file hierarchies rooted in the files, instead of just the files themselves. Beware of unintentionally matching the ``..'' hard link to the parent directory when using wildcards like ``.*''", - }, - { - name: "-v", - description: - "Cause chmod to be verbose, showing filenames as the mode is modified. If the -v flag is specified more than once, the old and new modes of the file will also be printed, in both octal and symbolic notation", - }, - ], + options: [ + { + name: "-f", + description: + "Do not display a diagnostic message if chmod could not modify the mode for file, nor modify the exit status to reflect such failures", + }, + { + name: "-H", + description: + "If the -R option is specified, symbolic links on the command line are followed and hence unaffected by the command. (Symbolic links encountered during tree traversal are not followed.)", + }, + { + name: "-h", + description: + "If the file is a symbolic link, change the mode of the link itself rather than the file that the link points to", + }, + { + name: "-L", + description: + "If the -R option is specified, all symbolic links are followed", + }, + { + name: "-P", + description: + "If the -R option is specified, no symbolic links are followed. This is the default", + }, + { + name: "-R", + description: + "Change the modes of the file hierarchies rooted in the files, instead of just the files themselves. Beware of unintentionally matching the ``..'' hard link to the parent directory when using wildcards like ``.*''", + }, + { + name: "-v", + description: + "Cause chmod to be verbose, showing filenames as the mode is modified. If the -v flag is specified more than once, the old and new modes of the file will also be printed, in both octal and symbolic notation", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/chown.ts b/extensions/terminal-suggest/src/completions/upstream/chown.ts index c271be0b..3ee4f0f0 100644 --- a/extensions/terminal-suggest/src/completions/upstream/chown.ts +++ b/extensions/terminal-suggest/src/completions/upstream/chown.ts @@ -1,117 +1,117 @@ export const existingUsersandGroups: Fig.Generator = { - custom: async function (tokens, executeShellCommand) { - const colonAdded = tokens.find((token) => token.includes(":")); - const nFlagUsed = tokens.find((token) => /^-.*n.*/.test(token)); + custom: async function (tokens, executeShellCommand) { + const colonAdded = tokens.find((token) => token.includes(":")); + const nFlagUsed = tokens.find((token) => /^-.*n.*/.test(token)); - let shell: string; - // Using `:` as a trigger, check to see if a colon is added - // in the current command. If it is, get the system groups - // else retrieve the list of system users - if (colonAdded) { - const { stdout } = await executeShellCommand({ - command: "bash", - args: [ - "-c", - "dscl . -list /Groups PrimaryGroupID | tr -s ' '| sort -r", - ], - }); - shell = stdout; - } else { - const { stdout } = await executeShellCommand({ - command: "bash", - args: ["-c", "dscl . -list /Users UniqueID | tr -s ' '| sort -r"], - }); - shell = stdout; - } + let shell: string; + // Using `:` as a trigger, check to see if a colon is added + // in the current command. If it is, get the system groups + // else retrieve the list of system users + if (colonAdded) { + const { stdout } = await executeShellCommand({ + command: "bash", + args: [ + "-c", + "dscl . -list /Groups PrimaryGroupID | tr -s ' '| sort -r", + ], + }); + shell = stdout; + } else { + const { stdout } = await executeShellCommand({ + command: "bash", + args: ["-c", "dscl . -list /Users UniqueID | tr -s ' '| sort -r"], + }); + shell = stdout; + } - return ( - shell - .split("\n") - // The shell command retrieves a table - // with rows that look like `user uid` - // so each row is split again to get the - // user/group and uid/gid - .map((line) => line.split(" ")) - .map((value) => { - return { - // If the user has entered the option n - // suggest the uid/gid instead of user/group - name: nFlagUsed ? value[1] : value[0], - description: colonAdded - ? `Group - ${nFlagUsed ? value[0] : `gid: ${value[1]}`}` - : `User - ${nFlagUsed ? value[0] : `uid: ${value[1]}`}`, - icon: colonAdded ? "👥" : "👤", - priority: 90, - }; - }) - ); - }, - trigger: ":", - getQueryTerm: ":", + return ( + shell + .split("\n") + // The shell command retrieves a table + // with rows that look like `user uid` + // so each row is split again to get the + // user/group and uid/gid + .map((line) => line.split(" ")) + .map((value) => { + return { + // If the user has entered the option n + // suggest the uid/gid instead of user/group + name: nFlagUsed ? value[1] : value[0], + description: colonAdded + ? `Group - ${nFlagUsed ? value[0] : `gid: ${value[1]}`}` + : `User - ${nFlagUsed ? value[0] : `uid: ${value[1]}`}`, + icon: colonAdded ? "👥" : "👤", + priority: 90, + }; + }) + ); + }, + trigger: ":", + getQueryTerm: ":", }; const completionSpec: Fig.Spec = { - name: "chown", - description: - "Change the user and/or group ownership of a given file, directory, or symbolic link", - args: [ - { - name: "owner[:group] or :group", - generators: existingUsersandGroups, - }, - { - name: "file/directory", - isVariadic: true, - template: ["filepaths", "folders"], - }, - ], - options: [ - { - name: "-f", - description: - "Don't report any failure to change file owner or group, nor modify the exit status to reflect such failures", - }, - { - name: "-h", - description: - "If the file is a symbolic link, change the user ID and/or the group ID of the link itself", - }, - { - name: "-n", - description: - "Interpret user ID and group ID as numeric, avoiding name lookups", - }, - { - name: "-v", - description: - "Cause chown to be verbose, showing files as the owner is modified", - }, - { - name: "-R", - description: - "Change the user ID and/or the group ID for the file hierarchies rooted in the files instead of just the files themselves", - }, - { - name: "-H", - description: - "If the -R option is specified, symbolic links on the command line are followed", - exclusiveOn: ["-L", "-P"], - dependsOn: ["-R"], - }, - { - name: "-L", - description: - "If the -R option is specified, all symbolic links are followed", - exclusiveOn: ["-H", "-P"], - dependsOn: ["-R"], - }, - { - name: "-P", - description: - "If the -R option is specified, no symbolic links are followed", - exclusiveOn: ["-H", "-L"], - dependsOn: ["-R"], - }, - ], + name: "chown", + description: + "Change the user and/or group ownership of a given file, directory, or symbolic link", + args: [ + { + name: "owner[:group] or :group", + generators: existingUsersandGroups, + }, + { + name: "file/directory", + isVariadic: true, + template: ["filepaths", "folders"], + }, + ], + options: [ + { + name: "-f", + description: + "Don't report any failure to change file owner or group, nor modify the exit status to reflect such failures", + }, + { + name: "-h", + description: + "If the file is a symbolic link, change the user ID and/or the group ID of the link itself", + }, + { + name: "-n", + description: + "Interpret user ID and group ID as numeric, avoiding name lookups", + }, + { + name: "-v", + description: + "Cause chown to be verbose, showing files as the owner is modified", + }, + { + name: "-R", + description: + "Change the user ID and/or the group ID for the file hierarchies rooted in the files instead of just the files themselves", + }, + { + name: "-H", + description: + "If the -R option is specified, symbolic links on the command line are followed", + exclusiveOn: ["-L", "-P"], + dependsOn: ["-R"], + }, + { + name: "-L", + description: + "If the -R option is specified, all symbolic links are followed", + exclusiveOn: ["-H", "-P"], + dependsOn: ["-R"], + }, + { + name: "-P", + description: + "If the -R option is specified, no symbolic links are followed", + exclusiveOn: ["-H", "-L"], + dependsOn: ["-R"], + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/cp.ts b/extensions/terminal-suggest/src/completions/upstream/cp.ts index 65d3d065..91a05f48 100644 --- a/extensions/terminal-suggest/src/completions/upstream/cp.ts +++ b/extensions/terminal-suggest/src/completions/upstream/cp.ts @@ -1,79 +1,79 @@ const completionSpec: Fig.Spec = { - name: "cp", - description: "Copy files and directories", - args: [ - { - name: "source", - template: ["filepaths", "folders"], - isVariadic: true, - }, - { - name: "target", - template: ["filepaths", "folders"], - }, - ], - options: [ - { - name: "-a", - description: - "Preserves structure and attributes of files but not directory structure", - }, - { - name: "-f", - description: - "If the destination file cannot be opened, remove it and create a new file, without prompting for confirmation", - exclusiveOn: ["-n"], - }, - { - name: "-H", - description: - "If the -R option is specified, symbolic links on the command line are followed", - exclusiveOn: ["-L", "-P"], - dependsOn: ["-R"], - }, - { - name: "-i", - description: - "Cause cp to write a prompt to the standard error output before copying a file that would overwrite an existing file", - exclusiveOn: ["-n"], - }, - { - name: "-L", - description: - "If the -R option is specified, all symbolic links are followed", - exclusiveOn: ["-H", "-P"], - dependsOn: ["-R"], - }, - { - name: "-n", - description: "Do not overwrite an existing file", - exclusiveOn: ["-f", "-i"], - }, - { - name: "-P", - description: - "If the -R option is specified, no symbolic links are followed", - exclusiveOn: ["-H", "-L"], - dependsOn: ["-R"], - }, - { - name: "-R", - description: - "If source designates a directory, cp copies the directory and the entire subtree connected at that point. If source ends in a /, the contents of the directory are copied rather than the directory itself", - }, - { - name: "-v", - description: "Cause cp to be verbose, showing files as they are copied", - }, - { - name: "-X", - description: "Do not copy Extended Attributes (EAs) or resource forks", - }, - { - name: "-c", - description: "Copy files using clonefile", - }, - ], + name: "cp", + description: "Copy files and directories", + args: [ + { + name: "source", + template: ["filepaths", "folders"], + isVariadic: true, + }, + { + name: "target", + template: ["filepaths", "folders"], + }, + ], + options: [ + { + name: "-a", + description: + "Preserves structure and attributes of files but not directory structure", + }, + { + name: "-f", + description: + "If the destination file cannot be opened, remove it and create a new file, without prompting for confirmation", + exclusiveOn: ["-n"], + }, + { + name: "-H", + description: + "If the -R option is specified, symbolic links on the command line are followed", + exclusiveOn: ["-L", "-P"], + dependsOn: ["-R"], + }, + { + name: "-i", + description: + "Cause cp to write a prompt to the standard error output before copying a file that would overwrite an existing file", + exclusiveOn: ["-n"], + }, + { + name: "-L", + description: + "If the -R option is specified, all symbolic links are followed", + exclusiveOn: ["-H", "-P"], + dependsOn: ["-R"], + }, + { + name: "-n", + description: "Do not overwrite an existing file", + exclusiveOn: ["-f", "-i"], + }, + { + name: "-P", + description: + "If the -R option is specified, no symbolic links are followed", + exclusiveOn: ["-H", "-L"], + dependsOn: ["-R"], + }, + { + name: "-R", + description: + "If source designates a directory, cp copies the directory and the entire subtree connected at that point. If source ends in a /, the contents of the directory are copied rather than the directory itself", + }, + { + name: "-v", + description: "Cause cp to be verbose, showing files as they are copied", + }, + { + name: "-X", + description: "Do not copy Extended Attributes (EAs) or resource forks", + }, + { + name: "-c", + description: "Copy files using clonefile", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/curl.ts b/extensions/terminal-suggest/src/completions/upstream/curl.ts index d47732b6..eb6cfd31 100644 --- a/extensions/terminal-suggest/src/completions/upstream/curl.ts +++ b/extensions/terminal-suggest/src/completions/upstream/curl.ts @@ -1,914 +1,914 @@ const completionSpec: Fig.Spec = { - name: "curl", - description: "Transfer a URL", - args: { name: "URL", template: "history" }, - options: [ - { - name: ["-a", "--append"], - description: "Append to target file when uploading", - }, - { - name: ["-E", "--cert"], - description: "Client certificate file and password", - args: { - name: "certificate[:password]", - generators: { - getQueryTerm: ":", - }, - }, - }, - { - name: ["-K", "--config"], - description: "Read config from a file", - args: { name: "file", template: "filepaths" }, - }, - { - name: ["-C", "--continue-at"], - description: "Resumed transfer offset", - args: { name: "offset" }, - }, - { - name: ["-b", "--cookie"], - description: "Send cookies from string/file", - args: { name: "data or filename", template: "filepaths" }, - }, - { - name: ["-c", "--cookie-jar"], - description: "Write cookies to after operation", - args: { name: "filename", template: "filepaths" }, - }, - { - name: ["-d", "--data"], - description: "HTTP POST data", - insertValue: "-d '{cursor}'", - args: { name: "data" }, - isRepeatable: true, - }, - { name: ["-q", "--disable"], description: "Disable .curlrc" }, - { - name: ["-D", "--dump-header"], - description: "Write the received headers to ", - args: { name: "filename", template: "filepaths" }, - }, - { - name: ["-f", "--fail"], - description: "Fail silently (no output at all) on HTTP errors", - }, - { - name: ["-F", "--form"], - description: "Specify multipart MIME data", - args: { name: "content" }, - isRepeatable: true, - }, - { - name: ["-P", "--ftp-port"], - description: "Use PORT instead of PASV", - args: { name: "address" }, - }, - { - name: ["-G", "--get"], - description: "Put the post data in the URL and use GET", - }, - { - name: ["-g", "--globoff"], - description: "Disable URL sequences and ranges using {} and []", - }, - { name: ["-I", "--head"], description: "Show document info only" }, - { - name: ["-H", "--header"], - description: "Pass custom header(s) to server", - args: { - name: "header/file", - suggestions: [ - { name: "Content-Type: application/json" }, - { name: "Content-Type: application/x-www-form-urlencoded" }, - ], - }, - }, - { name: ["-h", "--help"], description: "This help text" }, - { name: ["-0", "--http1.0"], description: "Use HTTP 1.0" }, - { - name: ["-i", "--include"], - description: "Include protocol response headers in the output", - }, - { - name: ["-k", "--insecure"], - description: "Allow insecure server connections when using SSL", - }, - { name: ["-4", "--ipv4"], description: "Resolve names to IPv4 addresses" }, - { name: ["-6", "--ipv6"], description: "Resolve names to IPv6 addresses" }, - { - name: ["-j", "--junk-session-cookies"], - description: "Ignore session cookies read from file", - }, - { name: ["-l", "--list-only"], description: "List only mode" }, - { name: ["-L", "--location"], description: "Follow redirects" }, - { name: ["-M", "--manual"], description: "Display the full manual" }, - { - name: ["-m", "--max-time"], - description: "Maximum time allowed for the transfer", - args: { name: "seconds" }, - }, - { - name: ["-n", "--netrc"], - description: "Must read .netrc for user name and password", - }, - { - name: ["-:", "--next"], - description: "Make next URL use its separate set of options", - }, - { - name: ["-N", "--no-buffer"], - description: "Disable buffering of the output stream", - }, - { - name: ["-o", "--output"], - description: "Write to file instead of stdout", - args: { name: "file", template: "filepaths" }, - }, - { - name: ["-#", "--progress-bar"], - description: "Display transfer progress as a bar", - }, - { - name: ["-x", "--proxy"], - description: "[protocol://]host[:port] Use this proxy", - }, - { - name: ["-U", "--proxy-user"], - description: "Proxy user and password", - args: { name: "user:password" }, - }, - { - name: ["-p", "--proxytunnel"], - description: "Operate through an HTTP proxy tunnel (using CONNECT)", - }, - { - name: ["-Q", "--quote"], - description: "Send command(s) to server before transfer", - }, - { - name: ["-r", "--range"], - description: "Retrieve only the bytes within RANGE", - args: { name: "range" }, - }, - { - name: ["-e", "--referer"], - description: "Referrer URL", - args: { name: "URL" }, - }, - { - name: ["-J", "--remote-header-name"], - description: "Use the header-provided filename", - }, - { - name: ["-O", "--remote-name"], - description: "Write output to a file named as the remote file", - }, - { - name: ["-R", "--remote-time"], - description: "Set the remote file's time on the local output", - }, - { - name: ["-X", "--request"], - description: "Specify request command to use", - args: { - name: "command", - suggestions: [ - { name: "GET" }, - { name: "HEAD" }, - { name: "POST" }, - { name: "PUT" }, - { name: "DELETE" }, - { name: "CONNECT" }, - { name: "OPTIONS" }, - { name: "TRACE" }, - { name: "PATCH" }, - ], - }, - }, - { - name: ["-S", "--show-error"], - description: "Show error even when -s is used", - }, - { name: ["-s", "--silent"], description: "Silent mode" }, - { - name: ["-Y", "--speed-limit"], - description: "Stop transfers slower than this", - args: { name: "speed" }, - }, - { - name: ["-y", "--speed-time"], - description: "Trigger 'speed-limit' abort after this time", - args: { name: "seconds" }, - }, - { name: ["-2", "--sslv2"], description: "Use SSLv2" }, - { name: ["-3", "--sslv3"], description: "Use SSLv3" }, - { - name: ["-t", "--telnet-option"], - description: "Set telnet option", - args: { name: "val" }, - }, - { - name: ["-z", "--time-cond"], - description: "Transfer based on a time condition", - args: { name: "time" }, - }, - { name: ["-1", "--tlsv1"], description: "Use TLSv1.0 or greater" }, - { - name: ["-T", "--upload-file"], - description: "Transfer local FILE to destination", - args: { name: "file", template: "filepaths" }, - }, - { name: ["-B", "--use-ascii"], description: "Use ASCII/text transfer" }, - { - name: ["-u", "--user"], - description: "Server user and password", - args: { name: "user:password" }, - }, - { - name: ["-A", "--user-agent"], - description: "Send User-Agent to server", - args: { name: "name" }, - }, - { - name: ["-v", "--verbose"], - description: "Make the operation more talkative", - }, - { name: ["-V", "--version"], description: "Show version number and quit" }, - { - name: ["-w", "--write-out"], - description: "Use output FORMAT after completion", - args: { name: "format" }, - }, - { - name: "--abstract-unix-socket", - description: "Connect via abstract Unix domain socket", - args: { name: "path" }, - }, - { - name: "--alt-svc", - description: "Name> Enable alt-svc with this cache file", - args: { name: "file", template: "filepaths" }, - }, - { name: "--anyauth", description: "Pick any authentication method" }, - { name: "--basic", description: "Use HTTP Basic Authentication" }, - { - name: "--cacert", - description: "CA certificate to verify peer against", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--capath", - description: "CA directory to verify peer against", - args: { name: "dir", template: "folders" }, - }, - { - name: "--cert-status", - description: "Verify the status of the server certificate", - }, - { - name: "--cert-type", - description: "Certificate file type", - args: { - name: "type", - suggestions: [{ name: "DER" }, { name: "PEM" }, { name: "ENG" }], - }, - }, - { - name: "--ciphers", - description: "Of ciphers> SSL ciphers to use", - args: { name: "list" }, - }, - { name: "--compressed", description: "Request compressed response" }, - { name: "--compressed-ssh", description: "Enable SSH compression" }, - { - name: "--connect-timeout", - description: "Maximum time allowed for connection", - args: { name: "seconds" }, - }, - { - name: "--connect-to", - description: "Connect to host", - args: { name: "HOST1:PORT1:HOST2:PORT2" }, - }, - { - name: "--create-dirs", - description: "Create necessary local directory hierarchy", - }, - { name: "--crlf", description: "Convert LF to CRLF in upload" }, - { - name: "--crlfile", - description: "Get a CRL list in PEM format from the given file", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--data-ascii", - description: "HTTP POST ASCII data", - args: { name: "data" }, - }, - { - name: "--data-binary", - description: "HTTP POST binary data", - args: { name: "data" }, - }, - { - name: "--data-raw", - description: "HTTP POST data, '@' allowed", - args: { name: "data" }, - }, - { - name: "--data-urlencode", - description: "HTTP POST data url encoded", - args: { name: "data" }, - }, - { - name: "--delegation", - description: "GSS-API delegation permission", - args: { name: "LEVEL" }, - }, - { name: "--digest", description: "Use HTTP Digest Authentication" }, - { name: "--disable-eprt", description: "Inhibit using EPRT or LPRT" }, - { name: "--disable-epsv", description: "Inhibit using EPSV" }, - { - name: "--disallow-username-in-url", - description: "Disallow username in url", - }, - { - name: "--dns-interface", - description: "Interface to use for DNS requests", - args: { name: "interface" }, - }, - { - name: "--dns-ipv4-addr", - description: "IPv4 address to use for DNS requests", - args: { name: "address" }, - }, - { - name: "--dns-ipv6-addr", - description: "IPv6 address to use for DNS requests", - args: { name: "address" }, - }, - { - name: "--dns-servers", - description: "DNS server addrs to use", - args: { name: "addresses" }, - }, - { - name: "--doh-url", - description: "Resolve host names over DOH", - args: { name: "URL" }, - }, - { - name: "--egd-file", - description: "EGD socket path for random data", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--engine", - description: "Crypto engine to use", - args: { name: "name" }, - }, - { - name: "--etag-compare", - description: - "Make a conditional HTTP request for the ETag read from the given file", - args: { name: "file" }, - }, - { - name: "--etag-save", - description: "Save an HTTP ETag to the specified file", - args: { name: "file" }, - }, - { - name: "--expect100-timeout", - description: "How long to wait for 100-continue", - args: { name: "seconds" }, - }, - { - name: "--fail-early", - description: "Fail on first transfer error, do not continue", - }, - { - name: "--fail-with-body", - description: - "On HTTP errors, return an error and also output any HTML response", - }, - { name: "--false-start", description: "Enable TLS False Start" }, - { - name: "--form-string", - description: "Specify multipart MIME data", - args: { name: "string" }, - }, - { - name: "--ftp-account", - description: "Account data string", - args: { name: "data" }, - }, - { - name: "--ftp-alternative-to-user", - description: "String to replace USER [name]", - args: { name: "command" }, - }, - { - name: "--ftp-create-dirs", - description: "Create the remote dirs if not present", - }, - { - name: "--ftp-method", - description: "Control CWD usage", - args: { name: "method" }, - }, - { name: "--ftp-pasv", description: "Use PASV/EPSV instead of PORT" }, - { name: "--ftp-pret", description: "Send PRET before PASV" }, - { name: "--ftp-skip-pasv-ip", description: "Skip the IP address for PASV" }, - { name: "--ftp-ssl-ccc", description: "Send CCC after authenticating" }, - { - name: "--ftp-ssl-ccc-mode", - description: "Set CCC mode", - args: { - name: "mode", - suggestions: [{ name: "active" }, { name: "passive" }], - }, - }, - { - name: "--ftp-ssl-control", - description: "Require SSL/TLS for FTP login, clear for transfer", - }, - { - name: "--happy-eyeballs-timeout-ms", - description: - "How long to wait in milliseconds for IPv6 before trying IPv4", - args: { name: "milliseconds" }, - }, - { - name: "--haproxy-protocol", - description: "Send HAProxy PROXY protocol v1 header", - }, - { - name: "--hostpubmd5", - description: "Acceptable MD5 hash of the host public key", - args: { name: "md5" }, - }, - { name: "--http0.9", description: "Allow HTTP 0.9 responses" }, - { name: "--http1.1", description: "Use HTTP 1.1" }, - { name: "--http2", description: "Use HTTP 2" }, - { - name: "--http2-prior-knowledge", - description: "Use HTTP 2 without HTTP/1.1 Upgrade", - }, - { - name: "--ignore-content-length", - description: "Ignore the size of the remote resource", - }, - { - name: "--interface", - description: "Use network INTERFACE (or address)", - args: { name: "name" }, - }, - { - name: "--keepalive-time", - description: "Interval time for keepalive probes", - args: { name: "seconds" }, - }, - { - name: "--key", - description: "Private key file name", - args: { name: "key" }, - }, - { - name: "--key-type", - description: "Private key file type", - args: { - name: "type", - suggestions: [{ name: "DER" }, { name: "PEM" }, { name: "ENG" }], - }, - }, - { - name: "--krb", - description: "Enable Kerberos with security ", - args: { name: "level" }, - }, - { - name: "--libcurl", - description: "Dump libcurl equivalent code of this command line", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--limit-rate", - description: "Limit transfer speed to RATE", - args: { name: "speed" }, - }, - { - name: "--local-port", - description: "Force use of RANGE for local port numbers", - args: { name: "num/range" }, - }, - { - name: "--location-trusted", - description: "Like --location, and send auth to other hosts", - }, - { - name: "--login-options", - description: "Server login options", - args: { name: "options" }, - }, - { - name: "--mail-auth", - description: "Originator address of the original email", - args: { name: "address" }, - }, - { - name: "--mail-from", - description: "Mail from this address", - args: { name: "address" }, - }, - { - name: "--mail-rcpt", - description: "Mail to this address", - args: { name: "address" }, - }, - { - name: "--max-filesize", - description: "Maximum file size to download", - args: { name: "bytes" }, - }, - { - name: "--max-redirs", - description: "Maximum number of redirects allowed", - args: { name: "num" }, - }, - { - name: "--metalink", - description: "Process given URLs as metalink XML file", - }, - { - name: "--negotiate", - description: "Use HTTP Negotiate (SPNEGO) authentication", - }, - { - name: "--netrc-file", - description: "Specify FILE for netrc", - args: { name: "filename", template: "filepaths" }, - }, - { name: "--netrc-optional", description: "Use either .netrc or URL" }, - { name: "--no-alpn", description: "Disable the ALPN TLS extension" }, - { - name: "--no-keepalive", - description: "Disable TCP keepalive on the connection", - }, - { name: "--no-npn", description: "Disable the NPN TLS extension" }, - { name: "--no-sessionid", description: "Disable SSL session-ID reusing" }, - { - name: "--noproxy", - description: "List of hosts which do not use proxy", - args: { name: "no-proxy-list" }, - }, - { name: "--ntlm", description: "Use HTTP NTLM authentication" }, - { - name: "--ntlm-wb", - description: "Use HTTP NTLM authentication with winbind", - }, - { - name: "--oauth2-bearer", - description: "OAuth 2 Bearer Token", - args: { name: "token" }, - }, - { - name: "--pass", - description: "Pass phrase for the private key", - args: { name: "phrase" }, - }, - { - name: "--path-as-is", - description: "Do not squash .. sequences in URL path", - }, - { - name: "--pinnedpubkey", - description: "FILE/HASHES Public key to verify peer against", - args: { name: "hashes" }, - }, - { - name: "--post301", - description: "Do not switch to GET after following a 301", - }, - { - name: "--post302", - description: "Do not switch to GET after following a 302", - }, - { - name: "--post303", - description: "Do not switch to GET after following a 303", - }, - { - name: "--preproxy", - description: "[protocol://]host[:port] Use this proxy first", - }, - { - name: "--proto", - description: "Enable/disable PROTOCOLS", - args: { name: "protocols" }, - }, - { - name: "--proto-default", - description: "Use PROTOCOL for any URL missing a scheme", - args: { name: "protocol" }, - }, - { - name: "--proto-redir", - description: "Enable/disable PROTOCOLS on redirect", - args: { name: "protocols" }, - }, - { - name: "--proxy-anyauth", - description: "Pick any proxy authentication method", - }, - { - name: "--proxy-basic", - description: "Use Basic authentication on the proxy", - }, - { - name: "--proxy-cacert", - description: "CA certificate to verify peer against for proxy", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--proxy-capath", - description: "CA directory to verify peer against for proxy", - args: { name: "dir", template: "folders" }, - }, - { - name: "--proxy-cert", - description: "Set client certificate for proxy", - args: { name: "cert[:passwd]" }, - }, - { - name: "--proxy-cert-type", - description: "Client certificate type for HTTPS proxy", - args: { name: "type" }, - }, - { - name: "--proxy-ciphers", - description: "SSL ciphers to use for proxy", - args: { name: "list" }, - }, - { - name: "--proxy-crlfile", - description: "Set a CRL list for proxy", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--proxy-digest", - description: "Use Digest authentication on the proxy", - }, - { - name: "--proxy-header", - description: "Pass custom header(s) to proxy", - args: { - name: "header/file", - suggestions: [ - { name: "Content-Type: application/json" }, - { name: "Content-Type: application/x-www-form-urlencoded" }, - ], - }, - }, - { - name: "--proxy-insecure", - description: "Do HTTPS proxy connections without verifying the proxy", - }, - { - name: "--proxy-key", - description: "Private key for HTTPS proxy", - args: { name: "key" }, - }, - { - name: "--proxy-key-type", - description: "Private key file type for proxy", - args: { name: "type" }, - }, - { - name: "--proxy-negotiate", - description: "Use HTTP Negotiate (SPNEGO) authentication on the proxy", - }, - { - name: "--proxy-ntlm", - description: "Use NTLM authentication on the proxy", - }, - { - name: "--proxy-pass", - description: "Pass phrase for the private key for HTTPS proxy", - args: { name: "phrase" }, - }, - { - name: "--proxy-pinnedpubkey", - description: "FILE/HASHES public key to verify proxy with", - args: { name: "hashes" }, - }, - { - name: "--proxy-service-name", - description: "SPNEGO proxy service name", - args: { name: "name" }, - }, - { - name: "--proxy-ssl-allow-beast", - description: "Allow security flaw for interop for HTTPS proxy", - }, - { - name: "--proxy-tls13-ciphers", - description: "List> TLS 1.3 proxy cipher suites", - args: { name: "ciphersuite" }, - }, - { - name: "--proxy-tlsauthtype", - description: "TLS authentication type for HTTPS proxy", - args: { name: "type" }, - }, - { - name: "--proxy-tlspassword", - description: "TLS password for HTTPS proxy", - args: { name: "string" }, - }, - { - name: "--proxy-tlsuser", - description: "TLS username for HTTPS proxy", - args: { name: "name" }, - }, - { name: "--proxy-tlsv1", description: "Use TLSv1 for HTTPS proxy" }, - { - name: "--proxy1.0", - description: "Use HTTP/1.0 proxy on given port", - args: { name: "host[:port]" }, - }, - { - name: "--pubkey", - description: "SSH Public key file name", - args: { name: "key", template: "filepaths" }, - }, - { - name: "--random-file", - description: "File for reading random data from", - args: { name: "file", template: "filepaths" }, - }, - { name: "--raw", description: 'Do HTTP "raw"; no transfer decoding' }, - { - name: "--remote-name-all", - description: "Use the remote file name for all URLs", - }, - { - name: "--request-target", - description: "Specify the target for this request", - }, - { - name: "--resolve", - description: "Resolve the host+port to this address", - args: { name: "host:port:address[,address]..." }, - }, - { - name: "--retry", - description: "Retry request if transient problems occur", - args: { name: "num" }, - }, - { - name: "--retry-connrefused", - description: "Retry on connection refused (use with --retry)", - }, - { - name: "--retry-delay", - description: "Wait time between retries", - args: { name: "seconds" }, - }, - { - name: "--retry-max-time", - description: "Retry only within this period", - args: { name: "seconds" }, - }, - { - name: "--sasl-ir", - description: "Enable initial response in SASL authentication", - }, - { - name: "--service-name", - description: "SPNEGO service name", - args: { name: "name" }, - }, - { - name: "--socks4", - description: "SOCKS4 proxy on given host + port", - args: { name: "host[:port]" }, - }, - { - name: "--socks4a", - description: "SOCKS4a proxy on given host + port", - args: { name: "host[:port]" }, - }, - { - name: "--socks5", - description: "SOCKS5 proxy on given host + port", - args: { name: "host[:port]" }, - }, - { - name: "--socks5-basic", - description: "Enable username/password auth for SOCKS5 proxies", - }, - { - name: "--socks5-gssapi", - description: "Enable GSS-API auth for SOCKS5 proxies", - }, - { - name: "--socks5-gssapi-nec", - description: "Compatibility with NEC SOCKS5 server", - }, - { - name: "--socks5-gssapi-service", - description: "SOCKS5 proxy service name for GSS-API", - args: { name: "name" }, - }, - { - name: "--socks5-hostname", - description: "SOCKS5 proxy, pass host name to proxy", - args: { name: "host[:port]" }, - }, - { name: "--ssl", description: "Try SSL/TLS" }, - { - name: "--ssl-auto-client-cert", - description: "Obtain and use a client certificate automatically", - }, - { - name: "--ssl-allow-beast", - description: "Allow security flaw to improve interop", - }, - { - name: "--ssl-no-revoke", - description: "Disable cert revocation checks (Schannel)", - }, - { name: "--ssl-reqd", description: "Require SSL/TLS" }, - { name: "--stderr", description: "Where to redirect stderr" }, - { - name: "--styled-output", - description: "Enable styled output for HTTP headers", - }, - { - name: "--suppress-connect-headers", - description: "Suppress proxy CONNECT response headers", - }, - { name: "--tcp-fastopen", description: "Use TCP Fast Open" }, - { name: "--tcp-nodelay", description: "Use the TCP_NODELAY option" }, - { - name: "--tftp-blksize", - description: "Set TFTP BLKSIZE option", - args: { name: "value" }, - }, - { name: "--tftp-no-options", description: "Do not send any TFTP options" }, - { - name: "--tls-max", - description: "Set maximum allowed TLS version", - args: { name: "VERSION" }, - }, - { - name: "--tls13-ciphers", - description: "Of TLS 1.3 ciphersuites> TLS 1.3 cipher suites to use", - args: { name: "list" }, - }, - { - name: "--tlsauthtype", - description: "TLS authentication type", - args: { name: "type" }, - }, - { name: "--tlspassword", description: "TLS password" }, - { name: "--tlsuser", description: "TLS user name", args: { name: "name" } }, - { name: "--tlsv1.0", description: "Use TLSv1.0 or greater" }, - { name: "--tlsv1.1", description: "Use TLSv1.1 or greater" }, - { name: "--tlsv1.2", description: "Use TLSv1.2 or greater" }, - { name: "--tlsv1.3", description: "Use TLSv1.3 or greater" }, - { - name: "--tr-encoding", - description: "Request compressed transfer encoding", - }, - { - name: "--trace", - description: "Write a debug trace to FILE", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--trace-ascii", - description: "Like --trace, but without hex output", - args: { name: "file", template: "filepaths" }, - }, - { - name: "--trace-time", - description: "Add time stamps to trace/verbose output", - }, - { - name: "--unix-socket", - description: "Connect through this Unix domain socket", - args: { name: "path" }, - }, - { name: "--url", description: "URL to work with", args: { name: "url" } }, - { - name: "--xattr", - description: "Store metadata in extended file attributes", - }, - ], + name: "curl", + description: "Transfer a URL", + args: { name: "URL", template: "history" }, + options: [ + { + name: ["-a", "--append"], + description: "Append to target file when uploading", + }, + { + name: ["-E", "--cert"], + description: "Client certificate file and password", + args: { + name: "certificate[:password]", + generators: { + getQueryTerm: ":", + }, + }, + }, + { + name: ["-K", "--config"], + description: "Read config from a file", + args: { name: "file", template: "filepaths" }, + }, + { + name: ["-C", "--continue-at"], + description: "Resumed transfer offset", + args: { name: "offset" }, + }, + { + name: ["-b", "--cookie"], + description: "Send cookies from string/file", + args: { name: "data or filename", template: "filepaths" }, + }, + { + name: ["-c", "--cookie-jar"], + description: "Write cookies to after operation", + args: { name: "filename", template: "filepaths" }, + }, + { + name: ["-d", "--data"], + description: "HTTP POST data", + insertValue: "-d '{cursor}'", + args: { name: "data" }, + isRepeatable: true, + }, + { name: ["-q", "--disable"], description: "Disable .curlrc" }, + { + name: ["-D", "--dump-header"], + description: "Write the received headers to ", + args: { name: "filename", template: "filepaths" }, + }, + { + name: ["-f", "--fail"], + description: "Fail silently (no output at all) on HTTP errors", + }, + { + name: ["-F", "--form"], + description: "Specify multipart MIME data", + args: { name: "content" }, + isRepeatable: true, + }, + { + name: ["-P", "--ftp-port"], + description: "Use PORT instead of PASV", + args: { name: "address" }, + }, + { + name: ["-G", "--get"], + description: "Put the post data in the URL and use GET", + }, + { + name: ["-g", "--globoff"], + description: "Disable URL sequences and ranges using {} and []", + }, + { name: ["-I", "--head"], description: "Show document info only" }, + { + name: ["-H", "--header"], + description: "Pass custom header(s) to server", + args: { + name: "header/file", + suggestions: [ + { name: "Content-Type: application/json" }, + { name: "Content-Type: application/x-www-form-urlencoded" }, + ], + }, + }, + { name: ["-h", "--help"], description: "This help text" }, + { name: ["-0", "--http1.0"], description: "Use HTTP 1.0" }, + { + name: ["-i", "--include"], + description: "Include protocol response headers in the output", + }, + { + name: ["-k", "--insecure"], + description: "Allow insecure server connections when using SSL", + }, + { name: ["-4", "--ipv4"], description: "Resolve names to IPv4 addresses" }, + { name: ["-6", "--ipv6"], description: "Resolve names to IPv6 addresses" }, + { + name: ["-j", "--junk-session-cookies"], + description: "Ignore session cookies read from file", + }, + { name: ["-l", "--list-only"], description: "List only mode" }, + { name: ["-L", "--location"], description: "Follow redirects" }, + { name: ["-M", "--manual"], description: "Display the full manual" }, + { + name: ["-m", "--max-time"], + description: "Maximum time allowed for the transfer", + args: { name: "seconds" }, + }, + { + name: ["-n", "--netrc"], + description: "Must read .netrc for user name and password", + }, + { + name: ["-:", "--next"], + description: "Make next URL use its separate set of options", + }, + { + name: ["-N", "--no-buffer"], + description: "Disable buffering of the output stream", + }, + { + name: ["-o", "--output"], + description: "Write to file instead of stdout", + args: { name: "file", template: "filepaths" }, + }, + { + name: ["-#", "--progress-bar"], + description: "Display transfer progress as a bar", + }, + { + name: ["-x", "--proxy"], + description: "[protocol://]host[:port] Use this proxy", + }, + { + name: ["-U", "--proxy-user"], + description: "Proxy user and password", + args: { name: "user:password" }, + }, + { + name: ["-p", "--proxytunnel"], + description: "Operate through an HTTP proxy tunnel (using CONNECT)", + }, + { + name: ["-Q", "--quote"], + description: "Send command(s) to server before transfer", + }, + { + name: ["-r", "--range"], + description: "Retrieve only the bytes within RANGE", + args: { name: "range" }, + }, + { + name: ["-e", "--referer"], + description: "Referrer URL", + args: { name: "URL" }, + }, + { + name: ["-J", "--remote-header-name"], + description: "Use the header-provided filename", + }, + { + name: ["-O", "--remote-name"], + description: "Write output to a file named as the remote file", + }, + { + name: ["-R", "--remote-time"], + description: "Set the remote file's time on the local output", + }, + { + name: ["-X", "--request"], + description: "Specify request command to use", + args: { + name: "command", + suggestions: [ + { name: "GET" }, + { name: "HEAD" }, + { name: "POST" }, + { name: "PUT" }, + { name: "DELETE" }, + { name: "CONNECT" }, + { name: "OPTIONS" }, + { name: "TRACE" }, + { name: "PATCH" }, + ], + }, + }, + { + name: ["-S", "--show-error"], + description: "Show error even when -s is used", + }, + { name: ["-s", "--silent"], description: "Silent mode" }, + { + name: ["-Y", "--speed-limit"], + description: "Stop transfers slower than this", + args: { name: "speed" }, + }, + { + name: ["-y", "--speed-time"], + description: "Trigger 'speed-limit' abort after this time", + args: { name: "seconds" }, + }, + { name: ["-2", "--sslv2"], description: "Use SSLv2" }, + { name: ["-3", "--sslv3"], description: "Use SSLv3" }, + { + name: ["-t", "--telnet-option"], + description: "Set telnet option", + args: { name: "val" }, + }, + { + name: ["-z", "--time-cond"], + description: "Transfer based on a time condition", + args: { name: "time" }, + }, + { name: ["-1", "--tlsv1"], description: "Use TLSv1.0 or greater" }, + { + name: ["-T", "--upload-file"], + description: "Transfer local FILE to destination", + args: { name: "file", template: "filepaths" }, + }, + { name: ["-B", "--use-ascii"], description: "Use ASCII/text transfer" }, + { + name: ["-u", "--user"], + description: "Server user and password", + args: { name: "user:password" }, + }, + { + name: ["-A", "--user-agent"], + description: "Send User-Agent to server", + args: { name: "name" }, + }, + { + name: ["-v", "--verbose"], + description: "Make the operation more talkative", + }, + { name: ["-V", "--version"], description: "Show version number and quit" }, + { + name: ["-w", "--write-out"], + description: "Use output FORMAT after completion", + args: { name: "format" }, + }, + { + name: "--abstract-unix-socket", + description: "Connect via abstract Unix domain socket", + args: { name: "path" }, + }, + { + name: "--alt-svc", + description: "Name> Enable alt-svc with this cache file", + args: { name: "file", template: "filepaths" }, + }, + { name: "--anyauth", description: "Pick any authentication method" }, + { name: "--basic", description: "Use HTTP Basic Authentication" }, + { + name: "--cacert", + description: "CA certificate to verify peer against", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--capath", + description: "CA directory to verify peer against", + args: { name: "dir", template: "folders" }, + }, + { + name: "--cert-status", + description: "Verify the status of the server certificate", + }, + { + name: "--cert-type", + description: "Certificate file type", + args: { + name: "type", + suggestions: [{ name: "DER" }, { name: "PEM" }, { name: "ENG" }], + }, + }, + { + name: "--ciphers", + description: "Of ciphers> SSL ciphers to use", + args: { name: "list" }, + }, + { name: "--compressed", description: "Request compressed response" }, + { name: "--compressed-ssh", description: "Enable SSH compression" }, + { + name: "--connect-timeout", + description: "Maximum time allowed for connection", + args: { name: "seconds" }, + }, + { + name: "--connect-to", + description: "Connect to host", + args: { name: "HOST1:PORT1:HOST2:PORT2" }, + }, + { + name: "--create-dirs", + description: "Create necessary local directory hierarchy", + }, + { name: "--crlf", description: "Convert LF to CRLF in upload" }, + { + name: "--crlfile", + description: "Get a CRL list in PEM format from the given file", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--data-ascii", + description: "HTTP POST ASCII data", + args: { name: "data" }, + }, + { + name: "--data-binary", + description: "HTTP POST binary data", + args: { name: "data" }, + }, + { + name: "--data-raw", + description: "HTTP POST data, '@' allowed", + args: { name: "data" }, + }, + { + name: "--data-urlencode", + description: "HTTP POST data url encoded", + args: { name: "data" }, + }, + { + name: "--delegation", + description: "GSS-API delegation permission", + args: { name: "LEVEL" }, + }, + { name: "--digest", description: "Use HTTP Digest Authentication" }, + { name: "--disable-eprt", description: "Inhibit using EPRT or LPRT" }, + { name: "--disable-epsv", description: "Inhibit using EPSV" }, + { + name: "--disallow-username-in-url", + description: "Disallow username in url", + }, + { + name: "--dns-interface", + description: "Interface to use for DNS requests", + args: { name: "interface" }, + }, + { + name: "--dns-ipv4-addr", + description: "IPv4 address to use for DNS requests", + args: { name: "address" }, + }, + { + name: "--dns-ipv6-addr", + description: "IPv6 address to use for DNS requests", + args: { name: "address" }, + }, + { + name: "--dns-servers", + description: "DNS server addrs to use", + args: { name: "addresses" }, + }, + { + name: "--doh-url", + description: "Resolve host names over DOH", + args: { name: "URL" }, + }, + { + name: "--egd-file", + description: "EGD socket path for random data", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--engine", + description: "Crypto engine to use", + args: { name: "name" }, + }, + { + name: "--etag-compare", + description: + "Make a conditional HTTP request for the ETag read from the given file", + args: { name: "file" }, + }, + { + name: "--etag-save", + description: "Save an HTTP ETag to the specified file", + args: { name: "file" }, + }, + { + name: "--expect100-timeout", + description: "How long to wait for 100-continue", + args: { name: "seconds" }, + }, + { + name: "--fail-early", + description: "Fail on first transfer error, do not continue", + }, + { + name: "--fail-with-body", + description: + "On HTTP errors, return an error and also output any HTML response", + }, + { name: "--false-start", description: "Enable TLS False Start" }, + { + name: "--form-string", + description: "Specify multipart MIME data", + args: { name: "string" }, + }, + { + name: "--ftp-account", + description: "Account data string", + args: { name: "data" }, + }, + { + name: "--ftp-alternative-to-user", + description: "String to replace USER [name]", + args: { name: "command" }, + }, + { + name: "--ftp-create-dirs", + description: "Create the remote dirs if not present", + }, + { + name: "--ftp-method", + description: "Control CWD usage", + args: { name: "method" }, + }, + { name: "--ftp-pasv", description: "Use PASV/EPSV instead of PORT" }, + { name: "--ftp-pret", description: "Send PRET before PASV" }, + { name: "--ftp-skip-pasv-ip", description: "Skip the IP address for PASV" }, + { name: "--ftp-ssl-ccc", description: "Send CCC after authenticating" }, + { + name: "--ftp-ssl-ccc-mode", + description: "Set CCC mode", + args: { + name: "mode", + suggestions: [{ name: "active" }, { name: "passive" }], + }, + }, + { + name: "--ftp-ssl-control", + description: "Require SSL/TLS for FTP login, clear for transfer", + }, + { + name: "--happy-eyeballs-timeout-ms", + description: + "How long to wait in milliseconds for IPv6 before trying IPv4", + args: { name: "milliseconds" }, + }, + { + name: "--haproxy-protocol", + description: "Send HAProxy PROXY protocol v1 header", + }, + { + name: "--hostpubmd5", + description: "Acceptable MD5 hash of the host public key", + args: { name: "md5" }, + }, + { name: "--http0.9", description: "Allow HTTP 0.9 responses" }, + { name: "--http1.1", description: "Use HTTP 1.1" }, + { name: "--http2", description: "Use HTTP 2" }, + { + name: "--http2-prior-knowledge", + description: "Use HTTP 2 without HTTP/1.1 Upgrade", + }, + { + name: "--ignore-content-length", + description: "Ignore the size of the remote resource", + }, + { + name: "--interface", + description: "Use network INTERFACE (or address)", + args: { name: "name" }, + }, + { + name: "--keepalive-time", + description: "Interval time for keepalive probes", + args: { name: "seconds" }, + }, + { + name: "--key", + description: "Private key file name", + args: { name: "key" }, + }, + { + name: "--key-type", + description: "Private key file type", + args: { + name: "type", + suggestions: [{ name: "DER" }, { name: "PEM" }, { name: "ENG" }], + }, + }, + { + name: "--krb", + description: "Enable Kerberos with security ", + args: { name: "level" }, + }, + { + name: "--libcurl", + description: "Dump libcurl equivalent code of this command line", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--limit-rate", + description: "Limit transfer speed to RATE", + args: { name: "speed" }, + }, + { + name: "--local-port", + description: "Force use of RANGE for local port numbers", + args: { name: "num/range" }, + }, + { + name: "--location-trusted", + description: "Like --location, and send auth to other hosts", + }, + { + name: "--login-options", + description: "Server login options", + args: { name: "options" }, + }, + { + name: "--mail-auth", + description: "Originator address of the original email", + args: { name: "address" }, + }, + { + name: "--mail-from", + description: "Mail from this address", + args: { name: "address" }, + }, + { + name: "--mail-rcpt", + description: "Mail to this address", + args: { name: "address" }, + }, + { + name: "--max-filesize", + description: "Maximum file size to download", + args: { name: "bytes" }, + }, + { + name: "--max-redirs", + description: "Maximum number of redirects allowed", + args: { name: "num" }, + }, + { + name: "--metalink", + description: "Process given URLs as metalink XML file", + }, + { + name: "--negotiate", + description: "Use HTTP Negotiate (SPNEGO) authentication", + }, + { + name: "--netrc-file", + description: "Specify FILE for netrc", + args: { name: "filename", template: "filepaths" }, + }, + { name: "--netrc-optional", description: "Use either .netrc or URL" }, + { name: "--no-alpn", description: "Disable the ALPN TLS extension" }, + { + name: "--no-keepalive", + description: "Disable TCP keepalive on the connection", + }, + { name: "--no-npn", description: "Disable the NPN TLS extension" }, + { name: "--no-sessionid", description: "Disable SSL session-ID reusing" }, + { + name: "--noproxy", + description: "List of hosts which do not use proxy", + args: { name: "no-proxy-list" }, + }, + { name: "--ntlm", description: "Use HTTP NTLM authentication" }, + { + name: "--ntlm-wb", + description: "Use HTTP NTLM authentication with winbind", + }, + { + name: "--oauth2-bearer", + description: "OAuth 2 Bearer Token", + args: { name: "token" }, + }, + { + name: "--pass", + description: "Pass phrase for the private key", + args: { name: "phrase" }, + }, + { + name: "--path-as-is", + description: "Do not squash .. sequences in URL path", + }, + { + name: "--pinnedpubkey", + description: "FILE/HASHES Public key to verify peer against", + args: { name: "hashes" }, + }, + { + name: "--post301", + description: "Do not switch to GET after following a 301", + }, + { + name: "--post302", + description: "Do not switch to GET after following a 302", + }, + { + name: "--post303", + description: "Do not switch to GET after following a 303", + }, + { + name: "--preproxy", + description: "[protocol://]host[:port] Use this proxy first", + }, + { + name: "--proto", + description: "Enable/disable PROTOCOLS", + args: { name: "protocols" }, + }, + { + name: "--proto-default", + description: "Use PROTOCOL for any URL missing a scheme", + args: { name: "protocol" }, + }, + { + name: "--proto-redir", + description: "Enable/disable PROTOCOLS on redirect", + args: { name: "protocols" }, + }, + { + name: "--proxy-anyauth", + description: "Pick any proxy authentication method", + }, + { + name: "--proxy-basic", + description: "Use Basic authentication on the proxy", + }, + { + name: "--proxy-cacert", + description: "CA certificate to verify peer against for proxy", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--proxy-capath", + description: "CA directory to verify peer against for proxy", + args: { name: "dir", template: "folders" }, + }, + { + name: "--proxy-cert", + description: "Set client certificate for proxy", + args: { name: "cert[:passwd]" }, + }, + { + name: "--proxy-cert-type", + description: "Client certificate type for HTTPS proxy", + args: { name: "type" }, + }, + { + name: "--proxy-ciphers", + description: "SSL ciphers to use for proxy", + args: { name: "list" }, + }, + { + name: "--proxy-crlfile", + description: "Set a CRL list for proxy", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--proxy-digest", + description: "Use Digest authentication on the proxy", + }, + { + name: "--proxy-header", + description: "Pass custom header(s) to proxy", + args: { + name: "header/file", + suggestions: [ + { name: "Content-Type: application/json" }, + { name: "Content-Type: application/x-www-form-urlencoded" }, + ], + }, + }, + { + name: "--proxy-insecure", + description: "Do HTTPS proxy connections without verifying the proxy", + }, + { + name: "--proxy-key", + description: "Private key for HTTPS proxy", + args: { name: "key" }, + }, + { + name: "--proxy-key-type", + description: "Private key file type for proxy", + args: { name: "type" }, + }, + { + name: "--proxy-negotiate", + description: "Use HTTP Negotiate (SPNEGO) authentication on the proxy", + }, + { + name: "--proxy-ntlm", + description: "Use NTLM authentication on the proxy", + }, + { + name: "--proxy-pass", + description: "Pass phrase for the private key for HTTPS proxy", + args: { name: "phrase" }, + }, + { + name: "--proxy-pinnedpubkey", + description: "FILE/HASHES public key to verify proxy with", + args: { name: "hashes" }, + }, + { + name: "--proxy-service-name", + description: "SPNEGO proxy service name", + args: { name: "name" }, + }, + { + name: "--proxy-ssl-allow-beast", + description: "Allow security flaw for interop for HTTPS proxy", + }, + { + name: "--proxy-tls13-ciphers", + description: "List> TLS 1.3 proxy cipher suites", + args: { name: "ciphersuite" }, + }, + { + name: "--proxy-tlsauthtype", + description: "TLS authentication type for HTTPS proxy", + args: { name: "type" }, + }, + { + name: "--proxy-tlspassword", + description: "TLS password for HTTPS proxy", + args: { name: "string" }, + }, + { + name: "--proxy-tlsuser", + description: "TLS username for HTTPS proxy", + args: { name: "name" }, + }, + { name: "--proxy-tlsv1", description: "Use TLSv1 for HTTPS proxy" }, + { + name: "--proxy1.0", + description: "Use HTTP/1.0 proxy on given port", + args: { name: "host[:port]" }, + }, + { + name: "--pubkey", + description: "SSH Public key file name", + args: { name: "key", template: "filepaths" }, + }, + { + name: "--random-file", + description: "File for reading random data from", + args: { name: "file", template: "filepaths" }, + }, + { name: "--raw", description: 'Do HTTP "raw"; no transfer decoding' }, + { + name: "--remote-name-all", + description: "Use the remote file name for all URLs", + }, + { + name: "--request-target", + description: "Specify the target for this request", + }, + { + name: "--resolve", + description: "Resolve the host+port to this address", + args: { name: "host:port:address[,address]..." }, + }, + { + name: "--retry", + description: "Retry request if transient problems occur", + args: { name: "num" }, + }, + { + name: "--retry-connrefused", + description: "Retry on connection refused (use with --retry)", + }, + { + name: "--retry-delay", + description: "Wait time between retries", + args: { name: "seconds" }, + }, + { + name: "--retry-max-time", + description: "Retry only within this period", + args: { name: "seconds" }, + }, + { + name: "--sasl-ir", + description: "Enable initial response in SASL authentication", + }, + { + name: "--service-name", + description: "SPNEGO service name", + args: { name: "name" }, + }, + { + name: "--socks4", + description: "SOCKS4 proxy on given host + port", + args: { name: "host[:port]" }, + }, + { + name: "--socks4a", + description: "SOCKS4a proxy on given host + port", + args: { name: "host[:port]" }, + }, + { + name: "--socks5", + description: "SOCKS5 proxy on given host + port", + args: { name: "host[:port]" }, + }, + { + name: "--socks5-basic", + description: "Enable username/password auth for SOCKS5 proxies", + }, + { + name: "--socks5-gssapi", + description: "Enable GSS-API auth for SOCKS5 proxies", + }, + { + name: "--socks5-gssapi-nec", + description: "Compatibility with NEC SOCKS5 server", + }, + { + name: "--socks5-gssapi-service", + description: "SOCKS5 proxy service name for GSS-API", + args: { name: "name" }, + }, + { + name: "--socks5-hostname", + description: "SOCKS5 proxy, pass host name to proxy", + args: { name: "host[:port]" }, + }, + { name: "--ssl", description: "Try SSL/TLS" }, + { + name: "--ssl-auto-client-cert", + description: "Obtain and use a client certificate automatically", + }, + { + name: "--ssl-allow-beast", + description: "Allow security flaw to improve interop", + }, + { + name: "--ssl-no-revoke", + description: "Disable cert revocation checks (Schannel)", + }, + { name: "--ssl-reqd", description: "Require SSL/TLS" }, + { name: "--stderr", description: "Where to redirect stderr" }, + { + name: "--styled-output", + description: "Enable styled output for HTTP headers", + }, + { + name: "--suppress-connect-headers", + description: "Suppress proxy CONNECT response headers", + }, + { name: "--tcp-fastopen", description: "Use TCP Fast Open" }, + { name: "--tcp-nodelay", description: "Use the TCP_NODELAY option" }, + { + name: "--tftp-blksize", + description: "Set TFTP BLKSIZE option", + args: { name: "value" }, + }, + { name: "--tftp-no-options", description: "Do not send any TFTP options" }, + { + name: "--tls-max", + description: "Set maximum allowed TLS version", + args: { name: "VERSION" }, + }, + { + name: "--tls13-ciphers", + description: "Of TLS 1.3 ciphersuites> TLS 1.3 cipher suites to use", + args: { name: "list" }, + }, + { + name: "--tlsauthtype", + description: "TLS authentication type", + args: { name: "type" }, + }, + { name: "--tlspassword", description: "TLS password" }, + { name: "--tlsuser", description: "TLS user name", args: { name: "name" } }, + { name: "--tlsv1.0", description: "Use TLSv1.0 or greater" }, + { name: "--tlsv1.1", description: "Use TLSv1.1 or greater" }, + { name: "--tlsv1.2", description: "Use TLSv1.2 or greater" }, + { name: "--tlsv1.3", description: "Use TLSv1.3 or greater" }, + { + name: "--tr-encoding", + description: "Request compressed transfer encoding", + }, + { + name: "--trace", + description: "Write a debug trace to FILE", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--trace-ascii", + description: "Like --trace, but without hex output", + args: { name: "file", template: "filepaths" }, + }, + { + name: "--trace-time", + description: "Add time stamps to trace/verbose output", + }, + { + name: "--unix-socket", + description: "Connect through this Unix domain socket", + args: { name: "path" }, + }, + { name: "--url", description: "URL to work with", args: { name: "url" } }, + { + name: "--xattr", + description: "Store metadata in extended file attributes", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/df.ts b/extensions/terminal-suggest/src/completions/upstream/df.ts index e7e00546..42f577d8 100644 --- a/extensions/terminal-suggest/src/completions/upstream/df.ts +++ b/extensions/terminal-suggest/src/completions/upstream/df.ts @@ -1,65 +1,65 @@ const completionSpec: Fig.Spec = { - name: "df", - description: "Display free disk space", - args: { - name: "file or filesystem", - }, - options: [ - { - name: "-a", - description: "Show all mount points", - }, - { - name: ["-b", "-P"], - description: "Use 512-byte blocks (default)", - exclusiveOn: ["-g", "-k", "-m"], - }, - { - name: "-g", - description: "Use 1073741824-byte (1-Gbyte) blocks", - exclusiveOn: ["-b", "-P", "-m", "-k"], - }, - { - name: "-m", - description: "Use 1048576-byte (1-Mbyte) blocks", - exclusiveOn: ["-b", "-P", "-g", "-k"], - }, - { - name: "-k", - description: "Use 1024-byte (1-Kbyte) blocks", - exclusiveOn: ["-b", "-P", "-g", "-m"], - }, - { - name: "-H", - description: '"Human-readable" output, uses base 10 unit suffixes', - exclusiveOn: ["-h"], - }, - { - name: "-h", - description: '"Human-readable" output, uses base 2 unit suffixes', - exclusiveOn: ["-H"], - }, - { - name: "-i", - description: "Include the number of free inodes", - }, - { - name: "-l", - description: "Only display information about locally-mounted filesystems", - }, - { - name: "-n", - description: "Print out the previously obtained statistics", - }, - { - name: "-T", - description: - "Only print out statistics for filesystems of the specified types (comma separated)", - args: { - name: "filesystem", - }, - }, - ], + name: "df", + description: "Display free disk space", + args: { + name: "file or filesystem", + }, + options: [ + { + name: "-a", + description: "Show all mount points", + }, + { + name: ["-b", "-P"], + description: "Use 512-byte blocks (default)", + exclusiveOn: ["-g", "-k", "-m"], + }, + { + name: "-g", + description: "Use 1073741824-byte (1-Gbyte) blocks", + exclusiveOn: ["-b", "-P", "-m", "-k"], + }, + { + name: "-m", + description: "Use 1048576-byte (1-Mbyte) blocks", + exclusiveOn: ["-b", "-P", "-g", "-k"], + }, + { + name: "-k", + description: "Use 1024-byte (1-Kbyte) blocks", + exclusiveOn: ["-b", "-P", "-g", "-m"], + }, + { + name: "-H", + description: '"Human-readable" output, uses base 10 unit suffixes', + exclusiveOn: ["-h"], + }, + { + name: "-h", + description: '"Human-readable" output, uses base 2 unit suffixes', + exclusiveOn: ["-H"], + }, + { + name: "-i", + description: "Include the number of free inodes", + }, + { + name: "-l", + description: "Only display information about locally-mounted filesystems", + }, + { + name: "-n", + description: "Print out the previously obtained statistics", + }, + { + name: "-T", + description: + "Only print out statistics for filesystems of the specified types (comma separated)", + args: { + name: "filesystem", + }, + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/du.ts b/extensions/terminal-suggest/src/completions/upstream/du.ts index 833fa526..ff3d6e42 100644 --- a/extensions/terminal-suggest/src/completions/upstream/du.ts +++ b/extensions/terminal-suggest/src/completions/upstream/du.ts @@ -1,93 +1,93 @@ const completionSpec: Fig.Spec = { - name: "du", - description: "Display disk usage statistics", - options: [ - { - name: "-a", - description: "Display an entry for each file in a file hierarchy", - exclusiveOn: ["-s", "-d"], - }, - { - name: "-c", - description: "Display a grand total", - }, - { - name: "-H", - description: - "Symbolic links on the command line are followed, symbolic links in file hierarchies are not followed", - exclusiveOn: ["-L", "-P"], - }, - { - name: "-h", - description: - '"Human-readable" output. Use unit suffixes: Byte, Kilobyte, Megabyte, Gigabyte, Terabyte and Petabyte', - exclusiveOn: ["-k", "-m", "-g"], - }, - { - name: "-g", - description: "Display block counts in 1073741824-byte (1-Gbyte) blocks", - exclusiveOn: ["-k", "-m", "-h"], - }, - { - name: "-k", - description: "Display block counts in 1024-byte (1-Kbyte) blocks", - exclusiveOn: ["-g", "-m", "-h"], - }, - { - name: "-m", - description: "Display block counts in 1048576-byte (1-Mbyte) blocks", - exclusiveOn: ["-g", "-k", "-h"], - }, - { - name: "-I", - description: "Ignore files and directories matching the specified mask", - args: { - name: "mask", - }, - }, - { - name: "-L", - description: - "Symbolic links on the command line and in file hierarchies are followed", - exclusiveOn: ["-H", "-P"], - }, - { - name: "-r", - description: - "Generate messages about directories that cannot be read, files that cannot be opened, and so on. This is the default case. This option exists solely for conformance with X/Open Portability Guide Issue 4 (``XPG4'')", - }, - { - name: "-P", - description: "No symbolic links are followed. This is the default", - exclusiveOn: ["-H", "-L"], - }, - { - name: "-d", - description: - "Display an entry for all files and directories depth directories deep", - exclusiveOn: ["-a", "-s"], - args: { - name: "depth", - suggestions: ["0", "1", "2"], - }, - }, - { - name: "-s", - description: - "Display an entry for each specified file. (Equivalent to -d 0)", - exclusiveOn: ["-a", "-d"], - }, - { - name: "-x", - description: - "Display an entry for each specified file. (Equivalent to -d 0)", - }, - ], - args: { - isOptional: true, - name: "files", - isVariadic: true, - template: ["filepaths", "folders"], - }, + name: "du", + description: "Display disk usage statistics", + options: [ + { + name: "-a", + description: "Display an entry for each file in a file hierarchy", + exclusiveOn: ["-s", "-d"], + }, + { + name: "-c", + description: "Display a grand total", + }, + { + name: "-H", + description: + "Symbolic links on the command line are followed, symbolic links in file hierarchies are not followed", + exclusiveOn: ["-L", "-P"], + }, + { + name: "-h", + description: + '"Human-readable" output. Use unit suffixes: Byte, Kilobyte, Megabyte, Gigabyte, Terabyte and Petabyte', + exclusiveOn: ["-k", "-m", "-g"], + }, + { + name: "-g", + description: "Display block counts in 1073741824-byte (1-Gbyte) blocks", + exclusiveOn: ["-k", "-m", "-h"], + }, + { + name: "-k", + description: "Display block counts in 1024-byte (1-Kbyte) blocks", + exclusiveOn: ["-g", "-m", "-h"], + }, + { + name: "-m", + description: "Display block counts in 1048576-byte (1-Mbyte) blocks", + exclusiveOn: ["-g", "-k", "-h"], + }, + { + name: "-I", + description: "Ignore files and directories matching the specified mask", + args: { + name: "mask", + }, + }, + { + name: "-L", + description: + "Symbolic links on the command line and in file hierarchies are followed", + exclusiveOn: ["-H", "-P"], + }, + { + name: "-r", + description: + "Generate messages about directories that cannot be read, files that cannot be opened, and so on. This is the default case. This option exists solely for conformance with X/Open Portability Guide Issue 4 (``XPG4'')", + }, + { + name: "-P", + description: "No symbolic links are followed. This is the default", + exclusiveOn: ["-H", "-L"], + }, + { + name: "-d", + description: + "Display an entry for all files and directories depth directories deep", + exclusiveOn: ["-a", "-s"], + args: { + name: "depth", + suggestions: ["0", "1", "2"], + }, + }, + { + name: "-s", + description: + "Display an entry for each specified file. (Equivalent to -d 0)", + exclusiveOn: ["-a", "-d"], + }, + { + name: "-x", + description: + "Display an entry for each specified file. (Equivalent to -d 0)", + }, + ], + args: { + isOptional: true, + name: "files", + isVariadic: true, + template: ["filepaths", "folders"], + }, }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/echo.ts b/extensions/terminal-suggest/src/completions/upstream/echo.ts index 8ca21b85..7b1452e2 100644 --- a/extensions/terminal-suggest/src/completions/upstream/echo.ts +++ b/extensions/terminal-suggest/src/completions/upstream/echo.ts @@ -1,42 +1,42 @@ const environmentVariableGenerator: Fig.Generator = { - custom: async (tokens, _, context) => { - if (tokens.length < 3 || tokens[tokens.length - 1].startsWith("$")) { - return Object.keys(context.environmentVariables).map((suggestion) => ({ - name: `$${suggestion}`, - type: "arg", - description: "Environment Variable", - })); - } else { - return []; - } - }, - trigger: "$", + custom: async (tokens, _, context) => { + if (tokens.length < 3 || tokens[tokens.length - 1].startsWith("$")) { + return Object.keys(context.environmentVariables).map((suggestion) => ({ + name: `$${suggestion}`, + type: "arg", + description: "Environment Variable", + })); + } else { + return []; + } + }, + trigger: "$", }; const completionSpec: Fig.Spec = { - name: "echo", - description: "Write arguments to the standard output", - args: { - name: "string", - isVariadic: true, - optionsCanBreakVariadicArg: false, - suggestCurrentToken: true, - generators: environmentVariableGenerator, - }, - options: [ - { - name: "-n", - description: "Do not print the trailing newline character", - }, - { - name: "-e", - description: "Interpret escape sequences", - }, - { - name: "-E", - description: "Disable escape sequences", - }, - ], + name: "echo", + description: "Write arguments to the standard output", + args: { + name: "string", + isVariadic: true, + optionsCanBreakVariadicArg: false, + suggestCurrentToken: true, + generators: environmentVariableGenerator, + }, + options: [ + { + name: "-n", + description: "Do not print the trailing newline character", + }, + { + name: "-e", + description: "Interpret escape sequences", + }, + { + name: "-E", + description: "Disable escape sequences", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/find.ts b/extensions/terminal-suggest/src/completions/upstream/find.ts index 5b5245cb..08b4d6a5 100644 --- a/extensions/terminal-suggest/src/completions/upstream/find.ts +++ b/extensions/terminal-suggest/src/completions/upstream/find.ts @@ -1,71 +1,71 @@ const completionSpec: Fig.Spec = { - name: "find", - description: "Walk a file hierarchy", - args: [ - { - name: "path", - isOptional: true, - isVariadic: true, - template: ["folders"], - }, - { - // TODO Suggestions for primaries and operands. See `man find` - name: "expression", - description: "Composition of primaries and operands", - isOptional: true, - isVariadic: true, - }, - ], - options: [ - { - name: "-E", - description: - "Interpret regular expressions followed by -regex and -iregex primaries as extended", - }, - { - name: "-H", - description: - "Cause the file information and file type returned for each symbolic link specified to be those referenced by the link", - exclusiveOn: ["-L", "-P"], - }, - { - name: "-L", - description: - "Cause the file information and file type returned for each symbolic link to be those of the file referenced by the link", - exclusiveOn: ["-H", "-P"], - }, - { - name: "-P", - description: - "Cause the file information and file type returned for each symbolic link to be those for the link itself", - exclusiveOn: ["-H", "-L"], - }, - { - name: "-X", - description: "Permit find to be safely used in conjunction with xargs", - }, - { - name: "-d", - description: "Cause find to perform a depth-first traversal", - }, - { - name: "-f", - description: "Specify a file hierarch for find to traverse", - args: { - name: "path", - }, - }, - { - name: "-s", - description: - "Cause find to traverse the file hierarchies in lexicographical order", - }, - { - name: "-x", - description: - "Prevent find from descending into directories that have a device number different than that of the file from which the descent began", - }, - ], + name: "find", + description: "Walk a file hierarchy", + args: [ + { + name: "path", + isOptional: true, + isVariadic: true, + template: ["folders"], + }, + { + // TODO Suggestions for primaries and operands. See `man find` + name: "expression", + description: "Composition of primaries and operands", + isOptional: true, + isVariadic: true, + }, + ], + options: [ + { + name: "-E", + description: + "Interpret regular expressions followed by -regex and -iregex primaries as extended", + }, + { + name: "-H", + description: + "Cause the file information and file type returned for each symbolic link specified to be those referenced by the link", + exclusiveOn: ["-L", "-P"], + }, + { + name: "-L", + description: + "Cause the file information and file type returned for each symbolic link to be those of the file referenced by the link", + exclusiveOn: ["-H", "-P"], + }, + { + name: "-P", + description: + "Cause the file information and file type returned for each symbolic link to be those for the link itself", + exclusiveOn: ["-H", "-L"], + }, + { + name: "-X", + description: "Permit find to be safely used in conjunction with xargs", + }, + { + name: "-d", + description: "Cause find to perform a depth-first traversal", + }, + { + name: "-f", + description: "Specify a file hierarch for find to traverse", + args: { + name: "path", + }, + }, + { + name: "-s", + description: + "Cause find to traverse the file hierarchies in lexicographical order", + }, + { + name: "-x", + description: + "Prevent find from descending into directories that have a device number different than that of the file from which the descent began", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/git.ts b/extensions/terminal-suggest/src/completions/upstream/git.ts index 62d90d63..56468610 100644 --- a/extensions/terminal-suggest/src/completions/upstream/git.ts +++ b/extensions/terminal-suggest/src/completions/upstream/git.ts @@ -1,4 +1,4 @@ -// import { ai } from "../../fig/ai/ai"; +function ai(...args: any[]): undefined { return undefined; } const filterMessages = (out: string): string => { return out.startsWith("warning:") || out.startsWith("error:") @@ -29,7 +29,7 @@ const postProcessTrackedFiles: Fig.Generator["postProcess"] = ( try { ext = file.split(".").slice(-1)[0]; - } catch (e) { } + } catch (e) {} if (file.endsWith("/")) { ext = "folder"; @@ -53,69 +53,67 @@ interface PostProcessBranchesOptions { const postProcessBranches = (options: PostProcessBranchesOptions = {}): Fig.Generator["postProcess"] => - (out): (Fig.Suggestion | null)[] => { - const { insertWithoutRemotes = false } = options; + (out) => { + const { insertWithoutRemotes = false } = options; - const output = filterMessages(out); + const output = filterMessages(out); - if (output.startsWith("fatal:")) { - return []; - } + if (output.startsWith("fatal:")) { + return []; + } - const seen = new Set(); - return output - .split("\n") - .filter((line) => !line.trim().startsWith("HEAD")) - .map((branch) => { - let name = branch.trim(); - const parts = branch.match(/\S+/g); - if (!parts) { - return null; - } - if (parts.length > 1) { - if (parts[0] === "*") { - // We are in a detached HEAD state - if (branch.includes("HEAD detached")) { - return null; - } - // Current branch - return { - name: branch.replace("*", "").trim(), - description: "Current branch", - priority: 100, - icon: "⭐️", - }; - } else if (parts[0] === "+") { - // Branch checked out in another worktree. - name = branch.replace("+", "").trim(); + const seen = new Set(); + return output + .split("\n") + .filter((line) => !line.trim().startsWith("HEAD")) + .map((branch) => { + let name = branch.trim(); + const parts = branch.match(/\S+/g); + if (parts && parts.length > 1) { + if (parts[0] === "*") { + // We are in a detached HEAD state + if (branch.includes("HEAD detached")) { + return null; } + // Current branch + return { + name: branch.replace("*", "").trim(), + description: "Current branch", + priority: 100, + icon: "⭐️", + }; + } else if (parts[0] === "+") { + // Branch checked out in another worktree. + name = branch.replace("+", "").trim(); } + } - let description = "Branch"; + let description = "Branch"; - if (insertWithoutRemotes && name.startsWith("remotes/")) { - name = name.slice(name.indexOf("/", 8) + 1); - description = "Remote branch"; - } + if (insertWithoutRemotes && name.startsWith("remotes/")) { + name = name.slice(name.indexOf("/", 8) + 1); + description = "Remote branch"; + } - const space = name.indexOf(" "); - if (space !== -1) { - name = name.slice(0, space); - } + const space = name.indexOf(" "); + if (space !== -1) { + name = name.slice(0, space); + } - return { - name, - description, - icon: "fig://icon?type=git", - priority: 75, - }; - }) - .filter((suggestion) => { - if (!suggestion || seen.has(suggestion.name)) return false; - seen.add(suggestion.name); - return true; - }); - }; + return { + name, + description, + icon: "fig://icon?type=git", + priority: 75, + }; + }) + .filter((suggestion) => { + if (!suggestion) return false; + if (seen.has(suggestion.name)) return false; + seen.add(suggestion.name); + return true; + }); + }; export const gitGenerators: Record = { // Commit history @@ -298,14 +296,16 @@ export const gitGenerators: Record = { remotes: { script: ["git", "--no-optional-locks", "remote", "-v"], postProcess: function (out) { - const remoteURLs = out.split("\n").reduce>((dict, line) => { - const pair = line.split("\t"); - const remote = pair[0]; - const url = pair[1].split(" ")[0]; + const remoteURLs = out + .split("\n") + .reduce>((dict, line) => { + const pair = line.split("\t"); + const remote = pair[0]; + const url = pair[1].split(" ")[0]; - dict[remote] = url; - return dict; - }, {}); + dict[remote] = url; + return dict; + }, {}); return Object.keys(remoteURLs).map((remote) => { const url = remoteURLs[remote]; @@ -426,7 +426,7 @@ export const gitGenerators: Record = { let ext = ""; try { ext = file.split(".").slice(-1)[0]; - } catch (e) { } + } catch (e) {} if (file.endsWith("/")) { ext = "folder"; @@ -4026,7 +4026,7 @@ const daemonServices: Fig.Suggestion[] = [ const completionSpec: Fig.Spec = { name: "git", - description: "Distributed version control system", + description: "The stupid content tracker", generateSpec: async (_, executeShellCommand) => { const { stdout } = await executeShellCommand({ command: "git", @@ -4401,35 +4401,34 @@ const completionSpec: Fig.Spec = { description: "Use the given message as the commit message", args: { name: "message", - // generators: ai({ - // name: "git commit -m", - // prompt: async ({ executeCommand }) => { - // const { stdout } = await executeCommand({ - // command: "git", - // args: [ - // "log", - // "--pretty=format:%s", - // "--abbrev-commit", - // "--max-count=20", - // ], - // }); + generators: ai({ + name: "git commit -m", + prompt: async ({ executeCommand }: any) => { + const { stdout } = await executeCommand({ + command: "git", + args: [ + "log", + "--pretty=format:%s", + "--abbrev-commit", + "--max-count=20", + ], + }); - // return ( - // 'Generate a git commit message summary based on this git diff, the "summary" must be no more ' + - // "than 70-75 characters, and it must describe both what the patch changes, as well as why the " + - // `patch might be necessary.\n\nHere are some examples from the repo:\n${stdout}` - // ); - // }, - // message: async ({ executeCommand }) => - // ( - // await executeCommand({ - // command: "git", - // args: ["diff", "--staged"], - // }) - // ).stdout, - // splitOn: "\n", - // }), - // }, + return ( + 'Generate a git commit message summary based on this git diff, the "summary" must be no more ' + + "than 70-75 characters, and it must describe both what the patch changes, as well as why the " + + `patch might be necessary.\n\nHere are some examples from the repo:\n${stdout}` + ); + }, + message: async ({ executeCommand }: any) => + ( + await executeCommand({ + command: "git", + args: ["diff", "--staged"], + }) + ).stdout, + splitOn: "\n", + }), }, }, { diff --git a/extensions/terminal-suggest/src/completions/upstream/grep.ts b/extensions/terminal-suggest/src/completions/upstream/grep.ts index a7c15281..a897d463 100644 --- a/extensions/terminal-suggest/src/completions/upstream/grep.ts +++ b/extensions/terminal-suggest/src/completions/upstream/grep.ts @@ -1,344 +1,344 @@ const completionSpec: Fig.Spec = { - name: "grep", - description: - "Matches patterns in input text. Supports simple patterns and regular expressions", - args: [ - { - name: "search pattern", - suggestCurrentToken: true, - }, - { - name: "file", - template: "filepaths", - }, - ], - options: [ - { - name: "--help", - description: - "Print a usage message briefly summarizing these command-line options and the bug-reporting address, then exit", - }, - { - name: ["-E", "--extended-regexp"], - description: - "Interpret PATTERN as an extended regular expression (-E is specified by POSIX.)", - }, - { - name: ["-F", "--fixed-string"], - description: - "Interpret PATTERN as a list of fixed strings, separated by newlines, any of which is to be matched. (-F is specified by POSIX.)", - }, - { - name: ["-G", "--basic-regexp"], - description: - "Interpret PATTERN as a basic regular expression (BRE, see below). This is the default", - }, - { - name: ["-e", "--regexp"], - description: - "Use PATTERN as the pattern. This can be used to specify multiple search patterns, or to protect a pattern beginning with a hyphen (-). (-e is specified by POSIX.)", - args: { - name: "pattern", - }, - }, - { - name: ["-i", "--ignore-case", "-y"], - description: - "Ignore case distinctions in both the PATTERN and the input files. (-i is specified by POSIX.)", - }, - { - name: ["-v", "--invert-match"], - description: - "Invert the sense of matching, to select non-matching lines. (-v is specified by POSIX.)", - }, - { - name: ["-w", "--word-regexp"], - description: - "Select only those lines containing matches that form whole words. The test is that the matching substring must either be at the beginning of the line, or preceded by a non-word constituent character. Similarly, it must be either at the end of the line or followed by a non-word constituent character. Word-constituent characters are letters, digits, and the underscore", - }, - { - name: ["-x", "--line-regexp"], - description: - "Select only those matches that exactly match the whole line. (-x is specified by POSIX.)", - }, - { - name: ["-c", "--count"], - description: - "Suppress normal output; instead print a count of matching lines for each input file. With the -v, --invert-match option, count non-matching lines. (-c is specified by POSIX.)", - }, - { - name: "--color", - description: - "Surround the matched (non-empty) strings, matching lines, context lines, file names, line numbers, byte offsets, and separators (for fields and groups of context lines) with escape sequences to display them in color on the terminal. The colors are defined by the environment variable GREP_COLORS. The deprecated environment variable GREP_COLOR is still supported, but its setting does not have priority", - args: { - name: "WHEN", - default: "auto", - suggestions: ["never", "always", "auto"], - }, - }, - { - name: ["-L", "--files-without-match"], - exclusiveOn: ["-l", "--files-with-matches"], - description: - "Suppress normal output; instead print the name of each input file from which no output would normally have been printed. The scanning will stop on the first match", - }, - { - name: ["-l", "--files-with-matches"], - exclusiveOn: ["-L", "--files-without-match"], - description: - "Suppress normal output; instead print the name of each input file from which output would normally have been printed. The scanning will stop on the first match. (-l is specified by POSIX.)", - }, - { - name: ["-m", "--max-count"], - description: - "Stop reading a file after NUM matching lines. If the input is standard input from a regular file, and NUM matching lines are output, grep ensures that the standard input is positioned to just after the last matching line before exiting, regardless of the presence of trailing context lines. This enables a calling process to resume a search. When grep stops after NUM matching lines, it outputs any trailing context lines. When the -c or --count option is also used, grep does not output a count greater than NUM. When the -v or --invert-match option is also used, grep stops after outputting NUM non-matching lines", - args: { - name: "NUM", - }, - }, - { - name: ["-o", "--only-matching"], - description: - "Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line", - }, - { - name: ["-q", "--quiet", "--silent"], - description: - "Quiet; do not write anything to standard output. Exit immediately with zero status if any match is found, even if an error was detected. Also see the -s or --no-messages option. (-q is specified by POSIX.)", - }, - { - name: ["-s", "--no-messages"], - description: - "Suppress error messages about nonexistent or unreadable files. Portability note: unlike GNU grep, 7th Edition Unix grep did not conform to POSIX, because it lacked -q and its -s option behaved like GNU grep's -q option. USG -style grep also lacked -q but its -s option behaved like GNU grep. Portable shell scripts should avoid both -q and -s and should redirect standard and error output to /dev/null instead. (-s is specified by POSIX.)", - }, - { - name: ["-b", "--byte-offset"], - description: - "Print the 0-based byte offset within the input file before each line of output. If -o (--only-matching) is specified, print the offset of the matching part itself", - }, - { - name: ["-H", "--with-filename"], - description: - "Print the file name for each match. This is the default when there is more than one file to search", - }, - { - name: ["-h", "--no-filename"], - description: - "Suppress the prefixing of file names on output. This is the default when there is only one file (or only standard input) to search", - }, - { - name: "--label", - description: - "Display input actually coming from standard input as input coming from file LABEL. This is especially useful when implementing tools like zgrep, e.g., gzip -cd foo.gz | grep --label=foo -H something", - args: { - name: "LABEL", - }, - }, - { - name: ["-n", "--line-number"], - description: - "Prefix each line of output with the 1-based line number within its input file. (-n is specified by POSIX.)", - }, - { - name: ["-T", "--initial-tab"], - description: - "Make sure that the first character of actual line content lies on a tab stop, so that the alignment of tabs looks normal. This is useful with options that prefix their output to the actual content: -H,-n, and -b. In order to improve the probability that lines from a single file will all start at the same column, this also causes the line number and byte offset (if present) to be printed in a minimum size field width", - }, - { - name: ["-u", "--unix-byte-offsets"], - description: - "Report Unix-style byte offsets. This switch causes grep to report byte offsets as if the file were a Unix-style text file, i.e., with CR characters stripped off. This will produce results identical to running grep on a Unix machine. This option has no effect unless -b option is also used; it has no effect on platforms other than MS-DOS and MS -Windows", - }, - { - name: "--null", - description: - "Output a zero byte (the ASCII NUL character) instead of the character that normally follows a file name. For example, grep -lZ outputs a zero byte after each file name instead of the usual newline. This option makes the output unambiguous, even in the presence of file names containing unusual characters like newlines. This option can be used with commands like find -print0, perl -0, sort -z, and xargs -0 to process arbitrary file names, even those that contain newline characters", - }, - { - name: ["-A", "--after-context"], - description: "Print num lines of trailing context after each match", - args: { - name: "NUM", - }, - }, - { - name: ["-B", "--before-context"], - description: - "Print num lines of leading context before each match. See also the -A and -C options", - args: { - name: "NUM", - }, - }, - { - name: ["-C", "--context"], - description: - "Print NUM lines of output context. Places a line containing a group separator (--) between contiguous groups of matches. With the -o or --only-matching option, this has no effect and a warning is given", - args: { - name: "NUM", - }, - }, - { - name: ["-a", "--text"], - description: - "Treat all files as ASCII text. Normally grep will simply print ``Binary file ... matches'' if files contain binary characters. Use of this option forces grep to output lines matching the specified pattern", - }, - { - name: "--binary-files", - description: "Controls searching and printing of binary files", - args: { - name: "value", - default: "binary", - suggestions: [ - { - name: "binary", - description: "Search binary files but do not print them", - }, - { - name: "without-match", - description: "Do not search binary files", - }, - { - name: "text", - description: "Treat all files as text", - }, - ], - }, - }, - { - name: ["-D", "--devices"], - description: "Specify the demanded action for devices, FIFOs and sockets", - args: { - name: "action", - default: "read", - suggestions: [ - { - name: "read", - description: "Read as if they were normal files", - }, - { - name: "skip", - description: "Devices will be silently skipped", - }, - ], - }, - }, - { - name: ["-d", "--directories"], - description: "Specify the demanded action for directories", - args: { - name: "action", - default: "read", - suggestions: [ - { - name: "read", - description: - "Directories are read in the same manner as normal files", - }, - { - name: "skip", - description: "Silently ignore the directories", - }, - { - name: "recurse", - description: "Read directories recursively", - }, - ], - }, - }, - { - name: "--exclude", - description: - "Note that --exclude patterns take priority over --include patterns, and if no --include pattern is specified, all files are searched that are not excluded. Patterns are matched to the full path specified, not only to the filename component", - args: { - name: "GLOB", - isOptional: true, - }, - }, - { - name: "--exclude-dir", - description: - "If -R is specified, only directories matching the given filename pattern are searched. Note that --exclude-dir patterns take priority over --include-dir patterns", - isRepeatable: true, - args: { - name: "dir", - template: "folders", - isOptional: true, - }, - }, - { - name: "-I", - description: - "Ignore binary files. This option is equivalent to --binary-file=without-match option", - }, - { - name: "--include", - description: - "If specified, only files matching the given filename pattern are searched. Note that --exclude patterns take priority over --include patterns. Patterns are matched to the full path specified, not only to the filename component", - args: { - name: "GLOB", - isOptional: true, - }, - }, - { - name: "--include-dir", - description: - "If -R is specified, only directories matching the given filename pattern are searched. Note that --exclude-dir patterns take priority over --include-dir patterns", - args: { - name: "dir", - template: "folders", - isOptional: true, - }, - }, - { - name: ["-R", "-r", "--recursive"], - description: "Recursively search subdirectories listed", - }, - { - name: "--line-buffered", - description: - "Force output to be line buffered. By default, output is line buffered when standard output is a terminal and block buffered otherwise", - }, - { - name: ["-U", "--binary"], - description: "Search binary files, but do not attempt to print them", - }, - { - name: ["-J", "-bz2decompress"], - description: - "Decompress the bzip2(1) compressed file before looking for the text", - }, - { - name: ["-V", "--version"], - description: "Print version number of grep to the standard output stream", - }, - { - name: ["-P", "--perl-regexp"], - description: "Interpret pattern as a Perl regular expression", - }, - { - name: ["-f", "--file"], - description: - "Obtain patterns from FILE, one per line. The empty file contains zero patterns, and therefore matches nothing. (-f is specified by POSIX.)", - args: { - name: "FILE", - template: "filepaths", - }, - }, - ], - additionalSuggestions: [ - { - name: "-RIn", - description: - "Search for a pattern [R]ecursively in the current directory, showing matching line [n]umbers, [I]gnoring non-text files", - insertValue: "-RI{cursor}", - }, - { - name: "-Hn", - description: - "Print file name with the corresponding line number (n) for each match", - insertValue: "-H{cursor}", - }, - ], + name: "grep", + description: + "Matches patterns in input text. Supports simple patterns and regular expressions", + args: [ + { + name: "search pattern", + suggestCurrentToken: true, + }, + { + name: "file", + template: "filepaths", + }, + ], + options: [ + { + name: "--help", + description: + "Print a usage message briefly summarizing these command-line options and the bug-reporting address, then exit", + }, + { + name: ["-E", "--extended-regexp"], + description: + "Interpret PATTERN as an extended regular expression (-E is specified by POSIX.)", + }, + { + name: ["-F", "--fixed-string"], + description: + "Interpret PATTERN as a list of fixed strings, separated by newlines, any of which is to be matched. (-F is specified by POSIX.)", + }, + { + name: ["-G", "--basic-regexp"], + description: + "Interpret PATTERN as a basic regular expression (BRE, see below). This is the default", + }, + { + name: ["-e", "--regexp"], + description: + "Use PATTERN as the pattern. This can be used to specify multiple search patterns, or to protect a pattern beginning with a hyphen (-). (-e is specified by POSIX.)", + args: { + name: "pattern", + }, + }, + { + name: ["-i", "--ignore-case", "-y"], + description: + "Ignore case distinctions in both the PATTERN and the input files. (-i is specified by POSIX.)", + }, + { + name: ["-v", "--invert-match"], + description: + "Invert the sense of matching, to select non-matching lines. (-v is specified by POSIX.)", + }, + { + name: ["-w", "--word-regexp"], + description: + "Select only those lines containing matches that form whole words. The test is that the matching substring must either be at the beginning of the line, or preceded by a non-word constituent character. Similarly, it must be either at the end of the line or followed by a non-word constituent character. Word-constituent characters are letters, digits, and the underscore", + }, + { + name: ["-x", "--line-regexp"], + description: + "Select only those matches that exactly match the whole line. (-x is specified by POSIX.)", + }, + { + name: ["-c", "--count"], + description: + "Suppress normal output; instead print a count of matching lines for each input file. With the -v, --invert-match option, count non-matching lines. (-c is specified by POSIX.)", + }, + { + name: "--color", + description: + "Surround the matched (non-empty) strings, matching lines, context lines, file names, line numbers, byte offsets, and separators (for fields and groups of context lines) with escape sequences to display them in color on the terminal. The colors are defined by the environment variable GREP_COLORS. The deprecated environment variable GREP_COLOR is still supported, but its setting does not have priority", + args: { + name: "WHEN", + default: "auto", + suggestions: ["never", "always", "auto"], + }, + }, + { + name: ["-L", "--files-without-match"], + exclusiveOn: ["-l", "--files-with-matches"], + description: + "Suppress normal output; instead print the name of each input file from which no output would normally have been printed. The scanning will stop on the first match", + }, + { + name: ["-l", "--files-with-matches"], + exclusiveOn: ["-L", "--files-without-match"], + description: + "Suppress normal output; instead print the name of each input file from which output would normally have been printed. The scanning will stop on the first match. (-l is specified by POSIX.)", + }, + { + name: ["-m", "--max-count"], + description: + "Stop reading a file after NUM matching lines. If the input is standard input from a regular file, and NUM matching lines are output, grep ensures that the standard input is positioned to just after the last matching line before exiting, regardless of the presence of trailing context lines. This enables a calling process to resume a search. When grep stops after NUM matching lines, it outputs any trailing context lines. When the -c or --count option is also used, grep does not output a count greater than NUM. When the -v or --invert-match option is also used, grep stops after outputting NUM non-matching lines", + args: { + name: "NUM", + }, + }, + { + name: ["-o", "--only-matching"], + description: + "Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line", + }, + { + name: ["-q", "--quiet", "--silent"], + description: + "Quiet; do not write anything to standard output. Exit immediately with zero status if any match is found, even if an error was detected. Also see the -s or --no-messages option. (-q is specified by POSIX.)", + }, + { + name: ["-s", "--no-messages"], + description: + "Suppress error messages about nonexistent or unreadable files. Portability note: unlike GNU grep, 7th Edition Unix grep did not conform to POSIX, because it lacked -q and its -s option behaved like GNU grep's -q option. USG -style grep also lacked -q but its -s option behaved like GNU grep. Portable shell scripts should avoid both -q and -s and should redirect standard and error output to /dev/null instead. (-s is specified by POSIX.)", + }, + { + name: ["-b", "--byte-offset"], + description: + "Print the 0-based byte offset within the input file before each line of output. If -o (--only-matching) is specified, print the offset of the matching part itself", + }, + { + name: ["-H", "--with-filename"], + description: + "Print the file name for each match. This is the default when there is more than one file to search", + }, + { + name: ["-h", "--no-filename"], + description: + "Suppress the prefixing of file names on output. This is the default when there is only one file (or only standard input) to search", + }, + { + name: "--label", + description: + "Display input actually coming from standard input as input coming from file LABEL. This is especially useful when implementing tools like zgrep, e.g., gzip -cd foo.gz | grep --label=foo -H something", + args: { + name: "LABEL", + }, + }, + { + name: ["-n", "--line-number"], + description: + "Prefix each line of output with the 1-based line number within its input file. (-n is specified by POSIX.)", + }, + { + name: ["-T", "--initial-tab"], + description: + "Make sure that the first character of actual line content lies on a tab stop, so that the alignment of tabs looks normal. This is useful with options that prefix their output to the actual content: -H,-n, and -b. In order to improve the probability that lines from a single file will all start at the same column, this also causes the line number and byte offset (if present) to be printed in a minimum size field width", + }, + { + name: ["-u", "--unix-byte-offsets"], + description: + "Report Unix-style byte offsets. This switch causes grep to report byte offsets as if the file were a Unix-style text file, i.e., with CR characters stripped off. This will produce results identical to running grep on a Unix machine. This option has no effect unless -b option is also used; it has no effect on platforms other than MS-DOS and MS -Windows", + }, + { + name: "--null", + description: + "Output a zero byte (the ASCII NUL character) instead of the character that normally follows a file name. For example, grep -lZ outputs a zero byte after each file name instead of the usual newline. This option makes the output unambiguous, even in the presence of file names containing unusual characters like newlines. This option can be used with commands like find -print0, perl -0, sort -z, and xargs -0 to process arbitrary file names, even those that contain newline characters", + }, + { + name: ["-A", "--after-context"], + description: "Print num lines of trailing context after each match", + args: { + name: "NUM", + }, + }, + { + name: ["-B", "--before-context"], + description: + "Print num lines of leading context before each match. See also the -A and -C options", + args: { + name: "NUM", + }, + }, + { + name: ["-C", "--context"], + description: + "Print NUM lines of output context. Places a line containing a group separator (--) between contiguous groups of matches. With the -o or --only-matching option, this has no effect and a warning is given", + args: { + name: "NUM", + }, + }, + { + name: ["-a", "--text"], + description: + "Treat all files as ASCII text. Normally grep will simply print ``Binary file ... matches'' if files contain binary characters. Use of this option forces grep to output lines matching the specified pattern", + }, + { + name: "--binary-files", + description: "Controls searching and printing of binary files", + args: { + name: "value", + default: "binary", + suggestions: [ + { + name: "binary", + description: "Search binary files but do not print them", + }, + { + name: "without-match", + description: "Do not search binary files", + }, + { + name: "text", + description: "Treat all files as text", + }, + ], + }, + }, + { + name: ["-D", "--devices"], + description: "Specify the demanded action for devices, FIFOs and sockets", + args: { + name: "action", + default: "read", + suggestions: [ + { + name: "read", + description: "Read as if they were normal files", + }, + { + name: "skip", + description: "Devices will be silently skipped", + }, + ], + }, + }, + { + name: ["-d", "--directories"], + description: "Specify the demanded action for directories", + args: { + name: "action", + default: "read", + suggestions: [ + { + name: "read", + description: + "Directories are read in the same manner as normal files", + }, + { + name: "skip", + description: "Silently ignore the directories", + }, + { + name: "recurse", + description: "Read directories recursively", + }, + ], + }, + }, + { + name: "--exclude", + description: + "Note that --exclude patterns take priority over --include patterns, and if no --include pattern is specified, all files are searched that are not excluded. Patterns are matched to the full path specified, not only to the filename component", + args: { + name: "GLOB", + isOptional: true, + }, + }, + { + name: "--exclude-dir", + description: + "If -R is specified, only directories matching the given filename pattern are searched. Note that --exclude-dir patterns take priority over --include-dir patterns", + isRepeatable: true, + args: { + name: "dir", + template: "folders", + isOptional: true, + }, + }, + { + name: "-I", + description: + "Ignore binary files. This option is equivalent to --binary-file=without-match option", + }, + { + name: "--include", + description: + "If specified, only files matching the given filename pattern are searched. Note that --exclude patterns take priority over --include patterns. Patterns are matched to the full path specified, not only to the filename component", + args: { + name: "GLOB", + isOptional: true, + }, + }, + { + name: "--include-dir", + description: + "If -R is specified, only directories matching the given filename pattern are searched. Note that --exclude-dir patterns take priority over --include-dir patterns", + args: { + name: "dir", + template: "folders", + isOptional: true, + }, + }, + { + name: ["-R", "-r", "--recursive"], + description: "Recursively search subdirectories listed", + }, + { + name: "--line-buffered", + description: + "Force output to be line buffered. By default, output is line buffered when standard output is a terminal and block buffered otherwise", + }, + { + name: ["-U", "--binary"], + description: "Search binary files, but do not attempt to print them", + }, + { + name: ["-J", "-bz2decompress"], + description: + "Decompress the bzip2(1) compressed file before looking for the text", + }, + { + name: ["-V", "--version"], + description: "Print version number of grep to the standard output stream", + }, + { + name: ["-P", "--perl-regexp"], + description: "Interpret pattern as a Perl regular expression", + }, + { + name: ["-f", "--file"], + description: + "Obtain patterns from FILE, one per line. The empty file contains zero patterns, and therefore matches nothing. (-f is specified by POSIX.)", + args: { + name: "FILE", + template: "filepaths", + }, + }, + ], + additionalSuggestions: [ + { + name: "-RIn", + description: + "Search for a pattern [R]ecursively in the current directory, showing matching line [n]umbers, [I]gnoring non-text files", + insertValue: "-RI{cursor}", + }, + { + name: "-Hn", + description: + "Print file name with the corresponding line number (n) for each match", + insertValue: "-H{cursor}", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/head.ts b/extensions/terminal-suggest/src/completions/upstream/head.ts index bc837e09..a7ae0a7a 100644 --- a/extensions/terminal-suggest/src/completions/upstream/head.ts +++ b/extensions/terminal-suggest/src/completions/upstream/head.ts @@ -1,35 +1,35 @@ const completionSpec: Fig.Spec = { - name: "head", - description: "Output the first part of files", - args: { - name: "file", - template: "filepaths", - }, - options: [ - { - name: ["-c", "--bytes"], - description: "Print the first [numBytes] bytes of each file", - args: { name: "numBytes" }, - }, - { - name: ["-n", "--lines"], - description: "Print the first [numLines] lines instead of the first 10", - args: { name: "numLines" }, - }, - { - name: ["-q", "--quiet", "--silent"], - description: "Never print headers giving file names", - }, - { - name: ["-v", "--verbose"], - description: "Always print headers giving file names", - }, - { name: "--help", description: "Display this help and exit" }, - { - name: "--version", - description: "Output version information and exit", - }, - ], + name: "head", + description: "Output the first part of files", + args: { + name: "file", + template: "filepaths", + }, + options: [ + { + name: ["-c", "--bytes"], + description: "Print the first [numBytes] bytes of each file", + args: { name: "numBytes" }, + }, + { + name: ["-n", "--lines"], + description: "Print the first [numLines] lines instead of the first 10", + args: { name: "numLines" }, + }, + { + name: ["-q", "--quiet", "--silent"], + description: "Never print headers giving file names", + }, + { + name: ["-v", "--verbose"], + description: "Always print headers giving file names", + }, + { name: "--help", description: "Display this help and exit" }, + { + name: "--version", + description: "Output version information and exit", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/killall.ts b/extensions/terminal-suggest/src/completions/upstream/killall.ts index c22e55dd..c963a971 100644 --- a/extensions/terminal-suggest/src/completions/upstream/killall.ts +++ b/extensions/terminal-suggest/src/completions/upstream/killall.ts @@ -1,153 +1,153 @@ // Linux incompatible const signals = [ - "hup", - "int", - "quit", - "ill", - "trap", - "abrt", - "emt", - "fpe", - "kill", - "bus", - "segv", - "sys", - "pipe", - "alrm", - // This is the default signal - // "term", - "urg", - "stop", - "tstp", - "cont", - "chld", - "ttin", - "ttou", - "io", - "xcpu", - "xfsz", - "vtalrm", - "prof", - "winch", - "info", - "usr1", - "usr2", + "hup", + "int", + "quit", + "ill", + "trap", + "abrt", + "emt", + "fpe", + "kill", + "bus", + "segv", + "sys", + "pipe", + "alrm", + // This is the default signal + // "term", + "urg", + "stop", + "tstp", + "cont", + "chld", + "ttin", + "ttou", + "io", + "xcpu", + "xfsz", + "vtalrm", + "prof", + "winch", + "info", + "usr1", + "usr2", ]; const completionSpec: Fig.Spec = { - name: "killall", - description: "Kill processes by name", - args: { - name: "process_name", - isVariadic: true, - generators: { - // All processes, only display the path - script: ["bash", "-c", "ps -A -o comm | sort -u"], - postProcess: (out) => - out - .trim() - .split("\n") - .map((path) => { - const appExtIndex = path.indexOf(".app/"); - const isApp = appExtIndex !== -1; - const name = path.slice(path.lastIndexOf("/") + 1); - const nameChars = new Set(name); - const badChars = ["(", "_", "."]; - return { - name, - description: path, - priority: - !badChars.some((char) => nameChars.has(char)) && isApp - ? 51 - : 40, - icon: isApp - ? "fig://" + path.slice(0, appExtIndex + 4) - : "fig://icon?type=gear", - }; - }), - }, - }, - options: [ - { - name: "-d", - description: "Be verbose (dry run) and display number of user processes", - }, - { - name: "-e", - description: - "Use the effective user ID instead of the real user ID for matching processes with -u", - }, - { - name: "-help", - description: "Display help and exit", - }, - { - name: "-I", - description: "Request confirmation before killing each process", - }, - { - name: "-l", - description: "List the names of the available signals and exit", - }, - { - name: "-m", - description: "Match the process name as a regular expression", - }, - { - name: "-v", - description: "Be verbose", - }, - { - name: "-s", - description: "Be verbose (dry run)", - }, - ...signals.map((signal) => ({ - name: "-SIG" + signal.toUpperCase(), - description: `Send ${signal.toUpperCase()} instead of TERM`, - })), - { - name: "-u", - description: - "Limit potentially matching processes to those belonging to the user", - args: { - name: "user", - generators: { - script: ["bash", "-c", "dscl . -list /Users | grep -v '^_'"], - postProcess: (out) => - out - .trim() - .split("\n") - .map((username) => ({ - name: username, - icon: "fig://template?badge=👤", - })), - }, - }, - }, - { - name: "-t", - description: - "Limit matching processes to those running on the specified TTY", - args: { - name: "tty", - }, - }, - { - name: "-c", - description: "Limit matching processes to those matching the given name", - args: { - name: "name", - }, - }, - { - name: "-q", - description: "Suppress error message if no processes are matched", - }, - { - name: "-z", - description: "Do not skip zombies", - }, - ], + name: "killall", + description: "Kill processes by name", + args: { + name: "process_name", + isVariadic: true, + generators: { + // All processes, only display the path + script: ["bash", "-c", "ps -A -o comm | sort -u"], + postProcess: (out) => + out + .trim() + .split("\n") + .map((path) => { + const appExtIndex = path.indexOf(".app/"); + const isApp = appExtIndex !== -1; + const name = path.slice(path.lastIndexOf("/") + 1); + const nameChars = new Set(name); + const badChars = ["(", "_", "."]; + return { + name, + description: path, + priority: + !badChars.some((char) => nameChars.has(char)) && isApp + ? 51 + : 40, + icon: isApp + ? "fig://" + path.slice(0, appExtIndex + 4) + : "fig://icon?type=gear", + }; + }), + }, + }, + options: [ + { + name: "-d", + description: "Be verbose (dry run) and display number of user processes", + }, + { + name: "-e", + description: + "Use the effective user ID instead of the real user ID for matching processes with -u", + }, + { + name: "-help", + description: "Display help and exit", + }, + { + name: "-I", + description: "Request confirmation before killing each process", + }, + { + name: "-l", + description: "List the names of the available signals and exit", + }, + { + name: "-m", + description: "Match the process name as a regular expression", + }, + { + name: "-v", + description: "Be verbose", + }, + { + name: "-s", + description: "Be verbose (dry run)", + }, + ...signals.map((signal) => ({ + name: "-SIG" + signal.toUpperCase(), + description: `Send ${signal.toUpperCase()} instead of TERM`, + })), + { + name: "-u", + description: + "Limit potentially matching processes to those belonging to the user", + args: { + name: "user", + generators: { + script: ["bash", "-c", "dscl . -list /Users | grep -v '^_'"], + postProcess: (out) => + out + .trim() + .split("\n") + .map((username) => ({ + name: username, + icon: "fig://template?badge=👤", + })), + }, + }, + }, + { + name: "-t", + description: + "Limit matching processes to those running on the specified TTY", + args: { + name: "tty", + }, + }, + { + name: "-c", + description: "Limit matching processes to those matching the given name", + args: { + name: "name", + }, + }, + { + name: "-q", + description: "Suppress error message if no processes are matched", + }, + { + name: "-z", + description: "Do not skip zombies", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/less.ts b/extensions/terminal-suggest/src/completions/upstream/less.ts index f8994808..f50abbd4 100644 --- a/extensions/terminal-suggest/src/completions/upstream/less.ts +++ b/extensions/terminal-suggest/src/completions/upstream/less.ts @@ -1,19 +1,19 @@ const completionSpec: Fig.Spec = { - name: "less", - description: "Opposite of more", - args: { - isVariadic: true, - template: "filepaths", - }, - options: [ - { - name: ["-?", "--help"], - description: - 'This option displays a summary of the commands accepted by less (the same as the h command). (Depending on how your shell interprets the question mark, it may be necessary to quote the question mark, thus: "-?"', - }, - { - name: ["-a", "--search-skip-screen"], - description: `By default, forward searches start at the top of the displayed + name: "less", + description: "Opposite of more", + args: { + isVariadic: true, + template: "filepaths", + }, + options: [ + { + name: ["-?", "--help"], + description: + 'This option displays a summary of the commands accepted by less (the same as the h command). (Depending on how your shell interprets the question mark, it may be necessary to quote the question mark, thus: "-?"', + }, + { + name: ["-a", "--search-skip-screen"], + description: `By default, forward searches start at the top of the displayed screen and backwards searches start at the bottom of the displayed screen (except for repeated searches invoked by the n or N commands, which start after or before the "target" line @@ -21,10 +21,10 @@ respectively; see the -j option for more about the target line). The -a option causes forward searches to instead start at the bottom of the screen and backward searches to start at the top of the screen, thus skipping all lines displayed on the screen`, - }, - { - name: ["-A", "--SEARCH-SKIP-SCREEN"], - description: `Causes all forward searches (not just non-repeated searches) to + }, + { + name: ["-A", "--SEARCH-SKIP-SCREEN"], + description: `Causes all forward searches (not just non-repeated searches) to start just after the target line, and all backward searches to start just before the target line. Thus, forward searches will skip part of the displayed screen (from the first line up to and @@ -32,23 +32,23 @@ including the target line). Similarly backwards searches will skip the displayed screen from the last line up to and including the target line. This was the default behavior in less versions prior to 441`, - }, + }, - { - name: ["-b", "--buffers"], - args: { name: "n" }, - description: `Specifies the amount of buffer space less will use for each + { + name: ["-b", "--buffers"], + args: { name: "n" }, + description: `Specifies the amount of buffer space less will use for each file, in units of kilobytes (1024 bytes). By default 64 KB of buffer space is used for each file (unless the file is a pipe; see the -B option). The -b option specifies instead that n kilobytes of buffer space should be used for each file. If n is -1, buffer space is unlimited; that is, the entire file can be read into memory`, - }, + }, - { - name: ["-B", "--auto-buffers"], - description: `By default, when data is read from a pipe, buffers are allocated + { + name: ["-B", "--auto-buffers"], + description: `By default, when data is read from a pipe, buffers are allocated automatically as needed. If a large amount of data is read from the pipe, this can cause a large amount of memory to be allocated. The -B option disables this automatic allocation of @@ -57,33 +57,33 @@ specified by the -b option) is used for the pipe. Warning: use of -B can result in erroneous display, since only the most recently viewed part of the piped data is kept in memory; any earlier data is lost`, - }, + }, - { - name: ["-c", "--clear-screen"], - description: `Causes full screen repaints to be painted from the top line + { + name: ["-c", "--clear-screen"], + description: `Causes full screen repaints to be painted from the top line down. By default, full screen repaints are done by scrolling from the bottom of the screen`, - }, + }, - { - name: ["-C", "--CLEAR-SCREEN"], - description: `Same as -c, for compatibility with older versions of less`, - }, + { + name: ["-C", "--CLEAR-SCREEN"], + description: `Same as -c, for compatibility with older versions of less`, + }, - { - name: ["-d", "--dumb"], - description: `The -d option suppresses the error message normally displayed if + { + name: ["-d", "--dumb"], + description: `The -d option suppresses the error message normally displayed if the terminal is dumb; that is, lacks some important capability, such as the ability to clear the screen or scroll backward. The -d option does not otherwise change the behavior of less on a dumb terminal`, - }, + }, - { - name: ["-D", "--color"], - args: { name: "xcolor" }, - description: `Changes the color of different parts of the displayed text. x + { + name: ["-D", "--color"], + args: { name: "xcolor" }, + description: `Changes the color of different parts of the displayed text. x is a single character which selects the type of text whose color is being set: B Binary characters. @@ -144,80 +144,80 @@ color is set to that of normal text. On MS-DOS versions of less, 8-bit color is not supported; instead, decimal values are interpreted as 4-bit CHAR_INFO.Attributes values (see https://docs.microsoft.com/en-us/windows/console/char-info-str)`, - }, + }, - { - name: ["-e", "--quit-at-eof"], - description: `Causes less to automatically exit the second time it reaches + { + name: ["-e", "--quit-at-eof"], + description: `Causes less to automatically exit the second time it reaches end-of-file. By default, the only way to exit less is via the "q" command`, - }, + }, - { - name: ["-E", "--QUIT-AT-EOF"], - description: `Causes less to automatically exit the first time it reaches end- + { + name: ["-E", "--QUIT-AT-EOF"], + description: `Causes less to automatically exit the first time it reaches end- of-file`, - }, + }, - { - name: ["-f", "--force"], - description: `Forces non-regular files to be opened. (A non-regular file is a + { + name: ["-f", "--force"], + description: `Forces non-regular files to be opened. (A non-regular file is a directory or a device special file.) Also suppresses the warning message when a binary file is opened. By default, less will refuse to open non-regular files. Note that some operating systems will not allow directories to be read, even if -f is set`, - }, + }, - { - name: ["-F", "--quit-if-one-screen"], - description: `Causes less to automatically exit if the entire file can be + { + name: ["-F", "--quit-if-one-screen"], + description: `Causes less to automatically exit if the entire file can be displayed on the first screen`, - }, + }, - { - name: ["-g", "--hilite-search"], - description: `Normally, less will highlight ALL strings which match the last + { + name: ["-g", "--hilite-search"], + description: `Normally, less will highlight ALL strings which match the last search command. The -g option changes this behavior to highlight only the particular string which was found by the last search command. This can cause less to run somewhat faster than the default`, - }, + }, - { - name: ["-G", "--HILITE-SEARCH"], - description: `The -G option suppresses all highlighting of strings found by + { + name: ["-G", "--HILITE-SEARCH"], + description: `The -G option suppresses all highlighting of strings found by search commands`, - }, + }, - { - name: ["-h", "--max-back-scroll"], - args: { name: "n" }, - description: `Specifies a maximum number of lines to scroll backward. If it + { + name: ["-h", "--max-back-scroll"], + args: { name: "n" }, + description: `Specifies a maximum number of lines to scroll backward. If it is necessary to scroll backward more than n lines, the screen is repainted in a forward direction instead. (If the terminal does not have the ability to scroll backward, -h0 is implied.)`, - }, + }, - { - name: ["-i", "--ignore-case"], - description: `Causes searches to ignore case; that is, uppercase and lowercase + { + name: ["-i", "--ignore-case"], + description: `Causes searches to ignore case; that is, uppercase and lowercase are considered identical. This option is ignored if any uppercase letters appear in the search pattern; in other words, if a pattern contains uppercase letters, then that search does not ignore case`, - }, + }, - { - name: ["-I", "--IGNORE-CASE"], - description: `Like -i, but searches ignore case even if the pattern contains + { + name: ["-I", "--IGNORE-CASE"], + description: `Like -i, but searches ignore case even if the pattern contains uppercase letters`, - }, + }, - { - name: ["-j", "--jump-target"], - args: { name: "n" }, - description: `Specifies a line on the screen where the "target" line is to be + { + name: ["-j", "--jump-target"], + args: { name: "n" }, + description: `Specifies a line on the screen where the "target" line is to be positioned. The target line is the line specified by any command to search for a pattern, jump to a line number, jump to a file percentage or jump to a tag. The screen line may be @@ -240,57 +240,57 @@ fourth line on the screen, so forward searches begin at the fifth line on the screen. However nonrepeated searches (invoked with "/" or "?") always begin at the start or end of the current screen respectively`, - }, + }, - { - name: ["-J", "--status-column"], - description: `Displays a status column at the left edge of the screen. The + { + name: ["-J", "--status-column"], + description: `Displays a status column at the left edge of the screen. The status column shows the lines that matched the current search, and any lines that are marked (via the m or M command)`, - }, + }, - { - name: ["-k", "--lesskey-file"], - args: { name: "filename", template: "filepaths" }, - description: `Causes less to open and interpret the named file as a lesskey(1) + { + name: ["-k", "--lesskey-file"], + args: { name: "filename", template: "filepaths" }, + description: `Causes less to open and interpret the named file as a lesskey(1) file. Multiple -k options may be specified. If the LESSKEY or LESSKEY_SYSTEM environment variable is set, or if a lesskey file is found in a standard place (see KEY BINDINGS), it is also used as a lesskey file`, - }, + }, - { - name: ["-K", "--quit-on-intr"], - description: `Causes less to exit immediately (with status 2) when an + { + name: ["-K", "--quit-on-intr"], + description: `Causes less to exit immediately (with status 2) when an interrupt character (usually ^C) is typed. Normally, an interrupt character causes less to stop whatever it is doing and return to its command prompt. Note that use of this option makes it impossible to return to the command prompt from the "F" command`, - }, + }, - { - name: ["-L", "--no-lessopen"], - description: `Ignore the LESSOPEN environment variable (see the INPUT + { + name: ["-L", "--no-lessopen"], + description: `Ignore the LESSOPEN environment variable (see the INPUT PREPROCESSOR section below). This option can be set from within less, but it will apply only to files opened subsequently, not to the file which is currently open`, - }, + }, - { - name: ["-m", "--long-prompt"], - description: `Causes less to prompt verbosely (like more), with the percent + { + name: ["-m", "--long-prompt"], + description: `Causes less to prompt verbosely (like more), with the percent into the file. By default, less prompts with a colon`, - }, + }, - { - name: ["-M", "--LONG-PROMPT"], - description: `Causes less to prompt even more verbosely than more`, - }, + { + name: ["-M", "--LONG-PROMPT"], + description: `Causes less to prompt even more verbosely than more`, + }, - { - name: ["-n", "--line-numbers"], - description: `Suppresses line numbers. The default (to use line numbers) may + { + name: ["-n", "--line-numbers"], + description: `Suppresses line numbers. The default (to use line numbers) may cause less to run more slowly in some cases, especially with a very large input file. Suppressing line numbers with the -n option will avoid this problem. Using line numbers means: the @@ -298,46 +298,46 @@ line number will be displayed in the verbose prompt and in the = command, and the v command will pass the current line number to the editor (see also the discussion of LESSEDIT in PROMPTS below)`, - }, + }, - { - name: ["-N", "--LINE-NUMBERS"], - description: `Causes a line number to be displayed at the beginning of each + { + name: ["-N", "--LINE-NUMBERS"], + description: `Causes a line number to be displayed at the beginning of each line in the display`, - }, + }, - { - name: ["-o", "--log-file"], - args: { name: "filename", template: "filepaths" }, - description: `Causes less to copy its input to the named file as it is being + { + name: ["-o", "--log-file"], + args: { name: "filename", template: "filepaths" }, + description: `Causes less to copy its input to the named file as it is being viewed. This applies only when the input file is a pipe, not an ordinary file. If the file already exists, less will ask for confirmation before overwriting it`, - }, + }, - { - name: ["-O", "--LOG-FILE"], - args: { name: "filename", template: "filepaths" }, - description: `The -O option is like -o, but it will overwrite an existing file + { + name: ["-O", "--LOG-FILE"], + args: { name: "filename", template: "filepaths" }, + description: `The -O option is like -o, but it will overwrite an existing file without asking for confirmation. If no log file has been specified, the -o and -O options can be used from within less to specify a log file. Without a file name, they will simply report the name of the log file. The "s" command is equivalent to specifying -o from within less`, - }, + }, - { - name: ["-p", "--pattern"], - args: { name: "pattern" }, - description: `The -p option on the command line is equivalent to specifying + { + name: ["-p", "--pattern"], + args: { name: "pattern" }, + description: `The -p option on the command line is equivalent to specifying +/pattern; that is, it tells less to start at the first occurrence of pattern in the file`, - }, + }, - { - name: ["-P", "--prompt"], - args: { name: "prompt" }, - description: `Provides a way to tailor the three prompt styles to your own + { + name: ["-P", "--prompt"], + args: { name: "prompt" }, + description: `Provides a way to tailor the three prompt styles to your own preference. This option would normally be put in the LESS environment variable, rather than being typed in with each less command. Such an option must either be the last option in the @@ -352,28 +352,28 @@ that string. F command). All prompt strings consist of a sequence of letters and special escape sequences. See the section on PROMPTS for more details`, - }, + }, - { - name: ["-q", "--quiet", "--silent"], - description: `Causes moderately "quiet" operation: the terminal bell is not + { + name: ["-q", "--quiet", "--silent"], + description: `Causes moderately "quiet" operation: the terminal bell is not rung if an attempt is made to scroll past the end of the file or before the beginning of the file. If the terminal has a "visual bell", it is used instead. The bell will be rung on certain other errors, such as typing an invalid character. The default is to ring the terminal bell in all such cases`, - }, + }, - { - name: ["-Q", "--QUIET", "--SILENT"], - description: `Causes totally "quiet" operation: the terminal bell is never + { + name: ["-Q", "--QUIET", "--SILENT"], + description: `Causes totally "quiet" operation: the terminal bell is never rung. If the terminal has a "visual bell", it is used in all cases where the terminal bell would have been rung`, - }, + }, - { - name: ["-r", "--raw-control-chars"], - description: `Causes "raw" control characters to be displayed. The default is + { + name: ["-r", "--raw-control-chars"], + description: `Causes "raw" control characters to be displayed. The default is to display control characters using the caret notation; for example, a control-A (octal 001) is displayed as "^A". Warning: when the -r option is used, less cannot keep track of the actual @@ -382,11 +382,11 @@ responds to each type of control character). Thus, various display problems may result, such as long lines being split in the wrong place. USE OF THE -r OPTION IS NOT RECOMMENDED`, - }, + }, - { - name: ["-R", "--RAW-CONTROL-CHARS"], - description: `Like -r, but only ANSI "color" escape sequences and OSC 8 + { + name: ["-R", "--RAW-CONTROL-CHARS"], + description: `Like -r, but only ANSI "color" escape sequences and OSC 8 hyperlink sequences are output in "raw" form. Unlike -r, the screen appearance is maintained correctly, provided that there are no escape sequences in the file other than these types of @@ -408,27 +408,27 @@ escape sequence. And you can make less think that characters other than the standard ones may appear between the ESC and the m by setting the environment variable LESSANSIMIDCHARS to the list of characters which can appear`, - }, + }, - { - name: ["-s", "--squeeze-blank-lines"], - description: `Causes consecutive blank lines to be squeezed into a single + { + name: ["-s", "--squeeze-blank-lines"], + description: `Causes consecutive blank lines to be squeezed into a single blank line. This is useful when viewing nroff output`, - }, + }, - { - name: ["-S", "--chop-long-lines"], - description: `Causes lines longer than the screen width to be chopped + { + name: ["-S", "--chop-long-lines"], + description: `Causes lines longer than the screen width to be chopped (truncated) rather than wrapped. That is, the portion of a long line that does not fit in the screen width is not displayed until you press RIGHT-ARROW. The default is to wrap long lines; that is, display the remainder on the next line`, - }, + }, - { - name: ["-t", "--tag"], - args: { name: "tag" }, - description: `The -t option, followed immediately by a TAG, will edit the file + { + name: ["-t", "--tag"], + args: { name: "tag" }, + description: `The -t option, followed immediately by a TAG, will edit the file containing that tag. For this to work, tag information must be available; for example, there may be a file in the current directory called "tags", which was previously built by ctags(1) @@ -439,24 +439,24 @@ the tag. (See http://www.gnu.org/software/global/global.html). The -t option may also be specified from within less (using the - command) as a way of examining a new file. The command ":t" is equivalent to specifying -t from within less`, - }, + }, - { - name: ["-T", "--tag-file"], - args: { name: "tagsfile" }, - description: `Specifies a tags file to be used instead of "tags"`, - }, + { + name: ["-T", "--tag-file"], + args: { name: "tagsfile" }, + description: `Specifies a tags file to be used instead of "tags"`, + }, - { - name: ["-u", "--underline-special"], - description: `Causes backspaces and carriage returns to be treated as + { + name: ["-u", "--underline-special"], + description: `Causes backspaces and carriage returns to be treated as printable characters; that is, they are sent to the terminal when they appear in the input`, - }, + }, - { - name: ["-U", "--UNDERLINE-SPECIAL"], - description: `Causes backspaces, tabs, carriage returns and "formatting + { + name: ["-U", "--UNDERLINE-SPECIAL"], + description: `Causes backspaces, tabs, carriage returns and "formatting characters" (as defined by Unicode) to be treated as control characters; that is, they are handled as specified by the -r option. @@ -473,16 +473,16 @@ specified by the -r option. Unicode formatting characters, such as the Byte Order Mark, are sent to the terminal. Text which is overstruck or underlined can be searched for if neither -u nor -U is in effect`, - }, + }, - { - name: ["-V", "--version"], - description: `Displays the version number of less`, - }, + { + name: ["-V", "--version"], + description: `Displays the version number of less`, + }, - { - name: ["-w", "--hilite-unread"], - description: `Temporarily highlights the first "new" line after a forward + { + name: ["-w", "--hilite-unread"], + description: `Temporarily highlights the first "new" line after a forward movement of a full page. The first "new" line is the line immediately following the line previously at the bottom of the screen. Also highlights the target line after a g or p command. @@ -490,47 +490,47 @@ The highlight is removed at the next command which causes movement. The entire line is highlighted, unless the -J option is in effect, in which case only the status column is highlighted`, - }, + }, - { - name: ["-W", "--HILITE-UNREAD"], - description: `Like -w, but temporarily highlights the first new line after any + { + name: ["-W", "--HILITE-UNREAD"], + description: `Like -w, but temporarily highlights the first new line after any forward movement command larger than one line`, - }, + }, - { - name: ["-x", "--tabs="], - args: { name: "n,..." }, - description: `Sets tab stops. If only one n is specified, tab stops are set + { + name: ["-x", "--tabs="], + args: { name: "n,..." }, + description: `Sets tab stops. If only one n is specified, tab stops are set at multiples of n. If multiple values separated by commas are specified, tab stops are set at those positions, and then continue with the same spacing as the last two. For example, -x9,17 will set tabs at positions 9, 17, 25, 33, etc. The default for n is 8`, - }, + }, - { - name: ["-X", "--no-init"], - description: `Disables sending the termcap initialization and deinitialization + { + name: ["-X", "--no-init"], + description: `Disables sending the termcap initialization and deinitialization strings to the terminal. This is sometimes desirable if the deinitialization string does something unnecessary, like clearing the screen`, - }, + }, - { - name: ["-y", "--max-forw-scroll"], - args: { name: "n" }, - description: `Specifies a maximum number of lines to scroll forward. If it is + { + name: ["-y", "--max-forw-scroll"], + args: { name: "n" }, + description: `Specifies a maximum number of lines to scroll forward. If it is necessary to scroll forward more than n lines, the screen is repainted instead. The -c or -C option may be used to repaint from the top of the screen if desired. By default, any forward movement causes scrolling`, - }, + }, - { - name: ["-z", "--window"], - args: { name: "n" }, - description: `Changes the default scrolling window size to n lines. The + { + name: ["-z", "--window"], + args: { name: "n" }, + description: `Changes the default scrolling window size to n lines. The default is one screenful. The z and w commands can also be used to change the window size. The "z" may be omitted for compatibility with some versions of more. If the number n is @@ -538,11 +538,11 @@ negative, it indicates n lines less than the current screen size. For example, if the screen is 24 lines, -z-4 sets the scrolling window to 20 lines. If the screen is resized to 40 lines, the scrolling window automatically changes to 36 lines`, - }, + }, - { - name: "--quotes", - description: `Changes the filename quoting character. This may be necessary + { + name: "--quotes", + description: `Changes the filename quoting character. This may be necessary if you are trying to name a file which contains both spaces and quote characters. Followed by a single character, this changes the quote character to that character. Filenames containing a @@ -554,18 +554,18 @@ by the open quote character and followed by the close quote character. Note that even after the quote characters are changed, this option remains -" (a dash followed by a double quote)`, - }, + }, - { - name: ["-~", "--tilde"], - description: `Normally lines after end of file are displayed as a single tilde + { + name: ["-~", "--tilde"], + description: `Normally lines after end of file are displayed as a single tilde (~). This option causes lines after end of file to be displayed as blank lines`, - }, + }, - { - name: ["-#", "--shift"], - description: `Specifies the default number of positions to scroll horizontally + { + name: ["-#", "--shift"], + description: `Specifies the default number of positions to scroll horizontally in the RIGHTARROW and LEFTARROW commands. If the number specified is zero, it sets the default number of positions to one half of the screen width. Alternately, the number may be @@ -576,11 +576,11 @@ specified as a fraction, the actual number of scroll positions is recalculated if the terminal window is resized, so that the actual scroll remains at the specified fraction of the screen width`, - }, + }, - { - name: "--follow-name", - description: `Normally, if the input file is renamed while an F command is + { + name: "--follow-name", + description: `Normally, if the input file is renamed while an F command is executing, less will continue to display the contents of the original file despite its name change. If --follow-name is specified, during an F command less will periodically attempt to @@ -588,89 +588,89 @@ reopen the file by name. If the reopen succeeds and the file is a different file from the original (which means that a new file has been created with the same name as the original (now renamed) file), less will display the contents of that new file`, - }, - { - name: "--incsearch", - description: `Subsequent search commands will be "incremental"; that is, less + }, + { + name: "--incsearch", + description: `Subsequent search commands will be "incremental"; that is, less will advance to the next line containing the search pattern as each character of the pattern is typed in`, - }, + }, - { - name: "--line-num-width", - description: `Sets the minimum width of the line number field when the -N + { + name: "--line-num-width", + description: `Sets the minimum width of the line number field when the -N option is in effect. The default is 7 characters`, - }, - { - name: "--mouse", - description: `Enables mouse input: scrolling the mouse wheel down moves + }, + { + name: "--mouse", + description: `Enables mouse input: scrolling the mouse wheel down moves forward in the file, scrolling the mouse wheel up moves backwards in the file, and clicking the mouse sets the "#" mark to the line where the mouse is clicked. The number of lines to scroll when the wheel is moved can be set by the --wheel-lines option. Mouse input works only on terminals which support X11 mouse reporting, and on the Windows version of less`, - }, - { - name: "--MOUSE", - description: `Like --mouse, except the direction scrolled on mouse wheel + }, + { + name: "--MOUSE", + description: `Like --mouse, except the direction scrolled on mouse wheel movement is reversed`, - }, - { - name: "--no-keypad", - description: `Disables sending the keypad initialization and deinitialization + }, + { + name: "--no-keypad", + description: `Disables sending the keypad initialization and deinitialization strings to the terminal. This is sometimes useful if the keypad strings make the numeric keypad behave in an undesirable manner`, - }, - { - name: "--no-histdups", - description: `This option changes the behavior so that if a search string or + }, + { + name: "--no-histdups", + description: `This option changes the behavior so that if a search string or file name is typed in, and the same string is already in the history list, the existing copy is removed from the history list before the new one is added. Thus, a given string will appear only once in the history list. Normally, a string may appear multiple times`, - }, - { - name: "--rscroll", - description: `This option changes the character used to mark truncated lines. + }, + { + name: "--rscroll", + description: `This option changes the character used to mark truncated lines. It may begin with a two-character attribute indicator like LESSBINFMT does. If there is no attribute indicator, standout is used. If set to "-", truncated lines are not marked`, - }, - { - name: "--save-marks", - description: `Save marks in the history file, so marks are retained across + }, + { + name: "--save-marks", + description: `Save marks in the history file, so marks are retained across different invocations of less`, - }, - { - name: "--status-col-width", - description: `Sets the width of the status column when the -J option is in + }, + { + name: "--status-col-width", + description: `Sets the width of the status column when the -J option is in effect. The default is 2 characters`, - }, - { - name: "--use-backslash", - description: `This option changes the interpretations of options which follow + }, + { + name: "--use-backslash", + description: `This option changes the interpretations of options which follow this one. After the --use-backslash option, any backslash in an option string is removed and the following character is taken literally. This allows a dollar sign to be included in option strings`, - }, - { - name: "--use-color", - description: `Enables the colored text in various places. The -D option can + }, + { + name: "--use-color", + description: `Enables the colored text in various places. The -D option can be used to change the colors. Colored text works only if the terminal supports ANSI color escape sequences (as defined in ECMA-48 SGR; see https://www.ecma-international.org/publications-and- standards/standards/ecma-48)`, - }, - { - name: "--wheel-lines", - args: { name: "n" }, - description: `Set the number of lines to scroll when the mouse wheel is rolled`, - }, - ], + }, + { + name: "--wheel-lines", + args: { name: "n" }, + description: `Set the number of lines to scroll when the mouse wheel is rolled`, + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/mkdir.ts b/extensions/terminal-suggest/src/completions/upstream/mkdir.ts index 90a6530c..2f9ff425 100644 --- a/extensions/terminal-suggest/src/completions/upstream/mkdir.ts +++ b/extensions/terminal-suggest/src/completions/upstream/mkdir.ts @@ -1,37 +1,37 @@ const completionSpec: Fig.Spec = { - name: "mkdir", - description: "Make directories", - args: { - name: "directory name", - template: "folders", - suggestCurrentToken: true, - }, - options: [ - { - name: ["-m", "--mode"], - description: "Set file mode (as in chmod), not a=rwx - umask", - args: { name: "MODE" }, - }, - { - name: ["-p", "--parents"], - description: "No error if existing, make parent directories as needed", - }, - { - name: ["-v", "--verbose"], - description: "Print a message for each created directory", - }, - { - name: ["-Z", "--context"], - description: - "Set the SELinux security context of each created directory to CTX", - args: { name: "CTX" }, - }, - { name: "--help", description: "Display this help and exit" }, - { - name: "--version", - description: "Output version information and exit", - }, - ], + name: "mkdir", + description: "Make directories", + args: { + name: "directory name", + template: "folders", + suggestCurrentToken: true, + }, + options: [ + { + name: ["-m", "--mode"], + description: "Set file mode (as in chmod), not a=rwx - umask", + args: { name: "MODE" }, + }, + { + name: ["-p", "--parents"], + description: "No error if existing, make parent directories as needed", + }, + { + name: ["-v", "--verbose"], + description: "Print a message for each created directory", + }, + { + name: ["-Z", "--context"], + description: + "Set the SELinux security context of each created directory to CTX", + args: { name: "CTX" }, + }, + { name: "--help", description: "Display this help and exit" }, + { + name: "--version", + description: "Output version information and exit", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/more.ts b/extensions/terminal-suggest/src/completions/upstream/more.ts index a70c8ee9..6a9d6e82 100644 --- a/extensions/terminal-suggest/src/completions/upstream/more.ts +++ b/extensions/terminal-suggest/src/completions/upstream/more.ts @@ -1,55 +1,55 @@ const completionSpec: Fig.Spec = { - name: "more", - description: "Opposite of less", - options: [ - { - name: ["-d", "--silent"], - description: - "Prompt with '[Press space to continue, 'q' to quit.]', and display '[Press 'h' for instructions.]' instead of ringing the bell when an illegal key is pressed", - }, - { - name: ["-l", "--logical"], - description: "Do not pause after any line containing a ^L (form feed)", - }, - { - name: ["-f", "--no-pause"], - description: "Count logical lines, rather than screen lines", - }, - { - name: ["-p", "--print-over"], - description: "Instead, clear the whole screen and then display the text", - }, - { - name: ["-c", "--clean-print"], - description: - "Instead, paint each screen from the top, clearing the remainder of each line as it is displayed", - }, - { - name: ["-s", "--squeeze"], - description: "Squeeze multiple blank lines into one", - }, - { - name: ["-u", "--plain"], - description: "Silently ignored as backwards compatibility", - }, - { - name: ["-n", "--lines"], - description: "Specify the number of lines per screenful", - args: { name: "n" }, - }, - { - name: "--help", - description: "Display help text", - }, - { - name: ["-V", "--version"], - description: "Display version information", - }, - ], - args: { - isVariadic: true, - template: "filepaths", - }, + name: "more", + description: "Opposite of less", + options: [ + { + name: ["-d", "--silent"], + description: + "Prompt with '[Press space to continue, 'q' to quit.]', and display '[Press 'h' for instructions.]' instead of ringing the bell when an illegal key is pressed", + }, + { + name: ["-l", "--logical"], + description: "Do not pause after any line containing a ^L (form feed)", + }, + { + name: ["-f", "--no-pause"], + description: "Count logical lines, rather than screen lines", + }, + { + name: ["-p", "--print-over"], + description: "Instead, clear the whole screen and then display the text", + }, + { + name: ["-c", "--clean-print"], + description: + "Instead, paint each screen from the top, clearing the remainder of each line as it is displayed", + }, + { + name: ["-s", "--squeeze"], + description: "Squeeze multiple blank lines into one", + }, + { + name: ["-u", "--plain"], + description: "Silently ignored as backwards compatibility", + }, + { + name: ["-n", "--lines"], + description: "Specify the number of lines per screenful", + args: { name: "n" }, + }, + { + name: "--help", + description: "Display help text", + }, + { + name: ["-V", "--version"], + description: "Display version information", + }, + ], + args: { + isVariadic: true, + template: "filepaths", + }, }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/mv.ts b/extensions/terminal-suggest/src/completions/upstream/mv.ts index bbf98bf4..2109b0dd 100644 --- a/extensions/terminal-suggest/src/completions/upstream/mv.ts +++ b/extensions/terminal-suggest/src/completions/upstream/mv.ts @@ -1,40 +1,40 @@ const completionSpec: Fig.Spec = { - name: "mv", - description: "Move & rename files and folders", - args: [ - { - name: "source", - isVariadic: true, - template: ["filepaths", "folders"], - }, - { - name: "target", - template: ["filepaths", "folders"], - }, - ], - options: [ - { - name: "-f", - description: - "Do not prompt for confirmation before overwriting the destination path", - exclusiveOn: ["-i", "-n"], - }, - { - name: "-i", - description: - "Cause mv to write a prompt to standard error before moving a file that would overwrite an existing file", - exclusiveOn: ["-f", "-n"], - }, - { - name: "-n", - description: "Do not overwrite existing file", - exclusiveOn: ["-f", "-i"], - }, - { - name: "-v", - description: "Cause mv to be verbose, showing files after they are moved", - }, - ], + name: "mv", + description: "Move & rename files and folders", + args: [ + { + name: "source", + isVariadic: true, + template: ["filepaths", "folders"], + }, + { + name: "target", + template: ["filepaths", "folders"], + }, + ], + options: [ + { + name: "-f", + description: + "Do not prompt for confirmation before overwriting the destination path", + exclusiveOn: ["-i", "-n"], + }, + { + name: "-i", + description: + "Cause mv to write a prompt to standard error before moving a file that would overwrite an existing file", + exclusiveOn: ["-f", "-n"], + }, + { + name: "-n", + description: "Do not overwrite existing file", + exclusiveOn: ["-f", "-i"], + }, + { + name: "-v", + description: "Cause mv to be verbose, showing files after they are moved", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/nano.ts b/extensions/terminal-suggest/src/completions/upstream/nano.ts index 6064072e..caa84c34 100644 --- a/extensions/terminal-suggest/src/completions/upstream/nano.ts +++ b/extensions/terminal-suggest/src/completions/upstream/nano.ts @@ -1,9 +1,9 @@ const completionSpec: Fig.Spec = { - name: "nano", - description: "Nano's ANOther editor, an enhanced free Pico clone", - args: { - template: "filepaths", - }, + name: "nano", + description: "Nano's ANOther editor, an enhanced free Pico clone", + args: { + template: "filepaths", + }, }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/node.ts b/extensions/terminal-suggest/src/completions/upstream/node.ts index b967b854..d6e7b52e 100644 --- a/extensions/terminal-suggest/src/completions/upstream/node.ts +++ b/extensions/terminal-suggest/src/completions/upstream/node.ts @@ -82,423 +82,422 @@ const completionSpec: Fig.Subcommand = { "Follows symlinks to directories when examining source code and templates for translation strings", }, ], - // generateSpec: async (tokens, executeShellCommand) => { - // const isAdonisJsonPresentCommand = "test -f .adonisrc.json"; - // if ( - // ( - // await executeShellCommand({ - // command: "bash", - // args: ["-c", "isAdonisJsonPresentCommand"], - // }) - // ).status === 0 - // ) { - // return { - // name: "node", - // subcommands: [ - // { - // name: "ace", - // description: "Run AdonisJS command-line", - // options: [ - // { - // name: ["-h", "--help"], - // description: "Display AdonisJS Ace help", - // }, - // { - // name: ["-v", "--version"], - // description: "Display AdonisJS version", - // }, - // ], - // subcommands: [ - // { - // name: "build", - // description: - // "Compile project from Typescript to Javascript. Also compiles the frontend assets if using webpack encore", - // options: [ - // { - // name: ["-prod", "--production"], - // description: "Build for production", - // }, - // { - // name: "--assets", - // description: - // "Build frontend assets when webpack encore is installed", - // }, - // { - // name: "--no-assets", - // description: "Disable building assets", - // }, - // { - // name: "--ignore-ts-errors", - // description: - // "Ignore typescript errors and complete the build process", - // }, - // { - // name: "--tsconfig", - // description: - // "Path to the TypeScript project configuration file", - // args: { - // name: "path", - // description: "Path to tsconfig.json", - // }, - // }, - // { - // name: "--encore-args", - // requiresSeparator: true, - // insertValue: "--encore-args='{cursor}'", - // description: - // "CLI options to pass to the encore command line", - // }, - // { - // name: "--client", - // args: { - // name: "name", - // }, - // description: - // "Select the package manager to decide which lock file to copy to the build folder", - // }, - // ], - // }, - // { - // name: ["configure", "invoke"], - // description: "Configure a given AdonisJS package", - // args: { - // name: "name", - // description: "Name of the package you want to configure", - // }, - // subcommands: [ - // { - // name: "@adonisjs/auth", - // description: "Trigger auto configuring auth package", - // }, - // { - // name: "@adonisjs/shield", - // description: "Trigger auto configuring shield package", - // }, - // { - // name: "@adonisjs/redis", - // description: "Trigger auto configuring redis package", - // }, - // { - // name: "@adonisjs/mail", - // description: "Trigger auto configuring mail package", - // }, - // ], - // }, - // { - // name: "repl", - // description: "Start a new REPL session", - // }, - // { - // name: "serve", - // description: - // "Start the AdonisJS HTTP server, along with the file watcher. Also starts the webpack dev server when webpack encore is installed", - // options: [ - // { - // name: "--assets", - // description: - // "Start webpack dev server when encore is installed", - // }, - // { - // name: "--no-assets", - // description: "Disable webpack dev server", - // }, - // { - // name: ["-w", "--watch"], - // description: - // "Watch for file changes and re-start the HTTP server on change", - // }, - // { - // name: ["-p", "--poll"], - // description: - // "Detect file changes by polling files instead of listening to filesystem events", - // }, - // { - // name: "--node-args", - // requiresSeparator: true, - // insertValue: "--node-args='{cursor}'", - // description: "CLI options to pass to the node command line", - // }, - // { - // name: "--encore-args", - // requiresSeparator: true, - // insertValue: "--encore-args='{cursor}'", - // description: - // "CLI options to pass to the encore command line", - // }, - // ], - // }, - // { - // name: "db:seed", - // description: "Execute database seeder files", - // options: [ - // { - // name: ["-c", "--connection"], - // description: - // "Define a custom database connection for the seeders", - // args: { - // name: "name", - // }, - // }, - // { - // name: ["-i", "--interactive"], - // description: "Run seeders in interactive mode", - // }, - // { - // name: ["-f", "--files"], - // args: { - // name: "file", - // isVariadic: true, - // template: "filepaths", - // }, - // description: - // "Define a custom set of seeders files names to run", - // }, - // ], - // }, - // { - // name: "dump:rcfile", - // description: - // "Dump contents of .adonisrc.json file along with defaults", - // }, - // { - // name: "generate:key", - // description: "Generate a new APP_KEY secret", - // }, - // { - // name: "generate:manifest", - // description: - // "Generate ace commands manifest file. Manifest file speeds up commands lookup", - // }, - // { - // name: "list:routes", - // description: "List application routes", - // }, - // { - // name: "make:command", - // description: "Make a new ace command", - // }, - // { - // name: "make:controller", - // description: "Make a new HTTP controller", - // args: { - // name: "name", - // description: "Name of the controller class", - // }, - // options: [ - // { - // name: ["-r", "--resource"], - // description: - // "Add resourceful methods to the controller class", - // }, - // { - // name: ["-e", "--exact"], - // description: - // "Create the controller with the exact name as provided", - // }, - // ], - // }, - // { - // name: "make:exception", - // description: "Make a new custom exception class", - // }, - // { - // name: "make:listener", - // description: "Make a new event listener class", - // }, - // { - // name: "make:mailer", - // description: "Make a new mailer class", - // args: { - // name: "name", - // description: "Mailer class name", - // }, - // }, - // { - // name: "make:middleware", - // description: "Make a new middleware", - // args: { - // name: "name", - // description: "Middleware class name", - // }, - // }, - // { - // name: "make:migration", - // description: "Make a new migration file", - // args: { - // name: "name", - // description: "Name of the migration file", - // }, - // options: [ - // { - // name: "--connection", - // description: - // "The connection flag is used to lookup the directory for the migration file", - // args: { - // name: "name", - // }, - // }, - // { - // name: "--folder", - // description: "Pre-select a migration directory", - // args: { - // name: "name", - // template: "filepaths", - // }, - // }, - // { - // name: "--create", - // description: - // "Define the table name for creating a new table", - // args: { - // name: "name", - // }, - // }, - // { - // name: "--table", - // description: - // "Define the table name for altering an existing table", - // args: { - // name: "name", - // }, - // }, - // ], - // }, - // { - // name: "make:model", - // description: "Make a new Lucid model", - // args: { - // name: "name", - // description: "Name of the model class", - // }, - // options: [ - // { - // name: ["-m", "--migration"], - // description: "Generate the migration for the model", - // }, - // { - // name: ["-c", "--controller"], - // description: "Generate the controller for the model", - // }, - // ], - // }, - // { - // name: "make:prldfile", - // description: "Make a new preload file", - // subcommands: [ - // { - // name: "events", - // description: "Make events preload file", - // }, - // ], - // }, - // { - // name: "make:provider", - // description: "Make a new provider class", - // }, - // { - // name: "make:seeder", - // description: "Make a new Seeder file", - // args: { - // name: "name", - // description: "Name of the seeder class", - // }, - // }, - // { - // name: "make:validator", - // description: "Make a new validator", - // args: { - // name: "name", - // description: "Name of the validator class", - // }, - // options: [ - // { - // name: ["-e", "--exact"], - // description: - // "Create the validator with the exact name as provided", - // }, - // ], - // }, - // { - // name: "make:view", - // description: "Make a new view template", - // args: { - // name: "name", - // description: "Name of the view", - // }, - // options: [ - // { - // name: ["-e", "--exact"], - // description: - // "Create the template file with the exact name as provided", - // }, - // ], - // }, - // { - // name: "migration:rollback", - // description: "Rollback migrations to a given batch number", - // options: [ - // { - // name: ["-c", "--connection"], - // description: "Define a custom database connection", - // args: { - // name: "name", - // }, - // }, - // { - // name: "--force", - // description: - // "Explicitly force to run migrations in production", - // isDangerous: true, - // }, - // { - // name: "--dry-run", - // description: - // "Print SQL queries, instead of running the migrations", - // }, - // { - // name: "--batch", - // args: { - // name: "number", - // description: "Use 0 to rollback to initial state", - // }, - // description: "Define custom batch number for rollback", - // }, - // ], - // }, - // { - // name: "migration:run", - // description: "Run pending migrations", - // options: [ - // { - // name: ["-c", "--connection"], - // description: "Define a custom database connection", - // args: { - // name: "name", - // }, - // }, - // { - // name: "--force", - // description: - // "Explicitly force to run migrations in production", - // isDangerous: true, - // }, - // { - // name: "--dry-run", - // description: - // "Print SQL queries, instead of running the migrations", - // }, - // ], - // }, - // { - // name: "migration:status", - // description: "Check migrations current status", - // }, - // ], - // }, - // ], - // }; - // } - // }, + generateSpec: async (tokens, executeShellCommand) => { + if ( + ( + await executeShellCommand({ + command: "bash", + args: ["-c", "isAdonisJsonPresentCommand"], + }) + ).status === 0 + ) { + return { + name: "node", + subcommands: [ + { + name: "ace", + description: "Run AdonisJS command-line", + options: [ + { + name: ["-h", "--help"], + description: "Display AdonisJS Ace help", + }, + { + name: ["-v", "--version"], + description: "Display AdonisJS version", + }, + ], + subcommands: [ + { + name: "build", + description: + "Compile project from Typescript to Javascript. Also compiles the frontend assets if using webpack encore", + options: [ + { + name: ["-prod", "--production"], + description: "Build for production", + }, + { + name: "--assets", + description: + "Build frontend assets when webpack encore is installed", + }, + { + name: "--no-assets", + description: "Disable building assets", + }, + { + name: "--ignore-ts-errors", + description: + "Ignore typescript errors and complete the build process", + }, + { + name: "--tsconfig", + description: + "Path to the TypeScript project configuration file", + args: { + name: "path", + description: "Path to tsconfig.json", + }, + }, + { + name: "--encore-args", + requiresSeparator: true, + insertValue: "--encore-args='{cursor}'", + description: + "CLI options to pass to the encore command line", + }, + { + name: "--client", + args: { + name: "name", + }, + description: + "Select the package manager to decide which lock file to copy to the build folder", + }, + ], + }, + { + name: ["configure", "invoke"], + description: "Configure a given AdonisJS package", + args: { + name: "name", + description: "Name of the package you want to configure", + }, + subcommands: [ + { + name: "@adonisjs/auth", + description: "Trigger auto configuring auth package", + }, + { + name: "@adonisjs/shield", + description: "Trigger auto configuring shield package", + }, + { + name: "@adonisjs/redis", + description: "Trigger auto configuring redis package", + }, + { + name: "@adonisjs/mail", + description: "Trigger auto configuring mail package", + }, + ], + }, + { + name: "repl", + description: "Start a new REPL session", + }, + { + name: "serve", + description: + "Start the AdonisJS HTTP server, along with the file watcher. Also starts the webpack dev server when webpack encore is installed", + options: [ + { + name: "--assets", + description: + "Start webpack dev server when encore is installed", + }, + { + name: "--no-assets", + description: "Disable webpack dev server", + }, + { + name: ["-w", "--watch"], + description: + "Watch for file changes and re-start the HTTP server on change", + }, + { + name: ["-p", "--poll"], + description: + "Detect file changes by polling files instead of listening to filesystem events", + }, + { + name: "--node-args", + requiresSeparator: true, + insertValue: "--node-args='{cursor}'", + description: "CLI options to pass to the node command line", + }, + { + name: "--encore-args", + requiresSeparator: true, + insertValue: "--encore-args='{cursor}'", + description: + "CLI options to pass to the encore command line", + }, + ], + }, + { + name: "db:seed", + description: "Execute database seeder files", + options: [ + { + name: ["-c", "--connection"], + description: + "Define a custom database connection for the seeders", + args: { + name: "name", + }, + }, + { + name: ["-i", "--interactive"], + description: "Run seeders in interactive mode", + }, + { + name: ["-f", "--files"], + args: { + name: "file", + isVariadic: true, + template: "filepaths", + }, + description: + "Define a custom set of seeders files names to run", + }, + ], + }, + { + name: "dump:rcfile", + description: + "Dump contents of .adonisrc.json file along with defaults", + }, + { + name: "generate:key", + description: "Generate a new APP_KEY secret", + }, + { + name: "generate:manifest", + description: + "Generate ace commands manifest file. Manifest file speeds up commands lookup", + }, + { + name: "list:routes", + description: "List application routes", + }, + { + name: "make:command", + description: "Make a new ace command", + }, + { + name: "make:controller", + description: "Make a new HTTP controller", + args: { + name: "name", + description: "Name of the controller class", + }, + options: [ + { + name: ["-r", "--resource"], + description: + "Add resourceful methods to the controller class", + }, + { + name: ["-e", "--exact"], + description: + "Create the controller with the exact name as provided", + }, + ], + }, + { + name: "make:exception", + description: "Make a new custom exception class", + }, + { + name: "make:listener", + description: "Make a new event listener class", + }, + { + name: "make:mailer", + description: "Make a new mailer class", + args: { + name: "name", + description: "Mailer class name", + }, + }, + { + name: "make:middleware", + description: "Make a new middleware", + args: { + name: "name", + description: "Middleware class name", + }, + }, + { + name: "make:migration", + description: "Make a new migration file", + args: { + name: "name", + description: "Name of the migration file", + }, + options: [ + { + name: "--connection", + description: + "The connection flag is used to lookup the directory for the migration file", + args: { + name: "name", + }, + }, + { + name: "--folder", + description: "Pre-select a migration directory", + args: { + name: "name", + template: "filepaths", + }, + }, + { + name: "--create", + description: + "Define the table name for creating a new table", + args: { + name: "name", + }, + }, + { + name: "--table", + description: + "Define the table name for altering an existing table", + args: { + name: "name", + }, + }, + ], + }, + { + name: "make:model", + description: "Make a new Lucid model", + args: { + name: "name", + description: "Name of the model class", + }, + options: [ + { + name: ["-m", "--migration"], + description: "Generate the migration for the model", + }, + { + name: ["-c", "--controller"], + description: "Generate the controller for the model", + }, + ], + }, + { + name: "make:prldfile", + description: "Make a new preload file", + subcommands: [ + { + name: "events", + description: "Make events preload file", + }, + ], + }, + { + name: "make:provider", + description: "Make a new provider class", + }, + { + name: "make:seeder", + description: "Make a new Seeder file", + args: { + name: "name", + description: "Name of the seeder class", + }, + }, + { + name: "make:validator", + description: "Make a new validator", + args: { + name: "name", + description: "Name of the validator class", + }, + options: [ + { + name: ["-e", "--exact"], + description: + "Create the validator with the exact name as provided", + }, + ], + }, + { + name: "make:view", + description: "Make a new view template", + args: { + name: "name", + description: "Name of the view", + }, + options: [ + { + name: ["-e", "--exact"], + description: + "Create the template file with the exact name as provided", + }, + ], + }, + { + name: "migration:rollback", + description: "Rollback migrations to a given batch number", + options: [ + { + name: ["-c", "--connection"], + description: "Define a custom database connection", + args: { + name: "name", + }, + }, + { + name: "--force", + description: + "Explicitly force to run migrations in production", + isDangerous: true, + }, + { + name: "--dry-run", + description: + "Print SQL queries, instead of running the migrations", + }, + { + name: "--batch", + args: { + name: "number", + description: "Use 0 to rollback to initial state", + }, + description: "Define custom batch number for rollback", + }, + ], + }, + { + name: "migration:run", + description: "Run pending migrations", + options: [ + { + name: ["-c", "--connection"], + description: "Define a custom database connection", + args: { + name: "name", + }, + }, + { + name: "--force", + description: + "Explicitly force to run migrations in production", + isDangerous: true, + }, + { + name: "--dry-run", + description: + "Print SQL queries, instead of running the migrations", + }, + ], + }, + { + name: "migration:status", + description: "Check migrations current status", + }, + ], + }, + ], + }; + } + }, }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/npm.ts b/extensions/terminal-suggest/src/completions/upstream/npm.ts index b48d8126..aa142e05 100644 --- a/extensions/terminal-suggest/src/completions/upstream/npm.ts +++ b/extensions/terminal-suggest/src/completions/upstream/npm.ts @@ -16,80 +16,82 @@ const atsInStr = (s: string) => (s.match(/@/g) || []).length; export const createNpmSearchHandler = (keywords?: string[]) => - async ( - context: string[], - executeShellCommand: Fig.ExecuteCommandFunction, - shellContext: Fig.ShellContext - ): Promise => { - const searchTerm = context[context.length - 1]; - if (searchTerm === "") { - return []; + async ( + context: string[], + executeShellCommand: Fig.ExecuteCommandFunction, + shellContext: Fig.ShellContext + ): Promise => { + const searchTerm = context[context.length - 1]; + if (searchTerm === "") { + return []; + } + // Add optional keyword parameter + const keywordParameter = + keywords && keywords.length > 0 ? `+keywords:${keywords.join(",")}` : ""; + + const queryPackagesUrl = keywordParameter + ? `https://api.npms.io/v2/search?size=20&q=${searchTerm}${keywordParameter}` + : `https://api.npms.io/v2/search/suggestions?q=${searchTerm}&size=20`; + + // Query the API with the package name + const queryPackages = [ + "-s", + "-H", + "Accept: application/json", + queryPackagesUrl, + ]; + // We need to remove the '@' at the end of the searchTerm before querying versions + const queryVersions = [ + "-s", + "-H", + "Accept: application/vnd.npm.install-v1+json", + `https://registry.npmjs.org/${searchTerm.slice(0, -1)}`, + ]; + // If the end of our token is '@', then we want to generate version suggestions + // Otherwise, we want packages + const out = (query: string) => + executeShellCommand({ + command: "curl", + args: query[query.length - 1] === "@" ? queryVersions : queryPackages, + }); + // If our token starts with '@', then a 2nd '@' tells us we want + // versions. + // Otherwise, '@' anywhere else in the string will indicate the same. + const shouldGetVersion = searchTerm.startsWith("@") + ? atsInStr(searchTerm) > 1 + : searchTerm.includes("@"); + + try { + const data = JSON.parse((await out(searchTerm)).stdout); + if (shouldGetVersion) { + // create dist tags suggestions + const versions = Object.entries(data["dist-tags"] || {}).map( + ([key, value]) => ({ + name: key, + description: value, + }) + ) as Fig.Suggestion[]; + // create versions + versions.push( + ...Object.keys(data.versions) + .map((version) => ({ name: version }) as Fig.Suggestion) + .reverse() + ); + return versions; } - // Add optional keyword parameter - const keywordParameter = - keywords && keywords.length > 0 ? `+keywords:${keywords.join(",")}` : ""; - const queryPackagesUrl = keywordParameter - ? `https://api.npms.io/v2/search?size=20&q=${searchTerm}${keywordParameter}` - : `https://api.npms.io/v2/search/suggestions?q=${searchTerm}&size=20`; - - // Query the API with the package name - const queryPackages = [ - "-s", - "-H", - "Accept: application/json", - queryPackagesUrl, - ]; - // We need to remove the '@' at the end of the searchTerm before querying versions - const queryVersions = [ - "-s", - "-H", - "Accept: application/vnd.npm.install-v1+json", - `https://registry.npmjs.org/${searchTerm.slice(0, -1)}`, - ]; - // If the end of our token is '@', then we want to generate version suggestions - // Otherwise, we want packages - const out = (query: string) => - executeShellCommand({ - command: "curl", - args: query[query.length - 1] === "@" ? queryVersions : queryPackages, - }); - // If our token starts with '@', then a 2nd '@' tells us we want - // versions. - // Otherwise, '@' anywhere else in the string will indicate the same. - const shouldGetVersion = searchTerm.startsWith("@") - ? atsInStr(searchTerm) > 1 - : searchTerm.includes("@"); - - try { - const data = JSON.parse((await out(searchTerm)).stdout); - if (shouldGetVersion) { - // create dist tags suggestions - const versions = Object.entries(data["dist-tags"] || {}).map( - ([key, value]) => ({ - name: key, - description: value, - }) - ) as Fig.Suggestion[]; - // create versions - versions.push( - ...Object.keys(data.versions) - .map((version) => ({ name: version }) as Fig.Suggestion) - .reverse() - ); - return versions; - } - - const results = keywordParameter ? data.results : data; - return results.map((item: any) => ({ + const results = keywordParameter ? data.results : data; + return results.map( + (item: { package: { name: string; description: string } }) => ({ name: item.package.name, description: item.package.description, - })) as Fig.Suggestion[]; - } catch (error) { - console.error({ error }); - return []; - } - }; + }) + ) as Fig.Suggestion[]; + } catch (error) { + console.error({ error }); + return []; + } + }; // GENERATORS export const npmSearchGenerator: Fig.Generator = { diff --git a/extensions/terminal-suggest/src/completions/upstream/npx.ts b/extensions/terminal-suggest/src/completions/upstream/npx.ts deleted file mode 100644 index 4143ed79..00000000 --- a/extensions/terminal-suggest/src/completions/upstream/npx.ts +++ /dev/null @@ -1,317 +0,0 @@ -// import autocannon from "./autocannon"; - -export const npxSuggestions: Fig.Suggestion[] = [ - // { - // name: autocannon.name, - // ...("icon" in autocannon && { icon: autocannon.icon }), - // }, - { - name: "vite", - icon: "https://vitejs.dev/logo.svg", - }, - { - name: "babel", - icon: "https://raw.githubusercontent.com/babel/logo/master/babel.png", - }, - { - name: "create-react-native-app", - icon: "https://reactnative.dev/img/pwa/manifest-icon-512.png", - }, - { - name: "react-native", - icon: "https://reactnative.dev/img/pwa/manifest-icon-512.png", - }, - { - name: "tailwindcss", - icon: "https://tailwindcss.com/favicons/favicon-32x32.png", - }, - { - name: "next", - icon: "https://nextjs.org/static/favicon/favicon-16x16.png", - }, - { - name: "nuxi", - icon: "https://raw.githubusercontent.com/nuxt/framework/main/docs/public/icon.png", - }, - { - name: "gltfjsx", - icon: "https://raw.githubusercontent.com/pmndrs/branding/master/logo.svg", - }, - { - name: "prisma", - icon: "https://raw.githubusercontent.com/prisma/docs/main/src/images/favicon-16x16.png", - }, - { - name: "eslint", - icon: "https://raw.githubusercontent.com/eslint/eslint.org/main/src/static/icon-512.png", - }, - { - name: "prettier", - icon: "https://prettier.io/icon.png", - }, - { - name: "tsc", - icon: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Typescript_logo_2020.svg/240px-Typescript_logo_2020.svg.png", - }, - { - name: "typeorm", - icon: "https://avatars.githubusercontent.com/u/20165699?s=200&v=4", - }, - // { - // name: "fig-teams", - // icon: "https://fig.io/icons/fig-light.png", - // }, - { - name: "@withfig/autocomplete-tools", - icon: "https://fig.io/icons/fig-light.png", - }, - { - name: "create-completion-spec", - icon: "https://fig.io/icons/fig-light.png", - }, - { - name: "@fig/publish-spec-to-team", - icon: "https://fig.io/icons/fig-light.png", - }, - { - name: "fig-teams@latest", - icon: "https://fig.io/icons/fig-light.png", - }, - { - name: "create-next-app", - icon: "https://nextjs.org/static/favicon/favicon-16x16.png", - }, - { - name: "create-t3-app", - icon: "https://create.t3.gg/favicon.svg", - }, - { - name: "create-discord-bot", - icon: "https://discordjs.dev/favicon-32x32.png", - }, - { - name: "create-video", - icon: "https://raw.githubusercontent.com/remotion-dev/remotion/main/packages/docs/static/img/logo-small.png", - }, - { - name: "remotion", - icon: "https://raw.githubusercontent.com/remotion-dev/remotion/main/packages/docs/static/img/logo-small.png", - }, - { - name: "create-remix", - icon: "https://remix.run/favicon-light.1.png", - }, - { - name: "remix", - icon: "https://remix.run/favicon-light.1.png", - }, - { - name: "playwright", - icon: "https://playwright.dev/img/playwright-logo.svg", - }, - { - name: "ignite-cli", - icon: "🔥", - }, - { - name: "vsce", - }, - { - name: "degit", - icon: "fig://icon?type=git", - }, - { - name: "@preset/cli", - icon: "https://raw.githubusercontent.com/preset/preset/main/.github/assets/logo.svg", - }, - { - name: "mikro-orm", - icon: "https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/favicon.ico", - }, - { - name: "pod-install", - }, - { - name: "capacitor", - icon: "https://capacitorjs.com/docs/img/meta/favicon.png", - }, - { - name: "cap", - icon: "https://capacitorjs.com/docs/img/meta/favicon.png", - }, - { - name: "@magnolia/cli", - icon: "https://avatars.githubusercontent.com/u/25686615?s=200&v=4", - }, - { - name: "stencil", - icon: "https://stenciljs.com/assets/icon/favicon.ico", - }, - { - name: "swagger-typescript-api", - icon: "https://static1.smartbear.co/swagger/media/assets/swagger_fav.png", - }, - { - name: "sta", - icon: "https://static1.smartbear.co/swagger/media/assets/swagger_fav.png", - }, - { - name: "@wordpress/create-block", - icon: "https://s1.wp.com/i/webclip.png", - }, - { - name: "astro", - icon: "https://astro.build/favicon.svg", - }, - { - name: "ampx", - icon: "https://raw.githubusercontent.com/aws-amplify/docs/refs/heads/main/public/favicon.ico", - }, -]; - -const completionSpec: Fig.Spec = { - name: "npx", - description: "Execute binaries from npm packages", - args: { - name: "command", - isCommand: true, - generators: { - script: [ - "bash", - "-c", - "until [[ -d node_modules/ ]] || [[ $PWD = '/' ]]; do cd ..; done; ls -1 node_modules/.bin/", - ], - postProcess: function (out) { - const cli = [...npxSuggestions].reduce( - (acc: any, { name }) => [...acc, name], - [] - ); - return out - .split("\n") - .filter((name) => !cli.includes(name)) - .map((name) => ({ - name, - icon: "fig://icon?type=command", - loadSpec: name, - })); - }, - }, - suggestions: [...npxSuggestions], - isOptional: true, - }, - - options: [ - { - name: ["--package", "-p"], - description: "Package to be installed", - args: { - name: "package", - }, - }, - { - name: "--cache", - args: { - name: "path", - template: "filepaths", - }, - description: "Location of the npm cache", - }, - { - name: "--always-spawn", - description: "Always spawn a child process to execute the command", - }, - { - name: "-y", - description: "Execute npx command without prompting for confirmation", - }, - { - description: "Skip installation if a package is missing", - name: "--no-install", - }, - { - args: { - name: "path", - template: "filepaths", - }, - description: "Path to user npmrc", - name: "--userconfig", - }, - { - name: ["--call", "-c"], - args: { - name: "script", - }, - description: "Execute string as if inside `npm run-script`", - }, - { - name: ["--shell", "-s"], - description: "Shell to execute the command with, if any", - args: { - name: "shell", - suggestions: [ - { - name: "bash", - }, - { - name: "fish", - }, - { - name: "zsh", - }, - ], - }, - }, - { - args: { - name: "shell-fallback", - suggestions: [ - { - name: "bash", - }, - { - name: "fish", - }, - { - name: "zsh", - }, - ], - }, - name: "--shell-auto-fallback", - description: - 'Generate shell code to use npx as the "command not found" fallback', - }, - { - name: "--ignore-existing", - description: - "Ignores existing binaries in $PATH, or in the localproject. This forces npx to do a temporary install and use the latest version", - }, - { - name: ["--quiet", "-q"], - description: - "Suppress output from npx itself. Subcommands will not be affected", - }, - { - name: "--npm", - args: { - name: "path to binary", - template: "filepaths", - }, - description: "Npm binary to use for internal operations", - }, - { - args: {}, - description: "Extra node argument when calling a node binary", - name: ["--node-arg", "-n"], - }, - { - description: "Show version number", - name: ["--version", "-v"], - }, - { - description: "Show help", - name: ["--help", "-h"], - }, - ], -}; - -export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/nvm.ts b/extensions/terminal-suggest/src/completions/upstream/nvm.ts index 843ffef5..6adafbcb 100644 --- a/extensions/terminal-suggest/src/completions/upstream/nvm.ts +++ b/extensions/terminal-suggest/src/completions/upstream/nvm.ts @@ -28,10 +28,6 @@ const args: Fig.Arg = { isVariadic: true, }; -// const pattern: Fig.Arg = { -// name: "pattern", -// }; - const name: Fig.Arg = { name: "name", }; diff --git a/extensions/terminal-suggest/src/completions/upstream/pnpm.ts b/extensions/terminal-suggest/src/completions/upstream/pnpm.ts index 17105495..9ce7c798 100644 --- a/extensions/terminal-suggest/src/completions/upstream/pnpm.ts +++ b/extensions/terminal-suggest/src/completions/upstream/pnpm.ts @@ -990,7 +990,8 @@ const completionSpec: Fig.Spec = { const { script, postProcess } = dependenciesGenerator as Fig.Generator & { script: string[]; }; - if (!postProcess) { + + if (postProcess === undefined) { return undefined; } @@ -1002,13 +1003,12 @@ const completionSpec: Fig.Spec = { }) ).stdout, tokens - )?.map((e: any) => e.name as string); - if (!packages) { - return undefined; - } + ) + ?.filter((e) => e !== null) + .map(({ name }) => name as string); const subcommands = packages - .filter((name) => nodeClis.has(name)) + ?.filter((name) => nodeClis.has(name)) .map((name) => ({ name, loadSpec: name, diff --git a/extensions/terminal-suggest/src/completions/upstream/ps.ts b/extensions/terminal-suggest/src/completions/upstream/ps.ts index 37c34815..17dd6623 100644 --- a/extensions/terminal-suggest/src/completions/upstream/ps.ts +++ b/extensions/terminal-suggest/src/completions/upstream/ps.ts @@ -1,153 +1,153 @@ const completionSpec: Fig.Spec = { - name: "ps", - description: "Report a snapshot of the current processes", - options: [ - { name: ["-A", "-e"], description: "Select all processes" }, - { - name: "-a", - description: "Select all processes except both session leaders", - args: { name: "getsid" }, - }, - { - name: "-d", - description: "Select all processes except session leaders", - }, - { - name: "--deselect", - description: - "Select all processes except those that fulfill the specified conditions", - }, - { - name: "-N", - description: - "Select all processes except those that fulfill the specified conditions (negates the selection)", - }, - { - name: "--pid", - description: "Select by process ID", - args: { name: "pidlist" }, - }, - { - name: "--ppid", - description: - "Select by parent process ID. This selects the processes with a parent process ID in pidlist", - args: { name: "pidlist" }, - }, - { - name: "--sid", - description: "Select by session ID", - args: { name: "sesslist" }, - }, - { - name: "--tty", - description: "Select by terminal", - args: { name: "ttylist" }, - }, - { - name: "U", - description: "Select by effective user ID (EUID) or name", - args: { name: "userlist" }, - }, - { - name: "-U", - description: "Select by real user ID (RUID) or name", - args: { name: "userlist" }, - }, - { - name: "-u", - description: "Select by effective user ID (EUID) or name", - args: { name: "userlist" }, - }, - { - name: "--User", - description: "Select by real user ID (RUID) or name", - args: { name: "userlist" }, - }, - { - name: "--user", - description: "Select by effective user ID (EUID) or name", - args: { name: "userlist" }, - }, - { - name: "-c", - description: "Show different scheduler information for the -l option", - }, - { - name: "--context", - description: "Display security context format (for SE Linux)", - }, - { name: "-f", description: "Do full-format listing" }, - { name: "-F", description: "Extra full format" }, - { - name: ["--format", "-o", "o"], - description: "", - args: { name: "format" }, - isRepeatable: true, - }, - { name: ["-M", "Z"], description: "(for SE Linux)" }, - { name: ["-y", "-l"], description: "" }, - { - name: "--cols", - description: "Set screen width", - args: { name: "n" }, - }, - { - name: "--columns", - description: "Set screen width", - args: { name: "n" }, - }, - { - name: "--cumulative", - description: - "Include some dead child process data (as a sum with the parent)", - }, - { name: "--forest", description: "ASCII art process tree" }, - { name: "-H", description: "Show process hierarchy (forest)" }, - { - name: "--headers", - description: "Repeat header lines, one per page of output", - }, - { - name: "-n", - description: "Set namelist file", - args: { name: "namelist" }, - }, - { - name: "--lines", - description: "Set screen height", - args: { name: "n" }, - }, - { - name: ["--no-headers", "--no-heading"], - description: "Print no header line at all", - }, - { - name: "--rows", - description: "Set screen height", - args: { name: "n" }, - }, - { - name: "--sort", - description: "Specify sorting order", - args: { name: "spec" }, - }, - { - name: "--width", - description: "Set screen width", - args: { name: "n" }, - }, - { - name: "-L", - description: "Show threads, possibly with LWP and NLWP columns", - }, - { - name: "-T", - description: "Show threads, possibly with SPID column", - }, - { name: "--help", description: "Print a help message" }, - { name: "--info", description: "Print debugging info" }, - { name: "--version", description: "Print the procps version" }, - ], + name: "ps", + description: "Report a snapshot of the current processes", + options: [ + { name: ["-A", "-e"], description: "Select all processes" }, + { + name: "-a", + description: "Select all processes except both session leaders", + args: { name: "getsid" }, + }, + { + name: "-d", + description: "Select all processes except session leaders", + }, + { + name: "--deselect", + description: + "Select all processes except those that fulfill the specified conditions", + }, + { + name: "-N", + description: + "Select all processes except those that fulfill the specified conditions (negates the selection)", + }, + { + name: "--pid", + description: "Select by process ID", + args: { name: "pidlist" }, + }, + { + name: "--ppid", + description: + "Select by parent process ID. This selects the processes with a parent process ID in pidlist", + args: { name: "pidlist" }, + }, + { + name: "--sid", + description: "Select by session ID", + args: { name: "sesslist" }, + }, + { + name: "--tty", + description: "Select by terminal", + args: { name: "ttylist" }, + }, + { + name: "U", + description: "Select by effective user ID (EUID) or name", + args: { name: "userlist" }, + }, + { + name: "-U", + description: "Select by real user ID (RUID) or name", + args: { name: "userlist" }, + }, + { + name: "-u", + description: "Select by effective user ID (EUID) or name", + args: { name: "userlist" }, + }, + { + name: "--User", + description: "Select by real user ID (RUID) or name", + args: { name: "userlist" }, + }, + { + name: "--user", + description: "Select by effective user ID (EUID) or name", + args: { name: "userlist" }, + }, + { + name: "-c", + description: "Show different scheduler information for the -l option", + }, + { + name: "--context", + description: "Display security context format (for SE Linux)", + }, + { name: "-f", description: "Do full-format listing" }, + { name: "-F", description: "Extra full format" }, + { + name: ["--format", "-o", "o"], + description: "", + args: { name: "format" }, + isRepeatable: true, + }, + { name: ["-M", "Z"], description: "(for SE Linux)" }, + { name: ["-y", "-l"], description: "" }, + { + name: "--cols", + description: "Set screen width", + args: { name: "n" }, + }, + { + name: "--columns", + description: "Set screen width", + args: { name: "n" }, + }, + { + name: "--cumulative", + description: + "Include some dead child process data (as a sum with the parent)", + }, + { name: "--forest", description: "ASCII art process tree" }, + { name: "-H", description: "Show process hierarchy (forest)" }, + { + name: "--headers", + description: "Repeat header lines, one per page of output", + }, + { + name: "-n", + description: "Set namelist file", + args: { name: "namelist" }, + }, + { + name: "--lines", + description: "Set screen height", + args: { name: "n" }, + }, + { + name: ["--no-headers", "--no-heading"], + description: "Print no header line at all", + }, + { + name: "--rows", + description: "Set screen height", + args: { name: "n" }, + }, + { + name: "--sort", + description: "Specify sorting order", + args: { name: "spec" }, + }, + { + name: "--width", + description: "Set screen width", + args: { name: "n" }, + }, + { + name: "-L", + description: "Show threads, possibly with LWP and NLWP columns", + }, + { + name: "-T", + description: "Show threads, possibly with SPID column", + }, + { name: "--help", description: "Print a help message" }, + { name: "--info", description: "Print debugging info" }, + { name: "--version", description: "Print the procps version" }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/pwd.ts b/extensions/terminal-suggest/src/completions/upstream/pwd.ts index 42126db3..11c390ad 100644 --- a/extensions/terminal-suggest/src/completions/upstream/pwd.ts +++ b/extensions/terminal-suggest/src/completions/upstream/pwd.ts @@ -1,16 +1,16 @@ const completionSpec: Fig.Spec = { - name: "pwd", - description: "Return working directory name", - options: [ - { - name: "-L", - description: "Display the logical current working directory", - }, - { - name: "-P", - description: "Display the physical current working directory", - }, - ], + name: "pwd", + description: "Return working directory name", + options: [ + { + name: "-L", + description: "Display the logical current working directory", + }, + { + name: "-P", + description: "Display the physical current working directory", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/python.ts b/extensions/terminal-suggest/src/completions/upstream/python.ts index 2374ee6b..beca86b3 100644 --- a/extensions/terminal-suggest/src/completions/upstream/python.ts +++ b/extensions/terminal-suggest/src/completions/upstream/python.ts @@ -21,6 +21,7 @@ const completionSpec: Fig.Spec = { }, args: { name: "python script", + isScript: true, generators: filepaths({ extensions: ["py"], editFileSuggestions: { priority: 76 }, diff --git a/extensions/terminal-suggest/src/completions/upstream/rm.ts b/extensions/terminal-suggest/src/completions/upstream/rm.ts index 7b52f909..9aaf1855 100644 --- a/extensions/terminal-suggest/src/completions/upstream/rm.ts +++ b/extensions/terminal-suggest/src/completions/upstream/rm.ts @@ -1,43 +1,43 @@ const completionSpec: Fig.Spec = { - name: "rm", - description: "Remove directory entries", - args: { - isVariadic: true, - template: ["folders", "filepaths"], - }, + name: "rm", + description: "Remove directory entries", + args: { + isVariadic: true, + template: ["folders", "filepaths"], + }, - options: [ - { - name: ["-r", "-R"], - description: - "Recursive. Attempt to remove the file hierarchy rooted in each file argument", - isDangerous: true, - }, - { - name: "-P", - description: "Overwrite regular files before deleting them", - isDangerous: true, - }, - { - name: "-d", - description: - "Attempt to remove directories as well as other types of files", - }, - { - name: "-f", - description: - "⚠️ Attempt to remove the files without prompting for confirmation", - isDangerous: true, - }, - { - name: "-i", - description: "Request confirmation before attempting to remove each file", - }, - { - name: "-v", - description: "Be verbose when deleting files", - }, - ], + options: [ + { + name: ["-r", "-R"], + description: + "Recursive. Attempt to remove the file hierarchy rooted in each file argument", + isDangerous: true, + }, + { + name: "-P", + description: "Overwrite regular files before deleting them", + isDangerous: true, + }, + { + name: "-d", + description: + "Attempt to remove directories as well as other types of files", + }, + { + name: "-f", + description: + "⚠️ Attempt to remove the files without prompting for confirmation", + isDangerous: true, + }, + { + name: "-i", + description: "Request confirmation before attempting to remove each file", + }, + { + name: "-v", + description: "Be verbose when deleting files", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/rmdir.ts b/extensions/terminal-suggest/src/completions/upstream/rmdir.ts index 92790e75..430b9a67 100644 --- a/extensions/terminal-suggest/src/completions/upstream/rmdir.ts +++ b/extensions/terminal-suggest/src/completions/upstream/rmdir.ts @@ -1,18 +1,18 @@ const completionSpec: Fig.Spec = { - name: "rmdir", - description: "Remove directories", - args: { - isVariadic: true, - template: "folders", - }, + name: "rmdir", + description: "Remove directories", + args: { + isVariadic: true, + template: "folders", + }, - options: [ - { - name: "-p", - description: "Remove each directory of path", - isDangerous: true, - }, - ], + options: [ + { + name: "-p", + description: "Remove each directory of path", + isDangerous: true, + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/scp.ts b/extensions/terminal-suggest/src/completions/upstream/scp.ts index 93da019b..3971e94d 100644 --- a/extensions/terminal-suggest/src/completions/upstream/scp.ts +++ b/extensions/terminal-suggest/src/completions/upstream/scp.ts @@ -1,226 +1,226 @@ import { knownHosts, configHosts } from "./ssh"; const completionSpec: Fig.Spec = { - name: "scp", - description: "Copies files or directories between hosts on a network", - args: [ - { - name: "sources", - description: "File or directory, local or remote ([user@]host:[path])", - isVariadic: true, - generators: [ - knownHosts, - configHosts, - { template: ["history", "filepaths", "folders"] }, - ], - }, - { - name: "target", - description: "File or directory, local or remote ([user@]host:[path])", - generators: [ - knownHosts, - configHosts, - { template: ["history", "filepaths", "folders"] }, - ], - }, - ], - options: [ - { - name: "-3", - description: `Copies between two remote hosts are transferred through the local + name: "scp", + description: "Copies files or directories between hosts on a network", + args: [ + { + name: "sources", + description: "File or directory, local or remote ([user@]host:[path])", + isVariadic: true, + generators: [ + knownHosts, + configHosts, + { template: ["history", "filepaths", "folders"] }, + ], + }, + { + name: "target", + description: "File or directory, local or remote ([user@]host:[path])", + generators: [ + knownHosts, + configHosts, + { template: ["history", "filepaths", "folders"] }, + ], + }, + ], + options: [ + { + name: "-3", + description: `Copies between two remote hosts are transferred through the local host. Without this option the data is copied directly between the two remote hosts. Note that this option disables the progress meter and selects batch mode for the second host, since scp cannot ask for passwords or passphrases for both hosts`, - }, - { - name: "-4", - description: "Forces scp to use IPv4 addresses only", - }, - { - name: "-6", - description: "Forces scp to use IPv6 addresses only", - }, - { - name: "-A", - description: - "Allows forwarding of ssh-agent(1) to the remote system. The default is not to forward an authentication agent", - }, - { - name: "-B", - description: - "Selects batch mode (prevents asking for passwords or passphrases)", - }, - { - name: "-C", - description: - "Compression enable. Passes the -C flag to ssh(1) to enable compression", - }, - { - name: "-c", - description: - "Selects the cipher to use for encrypting the data transfer. This option is directly passed to ssh(1)", - args: { - name: "cipher", - description: "The selected cipher specification", - }, - }, - { - name: "-F", - description: - "Specifies an alternative per-user configuration file for ssh. This option is directly passed to ssh(1)", - args: { - name: "ssh_config", - description: "The selected ssh config", - }, - }, - { - name: "-i", - description: - "Selects the file from which the identity (private key) for public key authentication is read. This option is directly passed to ssh(1)", - args: { - name: "identity_file", - description: "Specified identity file", - }, - }, - { - name: "-J", - description: `Connect to the target host by first making an scp connection to the + }, + { + name: "-4", + description: "Forces scp to use IPv4 addresses only", + }, + { + name: "-6", + description: "Forces scp to use IPv6 addresses only", + }, + { + name: "-A", + description: + "Allows forwarding of ssh-agent(1) to the remote system. The default is not to forward an authentication agent", + }, + { + name: "-B", + description: + "Selects batch mode (prevents asking for passwords or passphrases)", + }, + { + name: "-C", + description: + "Compression enable. Passes the -C flag to ssh(1) to enable compression", + }, + { + name: "-c", + description: + "Selects the cipher to use for encrypting the data transfer. This option is directly passed to ssh(1)", + args: { + name: "cipher", + description: "The selected cipher specification", + }, + }, + { + name: "-F", + description: + "Specifies an alternative per-user configuration file for ssh. This option is directly passed to ssh(1)", + args: { + name: "ssh_config", + description: "The selected ssh config", + }, + }, + { + name: "-i", + description: + "Selects the file from which the identity (private key) for public key authentication is read. This option is directly passed to ssh(1)", + args: { + name: "identity_file", + description: "Specified identity file", + }, + }, + { + name: "-J", + description: `Connect to the target host by first making an scp connection to the jump host described by destination and then establishing a TCP forwarding to the ultimate destination from there. Multiple jump hops may be specified separated by comma characters. This is a shortcut to specify a ProxyJump configuration directive. This option is directly passed to ssh(1)`, - args: { - name: "destination", - description: "Scp destination", - }, - }, - { - name: "-l", - description: "Limits the used bandwidth, specified in Kbit/s", - args: { - name: "limit", - description: "Limit bandwidth in Kbit/s", - }, - }, - { - name: "-o", - description: `Can be used to pass options to ssh in the format used in + args: { + name: "destination", + description: "Scp destination", + }, + }, + { + name: "-l", + description: "Limits the used bandwidth, specified in Kbit/s", + args: { + name: "limit", + description: "Limit bandwidth in Kbit/s", + }, + }, + { + name: "-o", + description: `Can be used to pass options to ssh in the format used in ssh_config(5). This is useful for specifying options for which there is no separate scp command-line flag. For full details of the options listed below, and their possible values, see ssh_config(5)`, - args: { - name: "option", - suggestions: [ - { name: "AddressFamily" }, - { name: "BatchMode" }, - { name: "BindAddress" }, - { name: "ChallengeResponseAuthentication" }, - { name: "CheckHostIP" }, - { name: "Cipher" }, - { name: "Ciphers" }, - { name: "ClearAllForwardings" }, - { name: "Compression" }, - { name: "CompressionLevel" }, - { name: "ConnectionAttempts" }, - { name: "ConnectTimeout" }, - { name: "ControlMaster" }, - { name: "ControlPath" }, - { name: "ControlPersist" }, - { name: "DynamicForward" }, - { name: "EscapeChar" }, - { name: "ExitOnForwardFailure" }, - { name: "ForwardAgent" }, - { name: "ForwardX11" }, - { name: "ForwardX11Timeout" }, - { name: "ForwardX11Trusted" }, - { name: "GatewayPorts" }, - { name: "GlobalKnownHostsFile" }, - { name: "GSSAPIAuthentication" }, - { name: "GSSAPIDelegateCredentials" }, - { name: "HashKnownHosts" }, - { name: "Host" }, - { name: "HostbasedAuthentication" }, - { name: "HostKeyAlgorithms" }, - { name: "HostKeyAlias" }, - { name: "HostName" }, - { name: "IdentityFile" }, - { name: "IdentitiesOnly" }, - { name: "IPQoS" }, - { name: "KbdInteractiveAuthentication" }, - { name: "KbdInteractiveDevices" }, - { name: "KexAlgorithms" }, - { name: "LocalCommand" }, - { name: "LocalForward" }, - { name: "LogLevel" }, - { name: "MACs" }, - { name: "NoHostAuthenticationForLocalhost" }, - { name: "NumberOfPasswordPrompts" }, - { name: "PasswordAuthentication" }, - { name: "PermitLocalCommand" }, - { name: "PKCS11Provider" }, - { name: "Port" }, - { name: "PreferredAuthentications" }, - { name: "Protocol" }, - { name: "ProxyCommand" }, - { name: "PubkeyAuthentication" }, - { name: "RekeyLimit" }, - { name: "RequestTTY" }, - { name: "RhostsRSAAuthentication" }, - { name: "RSAAuthentication" }, - { name: "SendEnv" }, - { name: "ServerAliveInterval" }, - { name: "ServerAliveCountMax" }, - { name: "StrictHostKeyChecking" }, - { name: "TCPKeepAlive" }, - { name: "Tunnel" }, - { name: "TunnelDevice" }, - { name: "UsePrivilegedPort" }, - { name: "User" }, - { name: "UserKnownHostsFile" }, - { name: "VerifyHostKeyDNS" }, - { name: "VisualHostKey" }, - { name: "XAuthLocation" }, - ], - }, - }, - { - name: "-P", - description: `Specifies the port to connect to on the remote host. Note that + args: { + name: "option", + suggestions: [ + { name: "AddressFamily" }, + { name: "BatchMode" }, + { name: "BindAddress" }, + { name: "ChallengeResponseAuthentication" }, + { name: "CheckHostIP" }, + { name: "Cipher" }, + { name: "Ciphers" }, + { name: "ClearAllForwardings" }, + { name: "Compression" }, + { name: "CompressionLevel" }, + { name: "ConnectionAttempts" }, + { name: "ConnectTimeout" }, + { name: "ControlMaster" }, + { name: "ControlPath" }, + { name: "ControlPersist" }, + { name: "DynamicForward" }, + { name: "EscapeChar" }, + { name: "ExitOnForwardFailure" }, + { name: "ForwardAgent" }, + { name: "ForwardX11" }, + { name: "ForwardX11Timeout" }, + { name: "ForwardX11Trusted" }, + { name: "GatewayPorts" }, + { name: "GlobalKnownHostsFile" }, + { name: "GSSAPIAuthentication" }, + { name: "GSSAPIDelegateCredentials" }, + { name: "HashKnownHosts" }, + { name: "Host" }, + { name: "HostbasedAuthentication" }, + { name: "HostKeyAlgorithms" }, + { name: "HostKeyAlias" }, + { name: "HostName" }, + { name: "IdentityFile" }, + { name: "IdentitiesOnly" }, + { name: "IPQoS" }, + { name: "KbdInteractiveAuthentication" }, + { name: "KbdInteractiveDevices" }, + { name: "KexAlgorithms" }, + { name: "LocalCommand" }, + { name: "LocalForward" }, + { name: "LogLevel" }, + { name: "MACs" }, + { name: "NoHostAuthenticationForLocalhost" }, + { name: "NumberOfPasswordPrompts" }, + { name: "PasswordAuthentication" }, + { name: "PermitLocalCommand" }, + { name: "PKCS11Provider" }, + { name: "Port" }, + { name: "PreferredAuthentications" }, + { name: "Protocol" }, + { name: "ProxyCommand" }, + { name: "PubkeyAuthentication" }, + { name: "RekeyLimit" }, + { name: "RequestTTY" }, + { name: "RhostsRSAAuthentication" }, + { name: "RSAAuthentication" }, + { name: "SendEnv" }, + { name: "ServerAliveInterval" }, + { name: "ServerAliveCountMax" }, + { name: "StrictHostKeyChecking" }, + { name: "TCPKeepAlive" }, + { name: "Tunnel" }, + { name: "TunnelDevice" }, + { name: "UsePrivilegedPort" }, + { name: "User" }, + { name: "UserKnownHostsFile" }, + { name: "VerifyHostKeyDNS" }, + { name: "VisualHostKey" }, + { name: "XAuthLocation" }, + ], + }, + }, + { + name: "-P", + description: `Specifies the port to connect to on the remote host. Note that this option is written with a capital ‘P’, because -p is already reserved for preserving the times and modes of the file`, - args: { - name: "port", - }, - }, - { - name: "-p", - description: - "Preserves modification times, access times, and modes from the original file", - }, - { - name: "-q", - description: - "Quiet mode: disables the progress meter as well as warning and diagnostic messages from ssh(1)", - }, - { - name: "-r", - description: - "Recursively copy entire directories. Note that scp follows symbolic links encountered in the tree traversal", - }, - { - name: "-S", - description: - "Name of program to use for the encrypted connection. The program must understand ssh(1) options", - args: { - name: "program", - }, - }, - { - name: "-T", - description: `Disable strict filename checking. By default when copying files + args: { + name: "port", + }, + }, + { + name: "-p", + description: + "Preserves modification times, access times, and modes from the original file", + }, + { + name: "-q", + description: + "Quiet mode: disables the progress meter as well as warning and diagnostic messages from ssh(1)", + }, + { + name: "-r", + description: + "Recursively copy entire directories. Note that scp follows symbolic links encountered in the tree traversal", + }, + { + name: "-S", + description: + "Name of program to use for the encrypted connection. The program must understand ssh(1) options", + args: { + name: "program", + }, + }, + { + name: "-T", + description: `Disable strict filename checking. By default when copying files from a remote host to a local directory scp checks that the received filenames match those requested on the command-line to prevent the remote end from sending unexpected or unwanted files. @@ -229,13 +229,13 @@ interpret filename wildcards, these checks may cause wanted files to be rejected. This option disables these checks at the expense of fully trusting that the server will not send unexpected filenames`, - }, - { - name: "-v", - description: - "Verbose mode. Causes scp and ssh(1) to print debugging messages about their progress. This is helpful in debugging connection, authentication, and configuration problems", - }, - ], + }, + { + name: "-v", + description: + "Verbose mode. Causes scp and ssh(1) to print debugging messages about their progress. This is helpful in debugging connection, authentication, and configuration problems", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/ssh.ts b/extensions/terminal-suggest/src/completions/upstream/ssh.ts index 9b0f5bd4..4998530d 100644 --- a/extensions/terminal-suggest/src/completions/upstream/ssh.ts +++ b/extensions/terminal-suggest/src/completions/upstream/ssh.ts @@ -43,7 +43,7 @@ const getConfigLines = async ( .map((line) => line.split(" ")[1]); // Get the lines of every include file - const includeLines: any = await Promise.all( + const includeLines: string[][] = await Promise.all( includes.map((file) => getConfigLines(file, executeShellCommand, home, basePath) ) @@ -89,10 +89,10 @@ export const configHosts: Fig.Generator = { return configLines .filter( - (line: any) => + (line) => line.trim().toLowerCase().startsWith("host ") && !line.includes("*") ) - .map((host: any) => ({ + .map((host) => ({ name: host.split(" ")[1], description: "SSH host", priority: 90, diff --git a/extensions/terminal-suggest/src/completions/upstream/tail.ts b/extensions/terminal-suggest/src/completions/upstream/tail.ts index 657c610b..fd905588 100644 --- a/extensions/terminal-suggest/src/completions/upstream/tail.ts +++ b/extensions/terminal-suggest/src/completions/upstream/tail.ts @@ -1,20 +1,20 @@ const completionSpec: Fig.Spec = { - name: "tail", - description: "Display the last part of a file", - args: { - isVariadic: true, - template: "filepaths", - }, - options: [ - { - name: "-f", - description: "Wait for additional data to be appended", - }, - { - name: "-r", - description: "Display in reverse order", - }, - ], + name: "tail", + description: "Display the last part of a file", + args: { + isVariadic: true, + template: "filepaths", + }, + options: [ + { + name: "-f", + description: "Wait for additional data to be appended", + }, + { + name: "-r", + description: "Display in reverse order", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/top.ts b/extensions/terminal-suggest/src/completions/upstream/top.ts index df922164..5831fd7b 100644 --- a/extensions/terminal-suggest/src/completions/upstream/top.ts +++ b/extensions/terminal-suggest/src/completions/upstream/top.ts @@ -1,49 +1,49 @@ const completionSpec: Fig.Spec = { - name: "top", - description: "Display Linux tasks", - options: [ - { - name: ["-h", "-v"], - description: "Show library version and usage prompt", - }, - { - name: "-b", - description: "Starts top in Batch mode", - args: { - name: "operation", - }, - }, - { - name: "-c", - description: "Starts top with last remembered c state reversed", - args: { - name: "toggle", - }, - }, - { - name: "-i", - description: - "Starts top with the last remembered 'i' state reversed. When this toggle is Off, tasks that are idled or zombied will not be displayed", - args: { - name: "toggle", - }, - }, - { - name: "-s", - description: "Starts top with secure mode forced", - args: { - name: "delay", - }, - }, - { - name: "-pid", - description: "Monitor pids", - args: { - name: "process ids", - isVariadic: true, - }, - }, - ], + name: "top", + description: "Display Linux tasks", + options: [ + { + name: ["-h", "-v"], + description: "Show library version and usage prompt", + }, + { + name: "-b", + description: "Starts top in Batch mode", + args: { + name: "operation", + }, + }, + { + name: "-c", + description: "Starts top with last remembered c state reversed", + args: { + name: "toggle", + }, + }, + { + name: "-i", + description: + "Starts top with the last remembered 'i' state reversed. When this toggle is Off, tasks that are idled or zombied will not be displayed", + args: { + name: "toggle", + }, + }, + { + name: "-s", + description: "Starts top with secure mode forced", + args: { + name: "delay", + }, + }, + { + name: "-pid", + description: "Monitor pids", + args: { + name: "process ids", + isVariadic: true, + }, + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/touch.ts b/extensions/terminal-suggest/src/completions/upstream/touch.ts index 45208878..6987272a 100644 --- a/extensions/terminal-suggest/src/completions/upstream/touch.ts +++ b/extensions/terminal-suggest/src/completions/upstream/touch.ts @@ -1,59 +1,59 @@ const completionSpec: Fig.Spec = { - name: "touch", - description: "Change file access and modification times", - args: { - name: "file", - isVariadic: true, - template: "folders", - suggestCurrentToken: true, - }, - options: [ - { - name: "-A", - description: - "Adjust the access and modification time stamps for the file by the specified value", - args: { - name: "time", - description: "[-][[hh]mm]SS", - }, - }, - { name: "-a", description: "Change the access time of the file" }, - { - name: "-c", - description: "Do not create the file if it does not exist", - }, - { - name: "-f", - description: - "Attempt to force the update, even if the file permissions do not currently permit it", - }, - { - name: "-h", - description: - "If the file is a symbolic link, change the times of the link itself rather than the file that the link points to", - }, - { - name: "-m", - description: "Change the modification time of the file", - }, - { - name: "-r", - description: - "Use the access and modifications times from the specified file instead of the current time of day", - args: { - name: "file", - }, - }, - { - name: "-t", - description: - "Change the access and modification times to the specified time instead of the current time of day", - args: { - name: "timestamp", - description: "[[CC]YY]MMDDhhmm[.SS]", - }, - }, - ], + name: "touch", + description: "Change file access and modification times", + args: { + name: "file", + isVariadic: true, + template: "folders", + suggestCurrentToken: true, + }, + options: [ + { + name: "-A", + description: + "Adjust the access and modification time stamps for the file by the specified value", + args: { + name: "time", + description: "[-][[hh]mm]SS", + }, + }, + { name: "-a", description: "Change the access time of the file" }, + { + name: "-c", + description: "Do not create the file if it does not exist", + }, + { + name: "-f", + description: + "Attempt to force the update, even if the file permissions do not currently permit it", + }, + { + name: "-h", + description: + "If the file is a symbolic link, change the times of the link itself rather than the file that the link points to", + }, + { + name: "-m", + description: "Change the modification time of the file", + }, + { + name: "-r", + description: + "Use the access and modifications times from the specified file instead of the current time of day", + args: { + name: "file", + }, + }, + { + name: "-t", + description: + "Change the access and modification times to the specified time instead of the current time of day", + args: { + name: "timestamp", + description: "[[CC]YY]MMDDhhmm[.SS]", + }, + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/uname.ts b/extensions/terminal-suggest/src/completions/upstream/uname.ts index e73b9efe..77538770 100644 --- a/extensions/terminal-suggest/src/completions/upstream/uname.ts +++ b/extensions/terminal-suggest/src/completions/upstream/uname.ts @@ -1,36 +1,36 @@ const completionSpec: Fig.Spec = { - name: "uname", - description: "Print operating system name", - options: [ - { - name: "-a", - description: "Print all available system information", - }, - { - name: "-m", - description: "Print the machine hardware name", - }, - { - name: "-n", - description: "Print the system hostname", - }, - { - name: "-p", - description: "Print the machine processor architecture name", - }, - { - name: "-r", - description: "Print the operating system release", - }, - { - name: "-s", - description: "Print the operating system name", - }, - { - name: "-v", - description: "Print the operating system version", - }, - ], + name: "uname", + description: "Print operating system name", + options: [ + { + name: "-a", + description: "Print all available system information", + }, + { + name: "-m", + description: "Print the machine hardware name", + }, + { + name: "-n", + description: "Print the system hostname", + }, + { + name: "-p", + description: "Print the machine processor architecture name", + }, + { + name: "-r", + description: "Print the operating system release", + }, + { + name: "-s", + description: "Print the operating system name", + }, + { + name: "-v", + description: "Print the operating system version", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/vim.ts b/extensions/terminal-suggest/src/completions/upstream/vim.ts index 7e41109f..267edb38 100644 --- a/extensions/terminal-suggest/src/completions/upstream/vim.ts +++ b/extensions/terminal-suggest/src/completions/upstream/vim.ts @@ -1,244 +1,244 @@ const completionSpec: Fig.Spec = { - name: "vim", - description: "Vi IMproved, a programmer's text editor", - args: { - template: "filepaths", - // suggestCurrentToken: true, - }, - options: [ - { - name: "-v", - description: "Vi mode (like 'vi')", - }, - { - name: "-e", - description: "Ex mode (like 'ex')", - }, - { - name: "-E", - description: "Improved Ex mode", - }, - { - name: "-s", - description: - "Enable silent mode (when in ex mode), or Read Normal mode commands from file", - args: { - name: "scriptin", - template: "filepaths", - isOptional: true, - }, - }, - { - name: "-d", - description: "Diff mode (like 'vimdiff')", - }, - { - name: "-y", - description: "Easy mode (like 'evim', modeless)", - }, - { - name: "-R", - description: "Readonly mode (like 'view')", - }, - { - name: "-Z", - description: "Restricted mode (like 'rvim')", - }, - { - name: "-m", - description: "Modifications (writing files) not allowed", - }, - { - name: "-M", - description: "Modifications in text not allowed", - }, - { - name: "-b", - description: "Binary mode", - }, - { - name: "-l", - description: "Lisp mode", - }, - { - name: "-C", - description: "Compatible with Vi: 'compatible'", - }, - { - name: "-N", - description: "Not fully Vi compatible: 'nocompatible'", - }, - { - name: "-V", - description: "Be verbose [level N] [log messages to fname]", - args: [ - { - name: "N", - }, - { - name: "fname", - template: "filepaths", - }, - ], - }, - { - name: "-D", - description: "Debugging mode", - }, - { - name: "-n", - description: "No swap file, use memory only", - }, - { - name: "-r", - description: - "Recover crashed session if filename is specified, otherwise list swap files and exit", - args: { - name: "filename", - isOptional: true, - template: "filepaths", - }, - }, - { - name: "-L", - description: "Same as -r", - args: { - name: "filename", - template: "filepaths", - }, - }, - { - name: "-T", - description: "Set terminal type to ", - args: { - name: "terminal", - }, - }, - { - name: "--not-a-term", - description: "Skip warning for input/output not being a terminal", - }, - { - name: "--ttyfail", - description: "Exit if input or output is not a terminal", - }, - { - name: "-u", - description: "Use instead of any .vimrc", - args: { - name: "vimrc", - template: "filepaths", - }, - }, - { - name: "--noplugin", - description: "Don't load plugin scripts", - }, - { - name: "-p", - description: "Open N tab pages (default: one for each file)", - args: { - name: "N", - isOptional: true, - }, - }, - { - name: "-o", - description: "Open N windows (default: one for each file)", - args: { - name: "N", - isOptional: true, - }, - }, - { - name: "-O", - description: "Like -o but split vertically", - args: { - name: "N", - isOptional: true, - }, - }, - { - name: "+", - description: - "Start at end of file, if line number is specified, start at that line", - args: { - name: "lnum", - isOptional: true, - }, - }, - { - name: "--cmd", - description: "Execute before loading any vimrc file", - args: { - name: "command", - isCommand: true, - }, - }, - { - name: "-c", - description: "Execute after loading the first file", - args: { - name: "command", - }, - }, - { - name: "-S", - description: "Source file after loading the first file", - args: { - name: "session", - template: "filepaths", - }, - }, - { - name: "-w", - description: "Append all typed commands to file ", - args: { - name: "scriptout", - template: "filepaths", - }, - }, - { - name: "-W", - description: "Write all typed commands to file ", - args: { - name: "scriptout", - template: "filepaths", - }, - }, - { - name: "-x", - description: "Edit encrypted files", - }, - { - name: "--startuptime", - description: "Write startup timing messages to ", - args: { - name: "file", - template: "filepaths", - }, - }, - { - name: "-i", - description: "Use instead of .viminfo", - args: { - name: "viminfo", - template: "filepaths", - }, - }, - { - name: "--clean", - description: "'nocompatible', Vim defaults, no plugins, no viminfo", - }, - { - name: ["-h", "--help"], - description: "Print Help message and exit", - }, - { - name: "--version", - description: "Print version information and exit", - }, - ], + name: "vim", + description: "Vi IMproved, a programmer's text editor", + args: { + template: "filepaths", + // suggestCurrentToken: true, + }, + options: [ + { + name: "-v", + description: "Vi mode (like 'vi')", + }, + { + name: "-e", + description: "Ex mode (like 'ex')", + }, + { + name: "-E", + description: "Improved Ex mode", + }, + { + name: "-s", + description: + "Enable silent mode (when in ex mode), or Read Normal mode commands from file", + args: { + name: "scriptin", + template: "filepaths", + isOptional: true, + }, + }, + { + name: "-d", + description: "Diff mode (like 'vimdiff')", + }, + { + name: "-y", + description: "Easy mode (like 'evim', modeless)", + }, + { + name: "-R", + description: "Readonly mode (like 'view')", + }, + { + name: "-Z", + description: "Restricted mode (like 'rvim')", + }, + { + name: "-m", + description: "Modifications (writing files) not allowed", + }, + { + name: "-M", + description: "Modifications in text not allowed", + }, + { + name: "-b", + description: "Binary mode", + }, + { + name: "-l", + description: "Lisp mode", + }, + { + name: "-C", + description: "Compatible with Vi: 'compatible'", + }, + { + name: "-N", + description: "Not fully Vi compatible: 'nocompatible'", + }, + { + name: "-V", + description: "Be verbose [level N] [log messages to fname]", + args: [ + { + name: "N", + }, + { + name: "fname", + template: "filepaths", + }, + ], + }, + { + name: "-D", + description: "Debugging mode", + }, + { + name: "-n", + description: "No swap file, use memory only", + }, + { + name: "-r", + description: + "Recover crashed session if filename is specified, otherwise list swap files and exit", + args: { + name: "filename", + isOptional: true, + template: "filepaths", + }, + }, + { + name: "-L", + description: "Same as -r", + args: { + name: "filename", + template: "filepaths", + }, + }, + { + name: "-T", + description: "Set terminal type to ", + args: { + name: "terminal", + }, + }, + { + name: "--not-a-term", + description: "Skip warning for input/output not being a terminal", + }, + { + name: "--ttyfail", + description: "Exit if input or output is not a terminal", + }, + { + name: "-u", + description: "Use instead of any .vimrc", + args: { + name: "vimrc", + template: "filepaths", + }, + }, + { + name: "--noplugin", + description: "Don't load plugin scripts", + }, + { + name: "-p", + description: "Open N tab pages (default: one for each file)", + args: { + name: "N", + isOptional: true, + }, + }, + { + name: "-o", + description: "Open N windows (default: one for each file)", + args: { + name: "N", + isOptional: true, + }, + }, + { + name: "-O", + description: "Like -o but split vertically", + args: { + name: "N", + isOptional: true, + }, + }, + { + name: "+", + description: + "Start at end of file, if line number is specified, start at that line", + args: { + name: "lnum", + isOptional: true, + }, + }, + { + name: "--cmd", + description: "Execute before loading any vimrc file", + args: { + name: "command", + isCommand: true, + }, + }, + { + name: "-c", + description: "Execute after loading the first file", + args: { + name: "command", + }, + }, + { + name: "-S", + description: "Source file after loading the first file", + args: { + name: "session", + template: "filepaths", + }, + }, + { + name: "-w", + description: "Append all typed commands to file ", + args: { + name: "scriptout", + template: "filepaths", + }, + }, + { + name: "-W", + description: "Write all typed commands to file ", + args: { + name: "scriptout", + template: "filepaths", + }, + }, + { + name: "-x", + description: "Edit encrypted files", + }, + { + name: "--startuptime", + description: "Write startup timing messages to ", + args: { + name: "file", + template: "filepaths", + }, + }, + { + name: "-i", + description: "Use instead of .viminfo", + args: { + name: "viminfo", + template: "filepaths", + }, + }, + { + name: "--clean", + description: "'nocompatible', Vim defaults, no plugins, no viminfo", + }, + { + name: ["-h", "--help"], + description: "Print Help message and exit", + }, + { + name: "--version", + description: "Print version information and exit", + }, + ], }; export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/wget.ts b/extensions/terminal-suggest/src/completions/upstream/wget.ts index fd8442d9..ff14b34f 100644 --- a/extensions/terminal-suggest/src/completions/upstream/wget.ts +++ b/extensions/terminal-suggest/src/completions/upstream/wget.ts @@ -1,399 +1,399 @@ const completionSpec: Fig.Spec = { - name: "wget", - description: "A non-interactive network retriever", - args: { - isVariadic: true, - name: "url", - description: "The url(s) to retrieve", - }, - options: [ - { - name: ["-V", "--version"], - description: "Display the version of Wget and exit", - }, - { name: ["-h", "--help"], description: "Print this help" }, - { - name: ["-b", "--background"], - description: "Go to background after startup", - }, - { - name: ["-e", "--execute=COMMAND"], - description: "Execute a `.wgetrc'-style command", - }, - { name: ["-o", "--output-file=FILE"], description: "Log messages to FILE" }, - { - name: ["-a", "--append-output=FILE"], - description: "Append messages to FILE", - }, - { name: ["-q", "--quiet"], description: "Quiet (no output)" }, - { - name: ["-v", "--verbose"], - description: "Be verbose (this is the default)", - }, - { - name: ["-nv", "--no-verbose"], - description: "Turn off verboseness, without being quiet", - }, - { - name: "--report-speed=TYPE", - description: "Output bandwidth as TYPE. TYPE can be bits", - }, - { - name: ["-i", "--input-file=FILE"], - description: "Download URLs found in local or external FILE", - }, - { name: ["-F", "--force-html"], description: "Treat input file as HTML" }, - { - name: ["-B", "--base=URL"], - description: "Resolves HTML input-file links (-i -F) relative to URL", - }, - { name: "--config=FILE", description: "Specify config file to use" }, - { name: "--no-config", description: "Do not read any config file" }, - { - name: "--rejected-log=FILE", - description: "Log reasons for URL rejection to FILE", - }, - { - name: ["-t", "--tries=NUMBER"], - description: "Set number of retries to NUMBER (0 unlimits)", - }, - { - name: "--retry-connrefused", - description: "Retry even if connection is refused", - }, - { - name: "--retry-on-http-error", - description: "Comma-separated list of HTTP errors to retry", - }, - { - name: ["-O", "--output-document=FILE"], - description: "Write documents to FILE", - }, - { - name: ["-nc", "--no-clobber"], - description: - "Skip downloads that would download to existing files (overwriting them)", - }, - { - name: "--no-netrc", - description: "Don't try to obtain credentials from .netrc", - }, - { - name: ["-c", "--continue"], - description: "Resume getting a partially-downloaded file", - }, - { - name: "--start-pos=OFFSET", - description: "Start downloading from zero-based position OFFSET", - }, - { name: "--progress=TYPE", description: "Select progress gauge type" }, - { - name: "--show-progress", - description: "Display the progress bar in any verbosity mode", - }, - { - name: ["-N", "--timestamping"], - description: "Don't re-retrieve files unless newer than local", - }, - { name: ["-S", "--server-response"], description: "Print server response" }, - { name: "--spider", description: "Don't download anything" }, - { - name: ["-T", "--timeout=SECONDS"], - description: "Set all timeout values to SECONDS", - }, - { - name: "--dns-timeout=SECS", - description: "Set the DNS lookup timeout to SECS", - }, - { - name: "--connect-timeout=SECS", - description: "Set the connect timeout to SECS", - }, - { - name: "--read-timeout=SECS", - description: "Set the read timeout to SECS", - }, - { - name: ["-w", "--wait=SECONDS"], - description: "Wait SECONDS between retrievals", - }, - { - name: "--waitretry=SECONDS", - description: "Wait 1..SECONDS between retries of a retrieval", - }, - { - name: "--random-wait", - description: "Wait from 0.5*WAIT...1.5*WAIT secs between retrievals", - }, - { name: "--no-proxy", description: "Explicitly turn off proxy" }, - { - name: ["-Q", "--quota=NUMBER"], - description: "Set retrieval quota to NUMBER", - }, - { - name: "--bind-address=ADDRESS", - description: "Bind to ADDRESS (hostname or IP) on local host", - }, - { name: "--limit-rate=RATE", description: "Limit download rate to RATE" }, - { name: "--no-dns-cache", description: "Disable caching DNS lookups" }, - { - name: "--restrict-file-names=OS", - description: "Restrict chars in file names to ones OS allows", - }, - { - name: "--ignore-case", - description: "Ignore case when matching files/directories", - }, - { - name: ["-4", "--inet4-only"], - description: "Connect only to IPv4 addresses", - }, - { - name: ["-6", "--inet6-only"], - description: "Connect only to IPv6 addresses", - }, - { - name: "--user=USER", - description: "Set both ftp and http user to USER", - }, - { - name: "--password=PASS", - description: "Set both ftp and http password to PASS", - }, - { name: "--ask-password", description: "Prompt for passwords" }, - { name: "--no-iri", description: "Turn off IRI support" }, - { - name: "--local-encoding=ENC", - description: "Use ENC as the local encoding for IRIs", - }, - { - name: "--remote-encoding=ENC", - description: "Use ENC as the default remote encoding", - }, - { name: "--unlink", description: "Remove file before clobber" }, - { - name: "--xattr", - description: "Turn on storage of metadata in extended file attributes", - }, - { - name: ["-nd", "--no-directories"], - description: "Don't create directories", - }, - { - name: ["-x", "--force-directories"], - description: "Force creation of directories", - }, - { - name: ["-nH", "--no-host-directories"], - description: "Don't create host directories", - }, - { - name: "--protocol-directories", - description: "Use protocol name in directories", - }, - { - name: ["-P", "--directory-prefix=PREFIX"], - description: "Save files to PREFIX/", - }, - { - name: "--cut-dirs=NUMBER", - description: "Ignore NUMBER remote directory components", - }, - { name: "--http-user=USER", description: "Set http user to USER" }, - { - name: "--http-password=PASS", - description: "Set http password to PASS", - }, - { name: "--no-cache", description: "Disallow server-cached data" }, - { - name: ["-E", "--adjust-extension"], - description: "Save HTML/CSS documents with proper extensions", - }, - { - name: "--ignore-length", - description: "Ignore 'Content-Length' header field", - }, - { - name: "--header=STRING", - description: "Insert STRING among the headers", - }, - { - name: "--compression=TYPE", - description: - "Choose compression, one of auto, gzip and none. (default: none)", - }, - { - name: "--max-redirect", - description: "Maximum redirections allowed per page", - }, - { name: "--proxy-user=USER", description: "Set USER as proxy username" }, - { - name: "--proxy-password=PASS", - description: "Set PASS as proxy password", - }, - { - name: "--referer=URL", - description: "Include 'Referer: URL' header in HTTP request", - }, - { name: "--save-headers", description: "Save the HTTP headers to file" }, - { - name: ["-U", "--user-agent=AGENT"], - description: "Identify as AGENT instead of Wget/VERSION", - }, - { - name: "--no-http-keep-alive", - description: "Disable HTTP keep-alive (persistent connections)", - }, - { name: "--no-cookies", description: "Don't use cookies" }, - { - name: "--load-cookies=FILE", - description: "Load cookies from FILE before session", - }, - { - name: "--save-cookies=FILE", - description: "Save cookies to FILE after session", - }, - { - name: "--keep-session-cookies", - description: "Load and save session (non-permanent) cookies", - }, - { - name: "--post-data=STRING", - description: "Use the POST method; send STRING as the data", - }, - { - name: "--post-file=FILE", - description: "Use the POST method; send contents of FILE", - }, - { - name: "--method=HTTPMethod", - description: 'Use method "HTTPMethod" in the request', - }, - { - name: "--body-data=STRING", - description: "Send STRING as data. --method MUST be set", - }, - { - name: "--body-file=FILE", - description: "Send contents of FILE. --method MUST be set", - }, - { - name: "--content-on-error", - description: "Output the received content on server errors", - }, - { - name: "--secure-protocol=PR", - description: "Choose secure protocol, one of auto, SSLv2,", - }, - { name: "--https-only", description: "Only follow secure HTTPS links" }, - { - name: "--no-check-certificate", - description: "Don't validate the server's certificate", - }, - { name: "--certificate=FILE", description: "Client certificate file" }, - { - name: "--certificate-type=TYPE", - description: "Client certificate type, PEM or DER", - }, - { name: "--private-key=FILE", description: "Private key file" }, - { - name: "--private-key-type=TYPE", - description: "Private key type, PEM or DER", - }, - { - name: "--ca-certificate=FILE", - description: "File with the bundle of CAs", - }, - { - name: "--ca-directory=DIR", - description: "Directory where hash list of CAs is stored", - }, - { name: "--crl-file=FILE", description: "File with bundle of CRLs" }, - { - name: "--ciphers=STR", - description: - "Set the priority string (GnuTLS) or cipher list string (OpenSSL) directly", - }, - { name: ["-r", "--recursive"], description: "Specify recursive download" }, - { - name: ["-l", "--level=NUMBER"], - description: "Maximum recursion depth (inf or 0 for infinite)", - }, - { - name: "--delete-after", - description: "Delete files locally after downloading them", - }, - { - name: ["-k", "--convert-links"], - description: "Make links in downloaded HTML or CSS point to local files", - }, - { - name: ["-K", "--backup-converted"], - description: "Before converting file X, back up as X.orig", - }, - { - name: ["-m", "--mirror"], - description: "Shortcut for -N -r -l inf --no-remove-listing", - }, - { - name: ["-p", "--page-requisites"], - description: "Get all images, etc. needed to display HTML page", - }, - { - name: ["-A", "--accept=LIST"], - description: "Comma-separated list of accepted extensions", - }, - { - name: ["-R", "--reject=LIST"], - description: "Comma-separated list of rejected extensions", - }, - { - name: "--accept-regex=REGEX", - description: "Regex matching accepted URLs", - }, - { - name: "--reject-regex=REGEX", - description: "Regex matching rejected URLs", - }, - { name: "--regex-type=TYPE", description: "Regex type (posix)" }, - { - name: ["-D", "--domains=LIST"], - description: "Comma-separated list of accepted domains", - }, - { - name: "--exclude-domains=LIST", - description: "Comma-separated list of rejected domains", - }, - { - name: "--follow-ftp", - description: "Follow FTP links from HTML documents", - }, - { - name: "--follow-tags=LIST", - description: "Comma-separated list of followed HTML tags", - }, - { - name: "--ignore-tags=LIST", - description: "Comma-separated list of ignored HTML tags", - }, - { - name: ["-H", "--span-hosts"], - description: "Go to foreign hosts when recursive", - }, - { name: ["-L", "--relative"], description: "Follow relative links only" }, - { - name: ["-I", "--include-directories=LIST"], - description: "List of allowed directories", - }, - { - name: ["-X", "--exclude-directories=LIST"], - description: "List of excluded directories", - }, - { - name: ["-np", "--no-parent"], - description: "Don't ascend to the parent directory", - }, - ], + name: "wget", + description: "A non-interactive network retriever", + args: { + isVariadic: true, + name: "url", + description: "The url(s) to retrieve", + }, + options: [ + { + name: ["-V", "--version"], + description: "Display the version of Wget and exit", + }, + { name: ["-h", "--help"], description: "Print this help" }, + { + name: ["-b", "--background"], + description: "Go to background after startup", + }, + { + name: ["-e", "--execute=COMMAND"], + description: "Execute a `.wgetrc'-style command", + }, + { name: ["-o", "--output-file=FILE"], description: "Log messages to FILE" }, + { + name: ["-a", "--append-output=FILE"], + description: "Append messages to FILE", + }, + { name: ["-q", "--quiet"], description: "Quiet (no output)" }, + { + name: ["-v", "--verbose"], + description: "Be verbose (this is the default)", + }, + { + name: ["-nv", "--no-verbose"], + description: "Turn off verboseness, without being quiet", + }, + { + name: "--report-speed=TYPE", + description: "Output bandwidth as TYPE. TYPE can be bits", + }, + { + name: ["-i", "--input-file=FILE"], + description: "Download URLs found in local or external FILE", + }, + { name: ["-F", "--force-html"], description: "Treat input file as HTML" }, + { + name: ["-B", "--base=URL"], + description: "Resolves HTML input-file links (-i -F) relative to URL", + }, + { name: "--config=FILE", description: "Specify config file to use" }, + { name: "--no-config", description: "Do not read any config file" }, + { + name: "--rejected-log=FILE", + description: "Log reasons for URL rejection to FILE", + }, + { + name: ["-t", "--tries=NUMBER"], + description: "Set number of retries to NUMBER (0 unlimits)", + }, + { + name: "--retry-connrefused", + description: "Retry even if connection is refused", + }, + { + name: "--retry-on-http-error", + description: "Comma-separated list of HTTP errors to retry", + }, + { + name: ["-O", "--output-document=FILE"], + description: "Write documents to FILE", + }, + { + name: ["-nc", "--no-clobber"], + description: + "Skip downloads that would download to existing files (overwriting them)", + }, + { + name: "--no-netrc", + description: "Don't try to obtain credentials from .netrc", + }, + { + name: ["-c", "--continue"], + description: "Resume getting a partially-downloaded file", + }, + { + name: "--start-pos=OFFSET", + description: "Start downloading from zero-based position OFFSET", + }, + { name: "--progress=TYPE", description: "Select progress gauge type" }, + { + name: "--show-progress", + description: "Display the progress bar in any verbosity mode", + }, + { + name: ["-N", "--timestamping"], + description: "Don't re-retrieve files unless newer than local", + }, + { name: ["-S", "--server-response"], description: "Print server response" }, + { name: "--spider", description: "Don't download anything" }, + { + name: ["-T", "--timeout=SECONDS"], + description: "Set all timeout values to SECONDS", + }, + { + name: "--dns-timeout=SECS", + description: "Set the DNS lookup timeout to SECS", + }, + { + name: "--connect-timeout=SECS", + description: "Set the connect timeout to SECS", + }, + { + name: "--read-timeout=SECS", + description: "Set the read timeout to SECS", + }, + { + name: ["-w", "--wait=SECONDS"], + description: "Wait SECONDS between retrievals", + }, + { + name: "--waitretry=SECONDS", + description: "Wait 1..SECONDS between retries of a retrieval", + }, + { + name: "--random-wait", + description: "Wait from 0.5*WAIT...1.5*WAIT secs between retrievals", + }, + { name: "--no-proxy", description: "Explicitly turn off proxy" }, + { + name: ["-Q", "--quota=NUMBER"], + description: "Set retrieval quota to NUMBER", + }, + { + name: "--bind-address=ADDRESS", + description: "Bind to ADDRESS (hostname or IP) on local host", + }, + { name: "--limit-rate=RATE", description: "Limit download rate to RATE" }, + { name: "--no-dns-cache", description: "Disable caching DNS lookups" }, + { + name: "--restrict-file-names=OS", + description: "Restrict chars in file names to ones OS allows", + }, + { + name: "--ignore-case", + description: "Ignore case when matching files/directories", + }, + { + name: ["-4", "--inet4-only"], + description: "Connect only to IPv4 addresses", + }, + { + name: ["-6", "--inet6-only"], + description: "Connect only to IPv6 addresses", + }, + { + name: "--user=USER", + description: "Set both ftp and http user to USER", + }, + { + name: "--password=PASS", + description: "Set both ftp and http password to PASS", + }, + { name: "--ask-password", description: "Prompt for passwords" }, + { name: "--no-iri", description: "Turn off IRI support" }, + { + name: "--local-encoding=ENC", + description: "Use ENC as the local encoding for IRIs", + }, + { + name: "--remote-encoding=ENC", + description: "Use ENC as the default remote encoding", + }, + { name: "--unlink", description: "Remove file before clobber" }, + { + name: "--xattr", + description: "Turn on storage of metadata in extended file attributes", + }, + { + name: ["-nd", "--no-directories"], + description: "Don't create directories", + }, + { + name: ["-x", "--force-directories"], + description: "Force creation of directories", + }, + { + name: ["-nH", "--no-host-directories"], + description: "Don't create host directories", + }, + { + name: "--protocol-directories", + description: "Use protocol name in directories", + }, + { + name: ["-P", "--directory-prefix=PREFIX"], + description: "Save files to PREFIX/", + }, + { + name: "--cut-dirs=NUMBER", + description: "Ignore NUMBER remote directory components", + }, + { name: "--http-user=USER", description: "Set http user to USER" }, + { + name: "--http-password=PASS", + description: "Set http password to PASS", + }, + { name: "--no-cache", description: "Disallow server-cached data" }, + { + name: ["-E", "--adjust-extension"], + description: "Save HTML/CSS documents with proper extensions", + }, + { + name: "--ignore-length", + description: "Ignore 'Content-Length' header field", + }, + { + name: "--header=STRING", + description: "Insert STRING among the headers", + }, + { + name: "--compression=TYPE", + description: + "Choose compression, one of auto, gzip and none. (default: none)", + }, + { + name: "--max-redirect", + description: "Maximum redirections allowed per page", + }, + { name: "--proxy-user=USER", description: "Set USER as proxy username" }, + { + name: "--proxy-password=PASS", + description: "Set PASS as proxy password", + }, + { + name: "--referer=URL", + description: "Include 'Referer: URL' header in HTTP request", + }, + { name: "--save-headers", description: "Save the HTTP headers to file" }, + { + name: ["-U", "--user-agent=AGENT"], + description: "Identify as AGENT instead of Wget/VERSION", + }, + { + name: "--no-http-keep-alive", + description: "Disable HTTP keep-alive (persistent connections)", + }, + { name: "--no-cookies", description: "Don't use cookies" }, + { + name: "--load-cookies=FILE", + description: "Load cookies from FILE before session", + }, + { + name: "--save-cookies=FILE", + description: "Save cookies to FILE after session", + }, + { + name: "--keep-session-cookies", + description: "Load and save session (non-permanent) cookies", + }, + { + name: "--post-data=STRING", + description: "Use the POST method; send STRING as the data", + }, + { + name: "--post-file=FILE", + description: "Use the POST method; send contents of FILE", + }, + { + name: "--method=HTTPMethod", + description: 'Use method "HTTPMethod" in the request', + }, + { + name: "--body-data=STRING", + description: "Send STRING as data. --method MUST be set", + }, + { + name: "--body-file=FILE", + description: "Send contents of FILE. --method MUST be set", + }, + { + name: "--content-on-error", + description: "Output the received content on server errors", + }, + { + name: "--secure-protocol=PR", + description: "Choose secure protocol, one of auto, SSLv2,", + }, + { name: "--https-only", description: "Only follow secure HTTPS links" }, + { + name: "--no-check-certificate", + description: "Don't validate the server's certificate", + }, + { name: "--certificate=FILE", description: "Client certificate file" }, + { + name: "--certificate-type=TYPE", + description: "Client certificate type, PEM or DER", + }, + { name: "--private-key=FILE", description: "Private key file" }, + { + name: "--private-key-type=TYPE", + description: "Private key type, PEM or DER", + }, + { + name: "--ca-certificate=FILE", + description: "File with the bundle of CAs", + }, + { + name: "--ca-directory=DIR", + description: "Directory where hash list of CAs is stored", + }, + { name: "--crl-file=FILE", description: "File with bundle of CRLs" }, + { + name: "--ciphers=STR", + description: + "Set the priority string (GnuTLS) or cipher list string (OpenSSL) directly", + }, + { name: ["-r", "--recursive"], description: "Specify recursive download" }, + { + name: ["-l", "--level=NUMBER"], + description: "Maximum recursion depth (inf or 0 for infinite)", + }, + { + name: "--delete-after", + description: "Delete files locally after downloading them", + }, + { + name: ["-k", "--convert-links"], + description: "Make links in downloaded HTML or CSS point to local files", + }, + { + name: ["-K", "--backup-converted"], + description: "Before converting file X, back up as X.orig", + }, + { + name: ["-m", "--mirror"], + description: "Shortcut for -N -r -l inf --no-remove-listing", + }, + { + name: ["-p", "--page-requisites"], + description: "Get all images, etc. needed to display HTML page", + }, + { + name: ["-A", "--accept=LIST"], + description: "Comma-separated list of accepted extensions", + }, + { + name: ["-R", "--reject=LIST"], + description: "Comma-separated list of rejected extensions", + }, + { + name: "--accept-regex=REGEX", + description: "Regex matching accepted URLs", + }, + { + name: "--reject-regex=REGEX", + description: "Regex matching rejected URLs", + }, + { name: "--regex-type=TYPE", description: "Regex type (posix)" }, + { + name: ["-D", "--domains=LIST"], + description: "Comma-separated list of accepted domains", + }, + { + name: "--exclude-domains=LIST", + description: "Comma-separated list of rejected domains", + }, + { + name: "--follow-ftp", + description: "Follow FTP links from HTML documents", + }, + { + name: "--follow-tags=LIST", + description: "Comma-separated list of followed HTML tags", + }, + { + name: "--ignore-tags=LIST", + description: "Comma-separated list of ignored HTML tags", + }, + { + name: ["-H", "--span-hosts"], + description: "Go to foreign hosts when recursive", + }, + { name: ["-L", "--relative"], description: "Follow relative links only" }, + { + name: ["-I", "--include-directories=LIST"], + description: "List of allowed directories", + }, + { + name: ["-X", "--exclude-directories=LIST"], + description: "List of excluded directories", + }, + { + name: ["-np", "--no-parent"], + description: "Don't ascend to the parent directory", + }, + ], }; // GNU Wget 1.20.3, a non-interactive network retriever. diff --git a/extensions/terminal-suggest/src/completions/upstream/yarn.ts b/extensions/terminal-suggest/src/completions/upstream/yarn.ts index a3de493a..04c573a1 100644 --- a/extensions/terminal-suggest/src/completions/upstream/yarn.ts +++ b/extensions/terminal-suggest/src/completions/upstream/yarn.ts @@ -82,7 +82,7 @@ const getGlobalPackagesGenerator: Fig.Generator = { name: dependencyName, icon: "📦", })); - } catch (e) { } + } catch (e) {} return []; }, @@ -101,7 +101,7 @@ const allDependenciesGenerator: Fig.Generator = { name: dependency.name.split("@")[0], icon: "📦", })); - } catch (e) { } + } catch (e) {} return []; }, }; @@ -127,7 +127,7 @@ const configList: Fig.Generator = { if (configObject) { return Object.keys(configObject).map((key) => ({ name: key })); } - } catch (e) { } + } catch (e) {} return []; }, @@ -367,7 +367,7 @@ export const createCLIsGenerator: Fig.Generator = { postProcess: function (out) { try { return JSON.parse(out).results.map( - (item: any) => + (item: { package: { name: string; description: string } }) => ({ name: item.package.name.substring(7), description: item.package.description, @@ -1550,9 +1550,9 @@ const completionSpec: Fig.Spec = { try { const workspacesDefinitions = isYarnV1 ? // transform Yarn V1 output to array of workspaces like Yarn V2 - await getWorkspacesDefinitionsV1() + await getWorkspacesDefinitionsV1() : // in yarn v>=2.0.0, workspaces definitions are a list of JSON lines - await getWorkspacesDefinitionsVOther(); + await getWorkspacesDefinitionsVOther(); const subcommands: Fig.Subcommand[] = workspacesDefinitions.map( ({ name, location }: { name: string; location: string }) => ({ @@ -1578,7 +1578,7 @@ const completionSpec: Fig.Spec = { name: script, })); } - } catch (e) { } + } catch (e) {} return []; }, }, diff --git a/extensions/terminal-suggest/src/constants.ts b/extensions/terminal-suggest/src/constants.ts index 91e0dfa1..25ada5a0 100644 --- a/extensions/terminal-suggest/src/constants.ts +++ b/extensions/terminal-suggest/src/constants.ts @@ -46,7 +46,6 @@ export const upstreamSpecs = [ 'pnpm', 'node', 'nvm', - 'npx', ]; diff --git a/extensions/terminal-suggest/src/env/pathExecutableCache.ts b/extensions/terminal-suggest/src/env/pathExecutableCache.ts index 4ce8090f..9932bc88 100644 --- a/extensions/terminal-suggest/src/env/pathExecutableCache.ts +++ b/extensions/terminal-suggest/src/env/pathExecutableCache.ts @@ -10,6 +10,8 @@ import { osIsWindows } from '../helpers/os'; import type { ICompletionResource } from '../types'; import { getFriendlyResourcePath } from '../helpers/uri'; import { SettingsIds } from '../constants'; +import * as filesystem from 'fs'; +import * as path from 'path'; const isWindows = osIsWindows(); @@ -38,7 +40,12 @@ export class PathExecutableCache implements vscode.Disposable { } } - async getExecutablesInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { + refresh(): void { + this._cachedExes = undefined; + this._cachedPathValue = undefined; + } + + async getExecutablesInPath(env: ITerminalEnvironment = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { // Create cache key let pathValue: string | undefined; if (isWindows) { @@ -96,7 +103,7 @@ export class PathExecutableCache implements vscode.Disposable { for (const [file, fileType] of files) { const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, this._cachedWindowsExeExtensions)) { - result.add({ label: file, detail: formattedPath }); + result.add({ label: file, documentation: formattedPath, kind: vscode.TerminalCompletionItemKind.Method }); labels.add(file); } } @@ -107,3 +114,47 @@ export class PathExecutableCache implements vscode.Disposable { } } } + +export async function watchPathDirectories(context: vscode.ExtensionContext, env: ITerminalEnvironment, pathExecutableCache: PathExecutableCache | undefined): Promise { + const pathDirectories = new Set(); + + const envPath = env.PATH; + if (envPath) { + envPath.split(path.delimiter).forEach(p => pathDirectories.add(p)); + } + + const activeWatchers = new Set(); + + // Watch each directory + for (const dir of pathDirectories) { + try { + if (activeWatchers.has(dir)) { + // Skip if already watching or directory doesn't exist + continue; + } + + const stat = await fs.stat(dir); + if (!stat.isDirectory()) { + continue; + } + + const watcher = filesystem.watch(dir, { persistent: false }, () => { + if (pathExecutableCache) { + // Refresh cache when directory contents change + pathExecutableCache.refresh(); + } + }); + + activeWatchers.add(dir); + + context.subscriptions.push(new vscode.Disposable(() => { + try { + watcher.close(); + activeWatchers.delete(dir); + } catch { } { } + })); + } catch { } + } +} + +export type ITerminalEnvironment = { [key: string]: string | undefined }; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts index c26f3a77..3436414c 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts @@ -35,6 +35,7 @@ import { } from './errors.js'; import { convertSubcommand, initializeDefault } from '../fig-autocomplete-shared'; import { exec, type ExecException } from 'child_process'; +import type { IFigExecuteExternals } from '../execute'; type ArgArrayState = { args: Array | null; @@ -605,8 +606,15 @@ const historyExecuteShellCommand: Fig.ExecuteCommandFunction = async () => { ); }; -const getExecuteShellCommandFunction = (isParsingHistory = false) => - isParsingHistory ? historyExecuteShellCommand : () => { throw new Error('Not implemented'); }; +function getExecuteShellCommandFunction( + isParsingHistory = false, + executeExternals: IFigExecuteExternals, +) { + if (isParsingHistory) { + return historyExecuteShellCommand; + } + return executeExternals.executeCommand; +} // const getGenerateSpecCacheKey = ( // completionObj: Internal.Subcommand, @@ -790,13 +798,14 @@ const parseArgumentsCached = async ( command: Command, context: Fig.ShellContext, spec: Fig.Spec, + executeExternals: IFigExecuteExternals, // authClient: AuthClient, isParsingHistory?: boolean, startIndex = 0, // localconsole: console.console = console, ): Promise => { // Route to cp.exec instead, we don't need to deal with ipc - const exec = getExecuteShellCommandFunction(isParsingHistory); + const exec = getExecuteShellCommandFunction(isParsingHistory, executeExternals); let currentCommand = command; let tokens = currentCommand.tokens.slice(startIndex); @@ -1124,6 +1133,7 @@ export const parseArguments = async ( command: Command | null, context: Fig.ShellContext, spec: Fig.Spec, + executeExternals: IFigExecuteExternals, // authClient: AuthClient, isParsingHistory = false, // localconsole: console.console = console, @@ -1157,6 +1167,7 @@ export const parseArguments = async ( context, // authClient, spec, + executeExternals, isParsingHistory, 0, ); diff --git a/extensions/terminal-suggest/src/fig/autocomplete/generators/scriptSuggestionsGenerator.ts b/extensions/terminal-suggest/src/fig/autocomplete/generators/scriptSuggestionsGenerator.ts index 07e90069..bd846326 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete/generators/scriptSuggestionsGenerator.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete/generators/scriptSuggestionsGenerator.ts @@ -28,7 +28,7 @@ export async function getScriptSuggestions( } try { - const { isDangerous, tokenArray, currentWorkingDirectory } = context; + const { isDangerous, tokenArray, currentWorkingDirectory, environmentVariables } = context; // A script can either be a string or a function that returns a string. // If the script is a function, run it, and get the output string. const commandToRun = @@ -46,6 +46,7 @@ export async function getScriptSuggestions( command: commandToRun[0], args: commandToRun.slice(1), cwd: currentWorkingDirectory, + env: environmentVariables }; } else { executeCommandInput = { diff --git a/extensions/terminal-suggest/src/fig/execute.ts b/extensions/terminal-suggest/src/fig/execute.ts index ab9792f6..2c642019 100644 --- a/extensions/terminal-suggest/src/fig/execute.ts +++ b/extensions/terminal-suggest/src/fig/execute.ts @@ -20,7 +20,7 @@ export const executeCommandTimeout = async ( ): Promise => { const command = [input.command, ...input.args].join(' '); try { - console.info(`About to run shell command '${command}'`); + console.debug(`About to run shell command '${command}'`); const result = await withTimeout( Math.max(timeout, input.timeout ?? 0), spawnHelper2(input.command, input.args, { diff --git a/extensions/terminal-suggest/src/fig/figInterface.ts b/extensions/terminal-suggest/src/fig/figInterface.ts index 7e09c8b6..cb764e4e 100644 --- a/extensions/terminal-suggest/src/fig/figInterface.ts +++ b/extensions/terminal-suggest/src/fig/figInterface.ts @@ -16,7 +16,7 @@ import type { ICompletionResource } from '../types'; import { osIsWindows } from '../helpers/os'; import { removeAnyFileExtension } from '../helpers/file'; import type { EnvironmentVariable } from './api-bindings/types'; -import { asArray } from '../terminalSuggestMain'; +import { asArray, availableSpecs } from '../terminalSuggestMain'; import { IFigExecuteExternals } from './execute'; export interface IFigSpecSuggestionsResult { @@ -52,11 +52,10 @@ export async function getFigSuggestions( if (!specLabels) { continue; } - for (const specLabel of specLabels) { const availableCommand = (osIsWindows() ? availableCommands.find(command => (typeof command.label === 'string' ? command.label : command.label.label).match(new RegExp(`${specLabel}(\\.[^ ]+)?$`))) - : availableCommands.find(command => (typeof command.label === 'string' ? command.label : command.label.label).startsWith(specLabel))); + : availableCommands.find(command => (typeof command.label === 'string' ? command.label : command.label.label) === (specLabel))); if (!availableCommand || (token && token.isCancellationRequested)) { continue; } @@ -81,17 +80,20 @@ export async function getFigSuggestions( const commandAndAliases = (osIsWindows() ? availableCommands.filter(command => specLabel === removeAnyFileExtension(command.definitionCommand ?? (typeof command.label === 'string' ? command.label : command.label.label))) - : availableCommands.filter(command => specLabel === (command.definitionCommand ?? command.label))); + : availableCommands.filter(command => specLabel === (command.definitionCommand ?? (typeof command.label === 'string' ? command.label : command.label.label)))); if ( !(osIsWindows() ? commandAndAliases.some(e => precedingText.startsWith(`${removeAnyFileExtension((typeof e.label === 'string' ? e.label : e.label.label))} `)) - : commandAndAliases.some(e => precedingText.startsWith(`${e.label} `))) + : commandAndAliases.some(e => precedingText.startsWith(`${typeof e.label === 'string' ? e.label : e.label.label} `))) ) { - // the spec label is not the first word in the command line, so do not provide options or args continue; } - const completionItemResult = await getFigSpecSuggestions(spec, terminalContext, prefix, shellIntegrationCwd, env, name, executeExternals, token); + const actualSpec = availableCommand.definitionCommand ? availableSpecs.find(s => s.name === availableCommand.definitionCommand) : spec; + if (!actualSpec) { + continue; + } + const completionItemResult = await getFigSpecSuggestions(actualSpec, terminalContext, prefix, shellIntegrationCwd, env, name, executeExternals, token); result.hasCurrentArg ||= !!completionItemResult?.hasCurrentArg; if (completionItemResult) { result.filesRequested ||= completionItemResult.filesRequested; @@ -131,7 +133,7 @@ async function getFigSpecSuggestions( currentProcess: name, // TODO: pass in aliases }; - const parsedArguments: ArgumentParserResult = await parseArguments(command, shellContext, spec); + const parsedArguments: ArgumentParserResult = await parseArguments(command, shellContext, spec, executeExternals); const items: vscode.TerminalCompletionItem[] = []; // TODO: Pass in and respect cancellation token @@ -273,11 +275,30 @@ export async function collectCompletionItemResult( itemKind = vscode.TerminalCompletionItemKind.OptionValue; } + // Add for every argument + let detail: string | undefined; + if (typeof item === 'object' && 'args' in item) { + const args = asArray(item.args); + if (args.every(e => !!e?.name)) { + if (args.length > 0) { + detail = ' ' + args.map(e => { + let result = `<${e!.name}>`; + if (e?.isOptional) { + result = `[${result}]`; + } + return result; + }).join(' '); + } + } + } + items.push( createCompletionItem( terminalContext.cursorPosition, prefix, - { label }, + { + label: detail ? { label, detail } : label + }, undefined, typeof item === 'string' ? item : item.description, itemKind, diff --git a/extensions/terminal-suggest/src/helpers/promise.ts b/extensions/terminal-suggest/src/helpers/promise.ts new file mode 100644 index 00000000..14e86bc0 --- /dev/null +++ b/extensions/terminal-suggest/src/helpers/promise.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function createTimeoutPromise(timeout: number, defaultValue: T): Promise { + return new Promise(resolve => setTimeout(() => resolve(defaultValue), timeout)); +} diff --git a/extensions/terminal-suggest/src/shell/bash.ts b/extensions/terminal-suggest/src/shell/bash.ts index 8db683af..15744908 100644 --- a/extensions/terminal-suggest/src/shell/bash.ts +++ b/extensions/terminal-suggest/src/shell/bash.ts @@ -16,7 +16,8 @@ export async function getBashGlobals(options: ExecOptionsWithStringEncoding, exi } async function getAliases(options: ExecOptionsWithStringEncoding): Promise { - return getAliasesHelper('bash', ['-ic', 'alias'], /^alias (?[a-zA-Z0-9\.:-]+)='(?.+)'$/, options); + const args = process.platform === 'darwin' ? ['-icl', 'alias'] : ['-ic', 'alias']; + return getAliasesHelper('bash', args, /^alias (?[a-zA-Z0-9\.:-]+)='(?.+)'$/, options); } export async function getBuiltins( diff --git a/extensions/terminal-suggest/src/shell/fish.ts b/extensions/terminal-suggest/src/shell/fish.ts index cc2fbaff..98c1ac09 100644 --- a/extensions/terminal-suggest/src/shell/fish.ts +++ b/extensions/terminal-suggest/src/shell/fish.ts @@ -3,21 +3,87 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import type { ICompletionResource } from '../types'; -import { execHelper, getAliasesHelper } from './common'; +import { getAliasesHelper } from './common'; import { type ExecOptionsWithStringEncoding } from 'node:child_process'; +import { fishBuiltinsCommandDescriptionsCache } from './fishBuiltinsCache'; + +const commandDescriptionsCache: Map | undefined = parseCache(fishBuiltinsCommandDescriptionsCache); export async function getFishGlobals(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise<(string | ICompletionResource)[]> { return [ ...await getAliases(options), - ...await getBuiltins(options, existingCommands), + ...await getBuiltins(options), ]; } -async function getBuiltins(options: ExecOptionsWithStringEncoding, existingCommands?: Set): Promise { - const compgenOutput = await execHelper('functions -n', options); - const filter = (cmd: string) => cmd && !existingCommands?.has(cmd); - return compgenOutput.split(', ').filter(filter); +async function getBuiltins(options: ExecOptionsWithStringEncoding): Promise<(string | ICompletionResource)[]> { + const completions: ICompletionResource[] = []; + + // Use the cache directly for all commands + for (const cmd of [...commandDescriptionsCache!.keys()]) { + try { + const result = getCommandDescription(cmd); + if (result) { + completions.push({ + label: { label: cmd, description: result.description }, + detail: result.args, + documentation: new vscode.MarkdownString(result.documentation), + kind: vscode.TerminalCompletionItemKind.Method + }); + } else { + console.warn(`Fish command "${cmd}" not found in cache.`); + completions.push({ + label: cmd, + kind: vscode.TerminalCompletionItemKind.Method + }); + } + } catch (e) { + // Ignore errors + completions.push({ + label: cmd, + kind: vscode.TerminalCompletionItemKind.Method + }); + } + } + + return completions; +} + +export function getCommandDescription(command: string): { documentation?: string; description?: string; args?: string | undefined } | undefined { + if (!commandDescriptionsCache) { + return undefined; + } + const result = commandDescriptionsCache.get(command); + if (!result) { + return undefined; + } + + if (result.shortDescription) { + return { + description: result.shortDescription, + args: result.args, + documentation: result.description + }; + } else { + return { + description: result.description, + args: result.args, + documentation: result.description + }; + } +} + +function parseCache(cache: Object): Map | undefined { + if (!cache) { + return undefined; + } + const result = new Map(); + for (const [key, value] of Object.entries(cache)) { + result.set(key, value); + } + return result; } async function getAliases(options: ExecOptionsWithStringEncoding): Promise { diff --git a/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts b/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts new file mode 100644 index 00000000..2a5aefc4 --- /dev/null +++ b/extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts @@ -0,0 +1,301 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const fishBuiltinsCommandDescriptionsCache = { + ".": { + "shortDescription": "source - evaluate contents of file", + "description": "source evaluates the commands of the specified FILE in the current\nshell as a new block of code. This is different from starting a new\nprocess to perform the commands (i.e. fish < FILE) since the commands\nwill be evaluated by the current shell, which means that changes in\nshell variables will affect the current shell. If additional\narguments are specified after the file name, they will be inserted\ninto the argv variable. The argv variable will not include the name\nof the sourced file.\n\nfish will search the working directory to resolve relative paths but\nwill not search PATH .\n\nIf no file is specified and stdin is not the terminal, or if the file\nname - is used, stdin will be read.\n\nThe exit status of source is the exit status of the last job to\nexecute. If something goes wrong while opening or reading the file,\nsource exits with a non-zero status.\n\n. (a single period) is an alias for the source command. The use of .\nis deprecated in favour of source, and . will be removed in a future\nversion of fish.\n\nsource creates a new local scope; set --local within a sourced block\nwill not affect variables in the enclosing scope.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\n\n source ~/.config/fish/config.fish\n # Causes fish to re-read its initialization file.\n\nCAVEATS\nIn fish versions prior to 2.3.0, the argv variable would have a\nsingle element (the name of the sourced file) if no arguments are\npresent. Otherwise, it would contain arguments without the name of\nthe sourced file. That behavior was very confusing and unlike other\nshells such as bash and zsh.", + "args": "source FILE [ARGUMENTS ...]\nSOMECOMMAND | source" + }, + ":": { + "shortDescription": "No operation command", + "description": "The `:` command is a no-op (no operation) command that returns a successful (zero) exit status. It can be used as a placeholder in scripts where a command is syntactically required but no action is desired." + }, + "[": { + "shortDescription": "Test if a statement is true", + "description": "Evaluate an expression and return a status of true (0) or false (non-zero). Unlike the `test` command, the `[` command requires a closing `]`.", + "args": "EXPRESSION ]" + }, + "_": { + "shortDescription": "", + "description": "" + }, + "abbr": { + "shortDescription": "manage fish abbreviations", + "description": "abbr manages abbreviations - user-defined words that are replaced\nwith longer phrases when entered.\n\nNOTE:\n Only typed-in commands use abbreviations. Abbreviations are not\n expanded in scripts.\n\nFor example, a frequently-run command like git checkout can be\nabbreviated to gco. After entering gco and pressing Space or Enter,\nthe full text git checkout will appear in the command line. To avoid\nexpanding something that looks like an abbreviation, the default\nControl+Space binding inserts a space without expanding.\n\nAn abbreviation may match a literal word, or it may match a pattern\ngiven by a regular expression. When an abbreviation matches a word,\nthat word is replaced by new text, called its expansion. This\nexpansion may be a fixed new phrase, or it can be dynamically created\nvia a fish function. This expansion occurs after pressing space or\nenter.\n\nCombining these features, it is possible to create custom syntaxes,\nwhere a regular expression recognizes matching tokens, and the\nexpansion function interprets them. See the Examples section.\n\nChanged in version 3.6.0: Previous versions of this allowed saving\nabbreviations in universal variables. That's no longer possible.\nExisting variables will still be imported and abbr --erase will also\nerase the variables. We recommend adding abbreviations to\nconfig.fish by just adding the abbr --add command. When you run\nabbr, you will see output like this\n\n > abbr\n abbr -a -- foo bar # imported from a universal variable, see `help abbr`\n\nIn that case you should take the part before the # comment and save\nit in config.fish, then you can run abbr --erase to remove the\nuniversal variable:\n\n > abbr >> ~/.config/fish/config.fish\n > abbr --erase (abbr --list)\n\nADD SUBCOMMAND\nabbr [-a | --add] NAME [--position command | anywhere] [-r | --regex PATTERN]\n [--set-cursor[=MARKER]] ([-f | --function FUNCTION] | EXPANSION)\n\nabbr --add creates a new abbreviation. With no other options, the\nstring NAME is replaced by EXPANSION.\n\nWith --position command, the abbreviation will only expand when it is\npositioned as a command, not as an argument to another command. With\n--position anywhere the abbreviation may expand anywhere in the\ncommand line. The default is command.\n\nWith --regex, the abbreviation matches using the regular expression\ngiven by PATTERN, instead of the literal NAME. The pattern is\ninterpreted using PCRE2 syntax and must match the entire token. If\nmultiple abbreviations match the same token, the last abbreviation\nadded is used.\n\nWith --set-cursor=MARKER, the cursor is moved to the first occurrence\nof MARKER in the expansion. The MARKER value is erased. The MARKER\nmay be omitted (i.e. simply --set-cursor), in which case it defaults\nto %.\n\nWith -f FUNCTION or --function FUNCTION, FUNCTION is treated as the\nname of a fish function instead of a literal replacement. When the\nabbreviation matches, the function will be called with the matching\ntoken as an argument. If the function's exit status is 0 (success),\nthe token will be replaced by the function's output; otherwise the\ntoken will be left unchanged. No EXPANSION may be given separately.\n\n Examples\n\n abbr --add gco git checkout\n\nAdd a new abbreviation where gco will be replaced with git checkout.\n\n abbr -a --position anywhere -- -C --color\n\nAdd a new abbreviation where -C will be replaced with --color. The --\nallows -C to be treated as the name of the abbreviation, instead of\nan option.\n\n abbr -a L --position anywhere --set-cursor \"% | less\"\n\nAdd a new abbreviation where L will be replaced with | less, placing\nthe cursor before the pipe.\n\n function last_history_item\n echo $history[1]\n end\n abbr -a !! --position anywhere --function last_history_item\n\nThis first creates a function last_history_item which outputs the\nlast entered command. It then adds an abbreviation which replaces !!\nwith the result of calling this function. Taken together, this is\nsimilar to the !! history expansion feature of bash.\n\n function vim_edit\n echo vim $argv\n end\n abbr -a vim_edit_texts --position command --regex \".+\\.txt\" --function vim_edit\n\nThis first creates a function vim_edit which prepends vim before its\nargument. It then adds an abbreviation which matches commands ending\nin .txt, and replaces the command with the result of calling this\nfunction. This allows text files to be \"executed\" as a command to\nopen them in vim, similar to the \"suffix alias\" feature in zsh.\n\n abbr 4DIRS --set-cursor=! \"$(string join \\n -- 'for dir in */' 'cd $dir' '!' 'cd ..' 'end')\"\n\nThis creates an abbreviation \"4DIRS\" which expands to a multi-line\nloop \"template.\" The template enters each directory and then leaves\nit. The cursor is positioned ready to enter the command to run in\neach directory, at the location of the !, which is itself erased.\n\nOTHER SUBCOMMANDS\n\n abbr --rename OLD_NAME NEW_NAME\n\nRenames an abbreviation, from OLD_NAME to NEW_NAME\n\n abbr [-s | --show]\n\nShow all abbreviations in a manner suitable for import and export\n\n abbr [-l | --list]\n\nPrints the names of all abbreviation\n\n abbr [-e | --erase] NAME\n\nErases the abbreviation with the given name\n\n abbr -q or --query [NAME...]\n\nReturn 0 (true) if one of the NAME is an abbreviation.\n\n abbr -h or --help\n\nDisplays help for the abbr command.", + "args": "abbr --add NAME [--position command | anywhere] [-r | --regex PATTERN]\n[--set-cursor[=MARKER]] ([-f | --function FUNCTION] | EXPANSION)\nabbr --erase NAME ...\nabbr --rename OLD_WORD NEW_WORD\nabbr --show\nabbr --list\nabbr --query NAME ..." + }, + "and": { + "shortDescription": "conditionally execute a command", + "description": "and is used to execute a command if the previous command was\nsuccessful (returned a status of 0).\n\nand statements may be used as part of the condition in an while or if\nblock.\n\nand does not change the current exit status itself, but the command\nit runs most likely will. The exit status of the last foreground\ncommand to exit can always be accessed using the $status variable.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nThe following code runs the make command to build a program. If the\nbuild succeeds, make's exit status is 0, and the program is\ninstalled. If either step fails, the exit status is 1, and make clean\nis run, which removes the files created by the build process.\n\n make; and make install; or make clean\n\nSEE ALSO\n\n• or command\n\n• not command", + "args": "PREVIOUS; and COMMAND" + }, + "argparse": { + "shortDescription": "parse options passed to a fish script or function", + "description": "This command makes it easy for fish scripts and functions to handle\narguments. You pass arguments that define the known options, followed\nby a literal --, then the arguments to be parsed (which might also\ninclude a literal --). argparse then sets variables to indicate the\npassed options with their values, and sets $argv to the remaining\narguments. See the usage section below.\n\nEach option specification (OPTION_SPEC) is written in the domain\nspecific language described below. All OPTION_SPECs must appear after\nany argparse flags and before the -- that separates them from the\narguments to be parsed.\n\nEach option that is seen in the ARG list will result in variables\nnamed _flag_X, where X is the short flag letter and the long flag\nname (if they are defined). For example a --help option could cause\nargparse to define one variable called _flag_h and another called\n_flag_help.\n\nThe variables will be set with local scope (i.e., as if the script\nhad done set -l _flag_X). If the flag is a boolean (that is, it just\nis passed or not, it doesn't have a value) the values are the short\nand long flags seen. If the option is not a boolean the values will\nbe zero or more values corresponding to the values collected when the\nARG list is processed. If the flag was not seen the flag variable\nwill not be set.\n\nOPTIONS\nThe following argparse options are available. They must appear before\nall OPTION_SPECs:\n\n-n or --name\n The command name for use in error messages. By default the\n current function name will be used, or argparse if run outside\n of a function.\n\n-x or --exclusive OPTIONS\n A comma separated list of options that are mutually exclusive.\n You can use this more than once to define multiple sets of\n mutually exclusive options. You give either the short or long\n version of each option, and you still need to otherwise define\n the options.\n\n-N or --min-args NUMBER\n The minimum number of acceptable non-option arguments. The\n default is zero.\n\n-X or --max-args NUMBER\n The maximum number of acceptable non-option arguments. The\n default is infinity.\n\n-i or --ignore-unknown\n Ignores unknown options, keeping them and their arguments in\n $argv instead.\n\n-s or --stop-nonopt\n Causes scanning the arguments to stop as soon as the first\n non-option argument is seen. Among other things, this is\n useful to implement subcommands that have their own options.\n\n-h or --help\n Displays help about using this command.\n\nUSAGE\nTo use this command, pass the option specifications (OPTION_SPEC), a\nmandatory --, and then the arguments to be parsed.\n\nA simple example:\n\n argparse --name=my_function 'h/help' 'n/name=' -- $argv\n or return\n\nIf $argv is empty then there is nothing to parse and argparse returns\nzero to indicate success. If $argv is not empty then it is checked\nfor flags -h, --help, -n and --name. If they are found they are\nremoved from the arguments and local variables called _flag_OPTION\nare set so the script can determine which options were seen. If $argv\ndoesn't have any errors, like a missing mandatory value for an\noption, then argparse exits with a status of zero. Otherwise it\nwrites appropriate error messages to stderr and exits with a status\nof one.\n\nThe or return means that the function returns argparse's status if it\nfailed, so if it goes on argparse succeeded.\n\nThe -- argument is required. You do not have to include any option\nspecifications or arguments after the -- but you must include the --.\nFor example, this is acceptable:\n\n set -l argv foo\n argparse 'h/help' 'n/name' -- $argv\n argparse --min-args=1 -- $argv\n\nBut this is not:\n\n set -l argv\n argparse 'h/help' 'n/name' $argv\n\nThe first -- seen is what allows the argparse command to reliably\nseparate the option specifications and options to argparse itself\n(like --ignore-unknown) from the command arguments, so it is\nrequired.\n\nOPTION SPECIFICATIONS\nEach option specification consists of:\n\n• An optional alphanumeric short flag character, followed by a / if\n the short flag can be used by someone invoking your command or, for\n backwards compatibility, a - if it should not be exposed as a valid\n short flag (in which case it will also not be exposed as a flag\n variable).\n\n• An optional long flag name, which if not present the short flag can\n be used, and if that is also not present, an error is reported\n\n• Nothing if the flag is a boolean that takes no argument or is an\n integer flag, or\n\n • = if it requires a value and only the last instance of the\n flag is saved, or\n\n • =? if it takes an optional value and only the last instance of\n the flag is saved, or\n\n • =+ if it requires a value and each instance of the flag is\n saved.\n\n• Optionally a ! followed by fish script to validate the value.\n Typically this will be a function to run. If the exit status is\n zero the value for the flag is valid. If non-zero the value is\n invalid. Any error messages should be written to stdout (not\n stderr). See the section on Flag Value Validation for more\n information.\n\nSee the fish_opt command for a friendlier but more verbose way to\ncreate option specifications.\n\nIf a flag is not seen when parsing the arguments then the\ncorresponding _flag_X var(s) will not be set.\n\nINTEGER FLAG\nSometimes commands take numbers directly as options, like foo -55. To\nallow this one option spec can have the # modifier so that any\ninteger will be understood as this flag, and the last number will be\ngiven as its value (as if = was used).\n\nThe # must follow the short flag letter (if any), and other modifiers\nlike = are not allowed, except for - (for backwards compatibility):\n\n m#maximum\n\nThis does not read numbers given as +NNN, only those that look like\nflags - -NNN.\n\nNOTE: OPTIONAL ARGUMENTS\nAn option defined with =? can take optional arguments. Optional\narguments have to be directly attached to the option they belong to.\n\nThat means the argument will only be used for the option if you use\nit like:\n\n cmd --flag=value\n # or\n cmd -fvalue\n\nbut not if used like:\n\n cmd --flag value\n # \"value\" here will be used as a positional argument\n # and \"--flag\" won't have an argument.\n\nIf this weren't the case, using an option without an optional\nargument would be difficult if you also wanted to use positional\narguments.\n\nFor example:\n\n grep --color auto\n # Here \"auto\" will be used as the search string,\n # \"color\" will not have an argument and will fall back to the default,\n # which also *happens to be* auto.\n grep --color always\n # Here grep will still only use color \"auto\"matically\n # and search for the string \"always\".\n\nThis isn't specific to argparse but common to all things using\ngetopt(3) (if they have optional arguments at all). That grep example\nis how GNU grep actually behaves.\n\nFLAG VALUE VALIDATION\nSometimes you need to validate the option values. For example, that\nit is a valid integer within a specific range, or an ip address, or\nsomething entirely different. You can always do this after argparse\nreturns but you can also request that argparse perform the validation\nby executing arbitrary fish script. To do so simply append an !\n(exclamation-mark) then the fish script to be run. When that code is\nexecuted three vars will be defined:\n\n• _argparse_cmd will be set to the value of the value of the argparse\n --name value.\n\n• _flag_name will be set to the short or long flag that being\n processed.\n\n• _flag_value will be set to the value associated with the flag being\n processed.\n\nThese variables are passed to the function as local exported\nvariables.\n\nThe script should write any error messages to stdout, not stderr. It\nshould return a status of zero if the flag value is valid otherwise a\nnon-zero status to indicate it is invalid.\n\nFish ships with a _validate_int function that accepts a --min and\n--max flag. Let's say your command accepts a -m or --max flag and the\nminimum allowable value is zero and the maximum is 5. You would\ndefine the option like this: m/max=!_validate_int --min 0 --max 5.\nThe default if you just call _validate_int without those flags is to\nsimply check that the value is a valid integer with no limits on the\nmin or max value allowed.\n\nHere are some examples of flag validations:\n\n # validate that a path is a directory\n argparse 'p/path=!test -d \"$_flag_value\"' -- --path $__fish_config_dir\n # validate that a function does not exist\n argparse 'f/func=!not functions -q \"$_flag_value\"' -- -f alias\n # validate that a string matches a regex\n argparse 'c/color=!string match -rq \\'^#?[0-9a-fA-F]{6}$\\' \"$_flag_value\"' -- -c 'c0ffee'\n # validate with a validator function\n argparse 'n/num=!_validate_int --min 0 --max 99' -- --num 42\n\nEXAMPLE OPTION_SPECS\nSome OPTION_SPEC examples:\n\n• h/help means that both -h and --help are valid. The flag is a\n boolean and can be used more than once. If either flag is used then\n _flag_h and _flag_help will be set to however either flag was seen,\n as many times as it was seen. So it could be set to -h, -h and\n --help, and count $_flag_h would yield \"3\".\n\n• help means that only --help is valid. The flag is a boolean and can\n be used more than once. If it is used then _flag_help will be set\n as above. Also h-help (with an arbitrary short letter) for\n backwards compatibility.\n\n• longonly= is a flag --longonly that requires an option, there is no\n short flag or even short flag variable.\n\n• n/name= means that both -n and --name are valid. It requires a\n value and can be used at most once. If the flag is seen then\n _flag_n and _flag_name will be set with the single mandatory value\n associated with the flag.\n\n• n/name=? means that both -n and --name are valid. It accepts an\n optional value and can be used at most once. If the flag is seen\n then _flag_n and _flag_name will be set with the value associated\n with the flag if one was provided else it will be set with no\n values.\n\n• name=+ means that only --name is valid. It requires a value and can\n be used more than once. If the flag is seen then _flag_name will be\n set with the values associated with each occurrence.\n\n• x means that only -x is valid. It is a boolean that can be used\n more than once. If it is seen then _flag_x will be set as above.\n\n• x=, x=?, and x=+ are similar to the n/name examples above but there\n is no long flag alternative to the short flag -x.\n\n• #max (or #-max) means that flags matching the regex \"^--?\\d+$\" are\n valid. When seen they are assigned to the variable _flag_max. This\n allows any valid positive or negative integer to be specified by\n prefixing it with a single \"-\". Many commands support this idiom.\n For example head -3 /a/file to emit only the first three lines of\n /a/file.\n\n• n#max means that flags matching the regex \"^--?\\d+$\" are valid.\n When seen they are assigned to the variables _flag_n and _flag_max.\n This allows any valid positive or negative integer to be specified\n by prefixing it with a single \"-\". Many commands support this\n idiom. For example head -3 /a/file to emit only the first three\n lines of /a/file. You can also specify the value using either flag:\n -n NNN or --max NNN in this example.\n\n• #longonly causes the last integer option to be stored in\n _flag_longonly.\n\nAfter parsing the arguments the argv variable is set with local scope\nto any values not already consumed during flag processing. If there\nare no unbound values the variable is set but count $argv will be\nzero.\n\nIf an error occurs during argparse processing it will exit with a\nnon-zero status and print error messages to stderr.\n\nEXAMPLES\nA simple use:\n\n argparse h/help -- $argv\n or return\n\n if set -q _flag_help\n # TODO: Print help here\n return 0\n end\n\nThis just wants one option - -h / --help. Any other option is an\nerror. If it is given it prints help and exits.\n\nHow fish_add_path - add to the path parses its args:\n\n argparse -x g,U -x P,U -x a,p g/global U/universal P/path p/prepend a/append h/help m/move v/verbose n/dry-run -- $argv\n\nThere are a variety of boolean flags, all with long and short\nversions. A few of these cannot be used together, and that is what\nthe -x flag is used for. -x g,U means that --global and --universal\nor their short equivalents conflict, and if they are used together\nyou get an error. In this case you only need to give the short or\nlong flag, not the full option specification.\n\nAfter this it figures out which variable it should operate on\naccording to the --path flag:\n\n set -l var fish_user_paths\n set -q _flag_path\n and set var PATH\n\nLIMITATIONS\nOne limitation with --ignore-unknown is that, if an unknown option is\ngiven in a group with known options, the entire group will be kept in\n$argv. argparse will not do any permutations here.\n\nFor instance:\n\n argparse --ignore-unknown h -- -ho\n echo $_flag_h # is -h, because -h was given\n echo $argv # is still -ho\n\nThis limitation may be lifted in future.\n\nAdditionally, it can only parse known options up to the first unknown\noption in the group - the unknown option could take options, so it\nisn't clear what any character after an unknown option means.", + "args": "argparse [OPTIONS] OPTION_SPEC ... -- [ARG ...]" + }, + "begin": { + "shortDescription": "start a new block of code", + "description": "begin is used to create a new block of code.\n\nA block allows the introduction of a new variable scope, redirection\nof the input or output of a set of commands as a group, or to specify\nprecedence when using the conditional commands like and.\n\nThe block is unconditionally executed. begin; ...; end is equivalent\nto if true; ...; end.\n\nbegin does not change the current exit status itself. After the block\nhas completed, $status will be set to the status returned by the most\nrecent command.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nThe following code sets a number of variables inside of a block\nscope. Since the variables are set inside the block and have local\nscope, they will be automatically deleted when the block ends.\n\n begin\n set -l PIRATE Yarrr\n\n ...\n end\n\n echo $PIRATE\n # This will not output anything, since the PIRATE variable\n # went out of scope at the end of the block\n\nIn the following code, all output is redirected to the file out.html.\n\n begin\n echo $xml_header\n echo $html_header\n if test -e $file\n ...\n end\n ...\n end > out.html", + "args": "begin; [COMMANDS ...]; end" + }, + "bg": { + "shortDescription": "send jobs to background", + "description": "bg sends jobs to the background, resuming them if they are stopped.\n\nA background job is executed simultaneously with fish, and does not\nhave access to the keyboard. If no job is specified, the last job to\nbe used is put in the background. If PID is specified, the jobs\ncontaining the specified process IDs are put in the background.\n\nFor compatibility with other shells, job expansion syntax is\nsupported for bg. A PID of the format %1 will be interpreted as the\nPID of job 1. Job numbers can be seen in the output of jobs.\n\nWhen at least one of the arguments isn't a valid job specifier, bg\nwill print an error without backgrounding anything.\n\nWhen all arguments are valid job specifiers, bg will background all\nmatching jobs that exist.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nbg 123 456 789 will background the jobs that contain processes 123,\n456 and 789.\n\nIf only 123 and 789 exist, it will still background them and print an\nerror about 456.\n\nbg 123 banana or bg banana 123 will complain that \"banana\" is not a\nvalid job specifier.\n\nbg %1 will background job 1.", + "args": "bg [PID ...]" + }, + "bind": { + "shortDescription": "handle fish key bindings", + "description": "bind manages bindings.\n\nIt can add bindings if given a SEQUENCE of characters to bind to.\nThese should be written as fish escape sequences. The most important\nof these are \\c for the control key, and \\e for escape, and because\nof historical reasons also the Alt key (sometimes also called\n\"Meta\").\n\nFor example, Alt+W can be written as \\ew, and Control+X (^X) can be\nwritten as \\cx. Note that Alt-based key bindings are case sensitive\nand Control-based key bindings are not. This is a constraint of\ntext-based terminals, not fish.\n\nThe generic key binding that matches if no other binding does can be\nset by specifying a SEQUENCE of the empty string (that is, '' ). For\nmost key bindings, it makes sense to bind this to the self-insert\nfunction (i.e. bind '' self-insert). This will insert any keystrokes\nnot specifically bound to into the editor. Non-printable characters\nare ignored by the editor, so this will not result in control\nsequences being inserted.\n\nIf the -k switch is used, the name of a key (such as 'down', 'up' or\n'backspace') is used instead of a sequence. The names used are the\nsame as the corresponding curses variables, but without the 'key'\nprefix. (See terminfo(5) for more information, or use bind\n--key-names for a list of all available named keys). Normally this\nwill print an error if the current $TERM entry doesn't have a given\nkey, unless the -s switch is given.\n\nTo find out what sequence a key combination sends, you can use\nfish_key_reader.\n\nCOMMAND can be any fish command, but it can also be one of a set of\nspecial input functions. These include functions for moving the\ncursor, operating on the kill-ring, performing tab completion, etc.\nUse bind --function-names for a complete list of these input\nfunctions.\n\nWhen COMMAND is a shellscript command, it is a good practice to put\nthe actual code into a function and simply bind to the function name.\nThis way it becomes significantly easier to test the function while\nediting, and the result is usually more readable as well.\n\nNOTE:\n Special input functions cannot be combined with ordinary shell\n script commands. The commands must be entirely a sequence of\n special input functions (from bind -f) or all shell script\n commands (i.e., valid fish script). To run special input functions\n from regular fish script, use commandline -f (see also\n commandline). If a script produces output, it should finish by\n calling commandline -f repaint to tell fish that a repaint is in\n order.\n\nIf no SEQUENCE is provided, all bindings (or just the bindings in the\ngiven MODE) are printed. If SEQUENCE is provided but no COMMAND, just\nthe binding matching that sequence is printed.\n\nTo save custom key bindings, put the bind statements into\nconfig.fish. Alternatively, fish also automatically executes a\nfunction called fish_user_key_bindings if it exists.\n\nKey bindings may use \"modes\", which mimics Vi's modal input behavior.\nThe default mode is \"default\". Every key binding applies to a single\nmode; you can specify which one with -M MODE. If the key binding\nshould change the mode, you can specify the new mode with -m\nNEW_MODE. The mode can be viewed and changed via the $fish_bind_mode\nvariable. If you want to change the mode from inside a fish function,\nuse set fish_bind_mode MODE.\n\nOPTIONS\nThe following options are available:\n\n-k or --key\n Specify a key name, such as 'left' or 'backspace' instead of a\n character sequence\n\n-K or --key-names\n Display a list of available key names. Specifying -a or --all\n includes keys that don't have a known mapping\n\n-f or --function-names\n Display a list of available input functions\n\n-L or --list-modes\n Display a list of defined bind modes\n\n-M MODE or --mode MODE\n Specify a bind mode that the bind is used in. Defaults to\n \"default\"\n\n-m NEW_MODE or --sets-mode NEW_MODE\n Change the current mode to NEW_MODE after this binding is\n executed\n\n-e or --erase\n Erase the binding with the given sequence and mode instead of\n defining a new one. Multiple sequences can be specified with\n this flag. Specifying -a or --all with -M or --mode erases\n all binds in the given mode regardless of sequence.\n Specifying -a or --all without -M or --mode erases all binds\n in all modes regardless of sequence.\n\n-a or --all\n See --erase and --key-names\n\n--preset and --user\n Specify if bind should operate on user or preset bindings.\n User bindings take precedence over preset bindings when fish\n looks up mappings. By default, all bind invocations work on\n the \"user\" level except for listing, which will show both\n levels. All invocations except for inserting new bindings can\n operate on both levels at the same time (if both --preset and\n --user are given). --preset should only be used in full\n binding sets (like when working on fish_vi_key_bindings).\n\n-s or --silent\n Silences some of the error messages, including for unknown key\n names and unbound sequences.\n\n-h or --help\n Displays help about using this command.\n\nSPECIAL INPUT FUNCTIONS\nThe following special input functions are available:\n\nand only execute the next function if the previous succeeded\n (note: only some functions report success)\n\naccept-autosuggestion\n accept the current autosuggestion\n\nbackward-char\n move one character to the left. If the completion pager is\n active, select the previous completion instead.\n\nbackward-bigword\n move one whitespace-delimited word to the left\n\nbackward-delete-char\n deletes one character of input to the left of the cursor\n\nbackward-kill-bigword\n move the whitespace-delimited word to the left of the cursor\n to the killring\n\nbackward-kill-line\n move everything from the beginning of the line to the cursor\n to the killring\n\nbackward-kill-path-component\n move one path component to the left of the cursor to the\n killring. A path component is everything likely to belong to a\n path component, i.e. not any of the following: /={,}'\":@\n |;<>&, plus newlines and tabs.\n\nbackward-kill-word\n move the word to the left of the cursor to the killring. The\n \"word\" here is everything up to punctuation or whitespace.\n\nbackward-word\n move one word to the left\n\nbeginning-of-buffer\n moves to the beginning of the buffer, i.e. the start of the\n first line\n\nbeginning-of-history\n move to the beginning of the history\n\nbeginning-of-line\n move to the beginning of the line\n\nbegin-selection\n start selecting text\n\ncancel cancel the current commandline and replace it with a new empty\n one\n\ncancel-commandline\n cancel the current commandline and replace it with a new empty\n one, leaving the old one in place with a marker to show that\n it was cancelled\n\ncapitalize-word\n make the current word begin with a capital letter\n\nclear-screen\n clears the screen and redraws the prompt. if the terminal\n doesn't support clearing the screen it is the same as repaint.\n\ncomplete\n guess the remainder of the current token\n\ncomplete-and-search\n invoke the searchable pager on completion options (for\n convenience, this also moves backwards in the completion\n pager)\n\ndelete-char\n delete one character to the right of the cursor\n\ndelete-or-exit\n delete one character to the right of the cursor, or exit the\n shell if the commandline is empty\n\ndown-line\n move down one line\n\ndowncase-word\n make the current word lowercase\n\nend-of-buffer\n moves to the end of the buffer, i.e. the end of the first line\n\nend-of-history\n move to the end of the history\n\nend-of-line\n move to the end of the line\n\nend-selection\n end selecting text\n\nexpand-abbr\n expands any abbreviation currently under the cursor\n\nexecute\n run the current commandline\n\nexit exit the shell\n\nforward-bigword\n move one whitespace-delimited word to the right\n\nforward-char\n move one character to the right; or if at the end of the\n commandline, accept the current autosuggestion. If the\n completion pager is active, select the next completion\n instead.\n\nforward-single-char\n move one character to the right; or if at the end of the\n commandline, accept a single char from the current\n autosuggestion.\n\nforward-word\n move one word to the right; or if at the end of the\n commandline, accept one word from the current autosuggestion.\n\nhistory-pager\n invoke the searchable pager on history (incremental search);\n or if the history pager is already active, search further\n backwards in time.\n\nhistory-pager-delete\n permanently delete the history item selected in the history\n pager\n\nhistory-search-backward\n search the history for the previous match\n\nhistory-search-forward\n search the history for the next match\n\nhistory-prefix-search-backward\n search the history for the previous prefix match\n\nhistory-prefix-search-forward\n search the history for the next prefix match\n\nhistory-token-search-backward\n search the history for the previous matching argument\n\nhistory-token-search-forward\n search the history for the next matching argument\n\nforward-jump and backward-jump\n read another character and jump to its next occurence\n after/before the cursor\n\nforward-jump-till and backward-jump-till\n jump to right before the next occurence\n\nrepeat-jump and repeat-jump-reverse\n redo the last jump in the same/opposite direction\n\nkill-bigword\n move the next whitespace-delimited word to the killring\n\nkill-line\n move everything from the cursor to the end of the line to the\n killring\n\nkill-selection\n move the selected text to the killring\n\nkill-whole-line\n move the line (including the following newline) to the\n killring. If the line is the last line, its preceeding newline\n is also removed\n\nkill-inner-line\n move the line (without the following newline) to the killring\n\nkill-word\n move the next word to the killring\n\nnextd-or-forward-word\n if the commandline is empty, then move forward in the\n directory history, otherwise move one word to the right; or if\n at the end of the commandline, accept one word from the\n current autosuggestion.\n\nor only execute the next function if the previous did not succeed\n (note: only some functions report failure)\n\npager-toggle-search\n toggles the search field if the completions pager is visible;\n or if used after history-pager, search forwards in time.\n\nprevd-or-backward-word\n if the commandline is empty, then move backward in the\n directory history, otherwise move one word to the left\n\nrepaint\n reexecutes the prompt functions and redraws the prompt (also\n force-repaint for backwards-compatibility)\n\nrepaint-mode\n reexecutes the fish_mode_prompt and redraws the prompt. This\n is useful for vi-mode. If no fish_mode_prompt exists or it\n prints nothing, it acts like a normal repaint.\n\nself-insert\n inserts the matching sequence into the command line\n\nself-insert-notfirst\n inserts the matching sequence into the command line, unless\n the cursor is at the beginning\n\nsuppress-autosuggestion\n remove the current autosuggestion. Returns true if there was a\n suggestion to remove.\n\nswap-selection-start-stop\n go to the other end of the highlighted text without changing\n the selection\n\ntranspose-chars\n transpose two characters to the left of the cursor\n\ntranspose-words\n transpose two words to the left of the cursor\n\ntogglecase-char\n toggle the capitalisation (case) of the character under the\n cursor\n\ntogglecase-selection\n toggle the capitalisation (case) of the selection\n\ninsert-line-under\n add a new line under the current line\n\ninsert-line-over\n add a new line over the current line\n\nup-line\n move up one line\n\nundo and redo\n revert or redo the most recent edits on the command line\n\nupcase-word\n make the current word uppercase\n\nyank insert the latest entry of the killring into the buffer\n\nyank-pop\n rotate to the previous entry of the killring\n\nADDITIONAL FUNCTIONS\nThe following functions are included as normal functions, but are\nparticularly useful for input editing:\n\nup-or-search and down-or-search\n move the cursor or search the history depending on the cursor\n position and current mode\n\nedit_command_buffer\n open the visual editor (controlled by the VISUAL or EDITOR\n environment variables) with the current command-line contents\n\nfish_clipboard_copy\n copy the current selection to the system clipboard\n\nfish_clipboard_paste\n paste the current selection from the system clipboard before\n the cursor\n\nfish_commandline_append\n append the argument to the command-line. If the command-line\n already ends with the argument, this removes the suffix\n instead. Starts with the last command from history if the\n command-line is empty.\n\nfish_commandline_prepend\n prepend the argument to the command-line. If the command-line\n already starts with the argument, this removes the prefix\n instead. Starts with the last command from history if the\n command-line is empty.\n\nEXAMPLES\nExit the shell when Control+D is pressed:\n\n bind \\cd 'exit'\n\nPerform a history search when Page Up is pressed:\n\n bind -k ppage history-search-backward\n\nTurn on Vi key bindings and rebind Control+C to clear the input line:\n\n set -g fish_key_bindings fish_vi_key_bindings\n bind -M insert \\cc kill-whole-line repaint\n\nLaunch git diff and repaint the commandline afterwards when Control+G\nis pressed:\n\n bind \\cg 'git diff; commandline -f repaint'\n\nTERMINAL LIMITATIONS\nUnix terminals, like the ones fish operates in, are at heart 70s\ntechnology. They have some limitations that applications running\ninside them can't workaround.\n\nFor instance, the control key modifies a character by setting the top\nthree bits to 0. This means:\n\n• Many characters + control are indistinguishable from other keys.\n Control+I is tab, Control+J is newline (\\n).\n\n• Control and shift don't work simultaneously\n\nOther keys don't have a direct encoding, and are sent as escape\nsequences. For example → (Right) often sends \\e\\[C. These can differ\nfrom terminal to terminal, and the mapping is typically available in\nterminfo(5). Sometimes however a terminal identifies as e.g.\nxterm-256color for compatibility, but then implements xterm's\nsequences incorrectly.\n\nSPECIAL CASE: THE ESCAPE CHARACTER\nThe escape key can be used standalone, for example, to switch from\ninsertion mode to normal mode when using Vi keybindings. Escape can\nalso be used as a \"meta\" key, to indicate the start of an escape\nsequence, like for function or arrow keys. Custom bindings can also\nbe defined that begin with an escape character.\n\nHolding alt and something else also typically sends escape, for\nexample holding alt+a will send an escape character and then an \"a\".\n\nfish waits for a period after receiving the escape character, to\ndetermine whether it is standalone or part of an escape sequence.\nWhile waiting, additional key presses make the escape key behave as a\nmeta key. If no other key presses come in, it is handled as a\nstandalone escape. The waiting period is set to 30 milliseconds (0.03\nseconds). It can be configured by setting the fish_escape_delay_ms\nvariable to a value between 10 and 5000 ms. This can be a universal\nvariable that you set once from an interactive session. So the\nescape character has its own timeout configured with\nfish_escape_delay_ms.\n\nSee also Key sequences.", + "args": "bind [(-M | --mode) MODE] [(-m | --sets-mode) NEW_MODE] [--preset | --user] [-s | --silent] [-k | --key] SEQUENCE COMMAND ...\nbind [(-M | --mode) MODE] [-k | --key] [--preset] [--user] SEQUENCE\nbind (-K | --key-names) [-a | --all] [--preset] [--user]\nbind (-f | --function-names)\nbind (-L | --list-modes)\nbind (-e | --erase) [(-M | --mode) MODE] [--preset] [--user] [-a | --all] | [-k | --key] SEQUENCE ..." + }, + "block": { + "shortDescription": "temporarily block delivery of events", + "description": "block prevents events triggered by fish or the emit command from\nbeing delivered and acted upon while the block is in place.\n\nIn functions, block can be useful while performing work that should\nnot be interrupted by the shell.\n\nThe block can be removed. Any events which triggered while the block\nwas in place will then be delivered.\n\nEvent blocks should not be confused with code blocks, which are\ncreated with begin, if, while or for\n\nWithout options, the block command acts with function scope.\n\nThe following options are available:\n\n-l or --local\n Release the block automatically at the end of the current\n innermost code block scope.\n\n-g or --global\n Never automatically release the lock.\n\n-e or --erase\n Release global block.\n\n-h or --help\n Displays help about using this command.\n\nEXAMPLE\n\n # Create a function that listens for events\n function --on-event foo foo; echo 'foo fired'; end\n\n # Block the delivery of events\n block -g\n\n emit foo\n # No output will be produced\n\n block -e\n # 'foo fired' will now be printed\n\nNOTES\nEvents are only received from the current fish process as there is no\nway to send events from one fish process to another (yet).", + "args": "block [(--local | --global)]\nblock --erase" + }, + "break": { + "shortDescription": "Exit the current loop", + "description": "Terminate the execution of the nearest enclosing `while` or `for` loop and proceed with the next command after the loop." + }, + "breakpoint": { + "shortDescription": "Launch debug mode", + "description": "Pause execution and launch an interactive debug prompt. This is useful for inspecting the state of a script at a specific point." + }, + "builtin": { + "shortDescription": "run a builtin command", + "description": "builtin forces the shell to use a builtin command named BUILTIN,\nrather than a function or external program.\n\nThe following options are available:\n\n-n or --names\n Lists the names of all defined builtins.\n\n-q or --query BUILTIN\n Tests if any of the specified builtins exist. If any exist, it\n returns 0, 1 otherwise.\n\n-h or --help\n Displays help about using this command.\n\nEXAMPLE\n\n builtin jobs\n # executes the jobs builtin, even if a function named jobs exists", + "args": "builtin [OPTIONS] BUILTINNAME\nbuiltin --query BUILTINNAME ...\nbuiltin --names" + }, + "case": { + "shortDescription": "Match a value against patterns", + "description": "Within a `switch` block, the `case` command specifies patterns to match against the given value, executing the associated block if a match is found.", + "args": "PATTERN..." + }, + "cd": { + "shortDescription": "change directory", + "description": "cd changes the current working directory.\n\nIf DIRECTORY is given, it will become the new directory. If no\nparameter is given, the HOME environment variable will be used.\n\nIf DIRECTORY is a relative path, all the paths in the CDPATH will be\ntried as prefixes for it, in addition to PWD. It is recommended to\nkeep . as the first element of CDPATH, or PWD will be tried last.\n\nFish will also try to change directory if given a command that looks\nlike a directory (starting with ., / or ~, or ending with /), without\nexplicitly requiring cd.\n\nFish also ships a wrapper function around the builtin cd that\nunderstands cd - as changing to the previous directory. See also\nprevd. This wrapper function maintains a history of the 25 most\nrecently visited directories in the $dirprev and $dirnext global\nvariables. If you make those universal variables your cd history is\nshared among all fish instances.\n\nAs a special case, cd . is equivalent to cd $PWD, which is useful in\ncases where a mountpoint has been recycled or a directory has been\nremoved and recreated.\n\nThe --help or -h option displays help about using this command, and\ndoes not change the directory.\n\nEXAMPLES\n\n cd\n # changes the working directory to your home directory.\n\n cd /usr/src/fish-shell\n # changes the working directory to /usr/src/fish-shell\n\nSEE ALSO\nNavigate directories using the directory history or the directory\nstack", + "args": "cd [DIRECTORY]" + }, + "command": { + "shortDescription": "run a program", + "description": "command forces the shell to execute the program COMMANDNAME and\nignore any functions or builtins with the same name.\n\nThe following options are available:\n\n-a or --all\n Prints all COMMAND found in PATH, in the order found.\n\n-q or --query\n Silence output and print nothing, setting only exit status.\n Implies --search. For compatibility, this is also --quiet\n (deprecated).\n\n-v (or -s or --search)\n Prints the external command that would be executed, or prints\n nothing if no file with the specified name could be found in\n PATH.\n\n-h or --help\n Displays help about using this command.\n\nWith the -v option, command treats every argument as a separate\ncommand to look up and sets the exit status to 0 if any of the\nspecified commands were found, or 127 if no commands could be found.\n--quiet used with -v prevents commands being printed, like type -q.\n\nEXAMPLES\ncommand ls executes the ls program, even if an ls function also exists.\ncommand -s ls prints the path to the ls program.\ncommand -q git; and command git log runs git log only if git exists.", + "args": "command [OPTIONS] [COMMANDNAME [ARG ...]]" + }, + "commandline": { + "shortDescription": "set or get the current command line buffer", + "description": "commandline can be used to set or get the current contents of the\ncommand line buffer.\n\nWith no parameters, commandline returns the current value of the\ncommand line.\n\nWith CMD specified, the command line buffer is erased and replaced\nwith the contents of CMD.\n\nThe following options are available:\n\n-C or --cursor\n Set or get the current cursor position, not the contents of\n the buffer. If no argument is given, the current cursor\n position is printed, otherwise the argument is interpreted as\n the new cursor position. If one of the options -j, -p or -t\n is given, the position is relative to the respective substring\n instead of the entire command line buffer.\n\n-B or --selection-start\n Get current position of the selection start in the buffer.\n\n-E or --selection-end\n Get current position of the selection end in the buffer.\n\n-f or --function\n Causes any additional arguments to be interpreted as input\n functions, and puts them into the queue, so that they will be\n read before any additional actual key presses are. This\n option cannot be combined with any other option. See bind for\n a list of input functions.\n\n-h or --help\n Displays help about using this command.\n\nThe following options change the way commandline updates the command\nline buffer:\n\n-a or --append\n Do not remove the current commandline, append the specified\n string at the end of it.\n\n-i or --insert\n Do not remove the current commandline, insert the specified\n string at the current cursor position\n\n-r or --replace\n Remove the current commandline and replace it with the\n specified string (default)\n\nThe following options change what part of the commandline is printed\nor updated:\n\n-b or --current-buffer\n Select the entire commandline, not including any displayed\n autosuggestion (default).\n\n-j or --current-job\n Select the current job - a job here is one pipeline. Stops at\n logical operators or terminators (;, &, and newlines).\n\n-p or --current-process\n Select the current process - a process here is one command.\n Stops at logical operators, terminators, and pipes.\n\n-s or --current-selection\n Selects the current selection\n\n-t or --current-token\n Selects the current token\n\nThe following options change the way commandline prints the current\ncommandline buffer:\n\n-c or --cut-at-cursor\n Only print selection up until the current cursor position. If\n combined with --tokenize, this will print up until the last\n completed token - excluding the token the cursor is in. This\n is typically what you would want for instance in completions.\n To get both, use both commandline --cut-at-cursor --tokenize;\n commandline --cut-at-cursor --current-token, or commandline\n -co; commandline -ct for short.\n\n-o or --tokenize\n Tokenize the selection and print one string-type token per\n line.\n\nIf commandline is called during a call to complete a given string\nusing complete -C STRING, commandline will consider the specified\nstring to be the current contents of the command line.\n\nThe following options output metadata about the commandline state:\n\n-L or --line\n Print the line that the cursor is on, with the topmost line\n starting at 1.\n\n-S or --search-mode\n Evaluates to true if the commandline is performing a history\n search.\n\n-P or --paging-mode\n Evaluates to true if the commandline is showing pager\n contents, such as tab completions.\n\n--paging-full-mode\n Evaluates to true if the commandline is showing pager\n contents, such as tab completions and all lines are shown (no\n \" more rows\" message).\n\n--is-valid\n Returns true when the commandline is syntactically valid and\n complete. If it is, it would be executed when the execute\n bind function is called. If the commandline is incomplete,\n return 2, if erroneus, return 1.\n\nEXAMPLE\ncommandline -j $history[3] replaces the job under the cursor with the\nthird item from the command line history.\n\nIf the commandline contains\n\n > echo $flounder >&2 | less; and echo $catfish\n\n(with the cursor on the \"o\" of \"flounder\")\n\nThe echo $flounder >& is the first process, less the second and and\necho $catfish the third.\n\necho $flounder >&2 | less is the first job, and echo $catfish the\nsecond.\n\n$flounder is the current token.\n\nThe most common use for something like completions is\n\n set -l tokens (commandline -opc)\n\nwhich gives the current process (what is being completed), tokenized\ninto separate entries, up to but excluding the currently being\ncompleted token\n\nIf you are then also interested in the in-progress token, add\n\n:: set -l current (commandline -ct)\n\nNote that this makes it easy to render fish's infix matching moot -\nif possible it's best if the completions just print all possibilities\nand leave the matching to the current token up to fish's logic.\n\nMore examples:\n\n > commandline -t\n $flounder\n > commandline -ct\n $fl\n > commandline -b # or just commandline\n echo $flounder >&2 | less; and echo $catfish\n > commandline -p\n echo $flounder >&2\n > commandline -j\n echo $flounder >&2 | less", + "args": "commandline [OPTIONS] [CMD]" + }, + "complete": { + "shortDescription": "edit command-specific tab-completions", + "description": "complete defines, removes or lists completions for a command.\n\nFor an introduction to writing your own completions, see Writing your\nown completions in the fish manual.\n\nThe following options are available:\n\n-c or --command COMMAND\n Specifies that COMMAND is the name of the command. If there is\n no -c or -p, one non-option argument will be used as the\n command.\n\n-p or --path COMMAND\n Specifies that COMMAND is the absolute path of the command\n (optionally containing wildcards).\n\n-e or --erase\n Deletes the specified completion.\n\n-s or --short-option SHORT_OPTION\n Adds a short option to the completions list.\n\n-l or --long-option LONG_OPTION\n Adds a GNU-style long option to the completions list.\n\n-o or --old-option OPTION\n Adds an old-style short or long option (see below for\n details).\n\n-a or --arguments ARGUMENTS\n Adds the specified option arguments to the completions list.\n\n-k or --keep-order\n Keeps the order of ARGUMENTS instead of sorting\n alphabetically. Multiple complete calls with -k result in\n arguments of the later ones displayed first.\n\n-f or --no-files\n This completion may not be followed by a filename.\n\n-F or --force-files\n This completion may be followed by a filename, even if another\n applicable complete specified --no-files.\n\n-r or --require-parameter\n This completion must have an option argument, i.e. may not be\n followed by another option.\n\n-x or --exclusive\n Short for -r and -f.\n\n-w or --wraps WRAPPED_COMMAND\n Causes the specified command to inherit completions from\n WRAPPED_COMMAND (see below for details).\n\n-n or --condition CONDITION\n This completion should only be used if the CONDITION (a shell\n command) returns 0. This makes it possible to specify\n completions that should only be used in some cases. If\n multiple conditions are specified, fish will try them in the\n order they are specified until one fails or all succeeded.\n\n-C or --do-complete STRING\n Makes complete try to find all possible completions for the\n specified string. If there is no STRING, the current\n commandline is used instead.\n\n--escape\n When used with -C, escape special characters in completions.\n\n-h or --help\n Displays help about using this command.\n\nCommand-specific tab-completions in fish are based on the notion of\noptions and arguments. An option is a parameter which begins with a\nhyphen, such as -h, -help or --help. Arguments are parameters that do\nnot begin with a hyphen. Fish recognizes three styles of options, the\nsame styles as the GNU getopt library. These styles are:\n\n• Short options, like -a. Short options are a single character long,\n are preceded by a single hyphen and can be grouped together (like\n -la, which is equivalent to -l -a). Option arguments may be\n specified by appending the option with the value (-w32), or, if\n --require-parameter is given, in the following parameter (-w 32).\n\n• Old-style options, long like -Wall or -name or even short like -a.\n Old-style options can be more than one character long, are preceded\n by a single hyphen and may not be grouped together. Option\n arguments are specified by default following a space (-foo null) or\n after = (-foo=null).\n\n• GNU-style long options, like --colors. GNU-style long options can\n be more than one character long, are preceded by two hyphens, and\n can't be grouped together. Option arguments may be specified after\n a = (--quoting-style=shell), or, if --require-parameter is given,\n in the following parameter (--quoting-style shell).\n\nMultiple commands and paths can be given in one call to define the\nsame completions for multiple commands.\n\nMultiple command switches and wrapped commands can also be given to\ndefine multiple completions in one call.\n\nInvoking complete multiple times for the same command adds the new\ndefinitions on top of any existing completions defined for the\ncommand.\n\nWhen -a or --arguments is specified in conjunction with long, short,\nor old-style options, the specified arguments are only completed as\narguments for any of the specified options. If -a or --arguments is\nspecified without any long, short, or old-style options, the\nspecified arguments are used when completing non-option arguments to\nthe command (except when completing an option argument that was\nspecified with -r or --require-parameter).\n\nCommand substitutions found in ARGUMENTS should return a\nnewline-separated list of arguments, and each argument may optionally\nhave a tab character followed by the argument description.\nDescription given this way override a description given with -d or\n--description.\n\nDescriptions given with --description are also used to group options\ngiven with -s, -o or -l. Options with the same (non-empty)\ndescription will be listed as one candidate, and one of them will be\npicked. If the description is empty or no description was given this\nis skipped.\n\nThe -w or --wraps options causes the specified command to inherit\ncompletions from another command, \"wrapping\" the other command. The\nwrapping command can also have additional completions. A command can\nwrap multiple commands, and wrapping is transitive: if A wraps B, and\nB wraps C, then A automatically inherits all of C's completions.\nWrapping can be removed using the -e or --erase options. Wrapping\nonly works for completions specified with -c or --command and are\nignored when specifying completions with -p or --path.\n\nWhen erasing completions, it is possible to either erase all\ncompletions for a specific command by specifying complete -c COMMAND\n-e, or by specifying a specific completion option to delete.\n\nWhen complete is called without anything that would define or erase\ncompletions (options, arguments, wrapping, ...), it shows matching\ncompletions instead. So complete without any arguments shows all\nloaded completions, complete -c foo shows all loaded completions for\nfoo. Since completions are autoloaded, you will have to trigger them\nfirst.\n\nEXAMPLES\nThe short-style option -o for the gcc command needs a file argument:\n\n complete -c gcc -s o -r\n\nThe short-style option -d for the grep command requires one of read,\nskip or recurse:\n\n complete -c grep -s d -x -a \"read skip recurse\"\n\nThe su command takes any username as an argument. Usernames are given\nas the first colon-separated field in the file /etc/passwd. This can\nbe specified as:\n\n complete -x -c su -d \"Username\" -a \"(cat /etc/passwd | cut -d : -f 1)\"\n\nThe rpm command has several different modes. If the -e or --erase\nflag has been specified, rpm should delete one or more packages, in\nwhich case several switches related to deleting packages are valid,\nlike the nodeps switch.\n\nThis can be written as:\n\n complete -c rpm -n \"__fish_contains_opt -s e erase\" -l nodeps -d \"Don't check dependencies\"\n\nwhere __fish_contains_opt is a function that checks the command line\nbuffer for the presence of a specified set of options.\n\nTo implement an alias, use the -w or --wraps option:\n\n complete -c hub -w git\n\nNow hub inherits all of the completions from git. Note this can also\nbe specified in a function declaration (function thing -w\notherthing).\n\n complete -c git\n\nShows all completions for git.\n\nAny command foo that doesn't support grouping multiple short options\nin one string (not supporting -xf as short for -x -f) or a short\noption and its value in one string (not supporting -d9 instead of -d\n9) should be specified as a single-character old-style option instead\nof as a short-style option; for example, complete -c foo -o s;\ncomplete -c foo -o v would never suggest foo -ov but rather foo -o\n-v.", + "args": "complete ((-c | --command) | (-p | --path)) COMMAND [OPTIONS]\ncomplete (-C | --do-complete) [--escape] STRING" + }, + "contains": { + "shortDescription": "test if a word is present in a list", + "description": "contains tests whether the set VALUES contains the string KEY. If\nso, contains exits with code 0; if not, it exits with code 1.\n\nThe following options are available:\n\n-i or --index\n Print the index (number of the element in the set) of the\n first matching element.\n\n-h or --help\n Displays help about using this command.\n\nNote that contains interprets all arguments starting with a - as an\noption to contains, until an -- argument is reached.\n\nSee the examples below.\n\nEXAMPLE\nIf animals is a list of animals, the following will test if animals\ncontains \"cat\":\n\n if contains cat $animals\n echo Your animal list is evil!\n end\n\nThis code will add some directories to PATH if they aren't yet\nincluded:\n\n for i in ~/bin /usr/local/bin\n if not contains $i $PATH\n set PATH $PATH $i\n end\n end\n\nWhile this will check if function hasargs is being ran with the -q\noption:\n\n function hasargs\n if contains -- -q $argv\n echo '$argv contains a -q option'\n end\n end\n\nThe -- here stops contains from treating -q to an option to itself.\nInstead it treats it as a normal string to check.", + "args": "contains [OPTIONS] KEY [VALUES ...]" + }, + "continue": { + "shortDescription": "Skip to the next iteration of a loop", + "description": "Within a `while` or `for` loop, `continue` skips the remaining commands in the current iteration and proceeds to the next iteration of the loop." + }, + "count": { + "shortDescription": "", + "description": "" + }, + "disown": { + "shortDescription": "remove a process from the list of jobs", + "description": "disown removes the specified job from the list of jobs. The job\nitself continues to exist, but fish does not keep track of it any\nlonger.\n\nJobs in the list of jobs are sent a hang-up signal when fish\nterminates, which usually causes the job to terminate; disown allows\nthese processes to continue regardless.\n\nIf no process is specified, the most recently-used job is removed\n(like bg and fg). If one or more PIDs are specified, jobs with the\nspecified process IDs are removed from the job list. Invalid jobs are\nignored and a warning is printed.\n\nIf a job is stopped, it is sent a signal to continue running, and a\nwarning is printed. It is not possible to use the bg builtin to\ncontinue a job once it has been disowned.\n\ndisown returns 0 if all specified jobs were disowned successfully,\nand 1 if any problems were encountered.\n\nThe --help or -h option displays help about using this command.\n\nEXAMPLE\nfirefox &; disown will start the Firefox web browser in the\nbackground and remove it from the job list, meaning it will not be\nclosed when the fish process is closed.\n\ndisown (jobs -p) removes all jobs from the job list without\nterminating them.", + "args": "disown [PID ...]" + }, + "echo": { + "shortDescription": "", + "description": "" + }, + "else": { + "shortDescription": "Execute commands if the previous condition was false", + "description": "In an `if` block, the `else` section contains commands that execute if none of the preceding `if` or `else if` conditions were true." + }, + "emit": { + "shortDescription": "emit a generic event", + "description": "emit emits, or fires, an event. Events are delivered to, or caught\nby, special functions called event handlers. The arguments are passed\nto the event handlers as function arguments.\n\nThe --help or -h option displays help about using this command.\n\nEXAMPLE\nThe following code first defines an event handler for the generic\nevent named 'test_event', and then emits an event of that type.\n\n function event_test --on-event test_event\n echo event test: $argv\n end\n\n emit test_event something\n\nNOTES\nNote that events are only sent to the current fish process as there\nis no way to send events from one fish process to another.", + "args": "emit EVENT_NAME [ARGUMENTS ...]" + }, + "end": { + "shortDescription": "Terminate a block of code", + "description": "Conclude a block of code initiated by constructs like `if`, `switch`, `while`, `for`, or `function`." + }, + "eval": { + "shortDescription": "Execute arguments as a command", + "description": "Concatenate all arguments into a single command and execute it. This allows for dynamic construction and execution of commands.", + "args": "COMMAND..." + }, + "exec": { + "shortDescription": "execute command in current process", + "description": "exec replaces the currently running shell with a new command. On\nsuccessful completion, exec never returns. exec cannot be used inside\na pipeline.\n\nThe --help or -h option displays help about using this command.\n\nEXAMPLE\nexec emacs starts up the emacs text editor, and exits fish. When\nemacs exits, the session will terminate.", + "args": "exec COMMAND" + }, + "exit": { + "shortDescription": "exit the shell", + "description": "exit is a special builtin that causes the shell to exit. Either 255\nor the CODE supplied is used, whichever is lesser. Otherwise, the\nexit status will be that of the last command executed.\n\nIf exit is called while sourcing a file (using the source builtin)\nthe rest of the file will be skipped, but the shell itself will not\nexit.\n\nThe --help or -h option displays help about using this command.", + "args": "exit [CODE]" + }, + "false": { + "shortDescription": "Return an unsuccessful result", + "description": "A command that returns a non-zero exit status, indicating failure. It is often used in scripts to represent a false condition." + }, + "fg": { + "shortDescription": "bring job to foreground", + "description": "The fg builtin brings the specified job to the foreground, resuming\nit if it is stopped. While a foreground job is executed, fish is\nsuspended. If no job is specified, the last job to be used is put in\nthe foreground. If PID is specified, the job containing a process\nwith the specified process ID is put in the foreground.\n\nFor compatibility with other shells, job expansion syntax is\nsupported for fg. A PID of the format %1 will foreground job 1. Job\nnumbers can be seen in the output of jobs.\n\nThe --help or -h option displays help about using this command.\n\nEXAMPLE\nfg will put the last job in the foreground.\n\nfg %3 will put job 3 into the foreground.", + "args": "fg [PID]" + }, + "for": { + "shortDescription": "perform a set of commands multiple times", + "description": "for is a loop construct. It will perform the commands specified by\nCOMMANDS multiple times. On each iteration, the local variable\nspecified by VARNAME is assigned a new value from VALUES. If VALUES\nis empty, COMMANDS will not be executed at all. The VARNAME is\nvisible when the loop terminates and will contain the last value\nassigned to it. If VARNAME does not already exist it will be set in\nthe local scope. For our purposes if the for block is inside a\nfunction there must be a local variable with the same name. If the\nfor block is not nested inside a function then global and universal\nvariables of the same name will be used if they exist.\n\nMuch like set, for does not modify $status, but the evaluation of its\nsubordinate commands can.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\n\n for i in foo bar baz; echo $i; end\n\n # would output:\n foo\n bar\n baz\n\nNOTES\nThe VARNAME was local to the for block in releases prior to 3.0.0.\nThis means that if you did something like this:\n\n for var in a b c\n if break_from_loop\n break\n end\n end\n echo $var\n\nThe last value assigned to var when the loop terminated would not be\navailable outside the loop. What echo $var would write depended on\nwhat it was set to before the loop was run. Likely nothing.", + "args": "for VARNAME in [VALUES ...]; COMMANDS ...; end" + }, + "function": { + "shortDescription": "create a function", + "description": "function creates a new function NAME with the body BODY.\n\nA function is a list of commands that will be executed when the name\nof the function is given as a command.\n\nThe following options are available:\n\n-a NAMES or --argument-names NAMES\n Assigns the value of successive command-line arguments to the\n names given in NAMES. These are the same arguments given in\n argv, and are still available there. See also Argument\n Handling.\n\n-d DESCRIPTION or --description DESCRIPTION\n A description of what the function does, suitable as a\n completion description.\n\n-w WRAPPED_COMMAND or --wraps WRAPPED_COMMAND\n Inherit completions from the given WRAPPED_COMMAND. See the\n documentation for complete for more information.\n\n-e EVENT_NAME or --on-event EVENT_NAME\n Run this function when the specified named event is emitted.\n Fish internally generates named events, for example, when\n showing the prompt. Custom events can be emitted using the\n emit command.\n\n-v VARIABLE_NAME or --on-variable VARIABLE_NAME\n Run this function when the variable VARIABLE_NAME changes\n value. Note that fish makes no guarantees on any particular\n timing or even that the function will be run for every single\n set. Rather it will be run when the variable has been set at\n least once, possibly skipping some values or being run when\n the variable has been set to the same value (except for\n universal variables set in other shells - only changes in the\n value will be picked up for those).\n\n-j PID or --on-job-exit PID\n Run this function when the job containing a child process with\n the given process identifier PID exits. Instead of a PID, the\n string 'caller' can be specified. This is only allowed when in\n a command substitution, and will result in the handler being\n triggered by the exit of the job which created this command\n substitution.\n\n-p PID or --on-process-exit PID\n Run this function when the fish child process with process ID\n PID exits. Instead of a PID, for backward compatibility,\n \"%self\" can be specified as an alias for $fish_pid, and the\n function will be run when the current fish instance exits.\n\n-s SIGSPEC or --on-signal SIGSPEC\n Run this function when the signal SIGSPEC is delivered.\n SIGSPEC can be a signal number, or the signal name, such as\n SIGHUP (or just HUP). Note that the signal must have been\n delivered to fish; for example, Ctrl-C sends SIGINT to the\n foreground process group, which will not be fish if you are\n running another command at the time. Observing a signal will\n prevent fish from exiting in response to that signal.\n\n-S or --no-scope-shadowing\n Allows the function to access the variables of calling\n functions. Normally, any variables inside the function that\n have the same name as variables from the calling function are\n \"shadowed\", and their contents are independent of the calling\n function.\n\n It's important to note that this does not capture referenced\n variables or the scope at the time of function declaration! At\n this time, fish does not have any concept of closures, and\n variable lifetimes are never extended. In other words, by\n using --no-scope-shadowing the scope of the function each time\n it is run is shared with the scope it was called from rather\n than the scope it was defined in.\n\n-V or --inherit-variable NAME\n Snapshots the value of the variable NAME and defines a local\n variable with that same name and value when the function is\n defined. This is similar to a closure in other languages like\n Python but a bit different. Note the word \"snapshot\" in the\n first sentence. If you change the value of the variable after\n defining the function, even if you do so in the same scope\n (typically another function) the new value will not be used by\n the function you just created using this option. See the\n function notify example below for how this might be used.\n\nThe event handler switches (on-event, on-variable, on-job-exit,\non-process-exit and on-signal) cause a function to run automatically\nat specific events. New named events for --on-event can be fired\nusing the emit builtin. Fish already generates a few events, see\nEvent handlers for more.\n\nFunctions may not be named the same as a reserved keyword. These are\nelements of fish syntax or builtin commands which are essential for\nthe operations of the shell. Current reserved words are [, , and,\nargparse, begin, break, builtin, case, command, continue, else, end,\neval, exec, for, function, if, not, or, read, return, set, status,\nstring, switch, test, time, and while.\n\nEXAMPLE\n\n function ll\n ls -l $argv\n end\n\nwill run the ls command, using the -l option, while passing on any\nadditional files and switches to ls.\n\n function mkdir -d \"Create a directory and set CWD\"\n command mkdir $argv\n if test $status = 0\n switch $argv[(count $argv)]\n case '-*'\n\n case '*'\n cd $argv[(count $argv)]\n return\n end\n end\n end\n\nThis will run the mkdir command, and if it is successful, change the\ncurrent working directory to the one just created.\n\n function notify\n set -l job (jobs -l -g)\n or begin; echo \"There are no jobs\" >&2; return 1; end\n\n function _notify_job$job --on-job-exit $job --inherit-variable job\n echo -n \\a # beep\n functions -e _notify_job$job\n end\n end\n\nThis will beep when the most recent job completes.\n\nNOTES\nEvents are only received from the current fish process as there is no\nway to send events from one fish process to another.\n\nSEE MORE\nFor more explanation of how functions fit into fish, see Functions.", + "args": "function NAME [OPTIONS]; BODY; end" + }, + "functions": { + "shortDescription": "print or erase functions", + "description": "functions prints or erases functions.\n\nThe following options are available:\n\n-a or --all\n Lists all functions, even those whose name starts with an\n underscore.\n\n-c or --copy OLDNAME NEWNAME\n Creates a new function named NEWNAME, using the definition of\n the OLDNAME function.\n\n-d or --description DESCRIPTION\n Changes the description of this function.\n\n-e or --erase\n Causes the specified functions to be erased. This also means\n that it is prevented from autoloading in the current session.\n Use funcsave to remove the saved copy.\n\n-D or --details\n Reports the path name where the specified function is defined\n or could be autoloaded, stdin if the function was defined\n interactively or on the command line or by reading standard\n input, - if the function was created via source, and n/a if\n the function isn't available. (Functions created via alias\n will return -, because alias uses source internally.) If the\n --verbose option is also specified then five lines are\n written:\n\n • the pathname as already described,\n\n • autoloaded, not-autoloaded or n/a,\n\n • the line number within the file or zero if not applicable,\n\n • scope-shadowing if the function shadows the vars in the\n calling function (the normal case if it wasn't defined with\n --no-scope-shadowing), else no-scope-shadowing, or n/a if\n the function isn't defined,\n\n • the function description minimally escaped so it is a single\n line, or n/a if the function isn't defined or has no\n description.\n\n You should not assume that only five lines will be written\n since we may add additional information to the output in the\n future.\n\n--no-details\n Turns off function path reporting, so just the definition will\n be printed.\n\n-n or --names\n Lists the names of all defined functions.\n\n-q or --query\n Tests if the specified functions exist.\n\n-v or --verbose\n Make some output more verbose.\n\n-H or --handlers\n Show all event handlers.\n\n-t or --handlers-type TYPE\n Show all event handlers matching the given TYPE.\n\n-h or --help\n Displays help about using this command.\n\nThe default behavior of functions, when called with no arguments, is\nto print the names of all defined functions. Unless the -a option is\ngiven, no functions starting with underscores are included in the\noutput.\n\nIf any non-option parameters are given, the definition of the\nspecified functions are printed.\n\nCopying a function using -c copies only the body of the function, and\ndoes not attach any event notifications from the original function.\n\nOnly one function's description can be changed in a single invocation\nof functions -d.\n\nThe exit status of functions is the number of functions specified in\nthe argument list that do not exist, which can be used in concert\nwith the -q option.\n\nEXAMPLES\n\n functions -n\n # Displays a list of currently-defined functions\n\n functions -c foo bar\n # Copies the 'foo' function to a new function called 'bar'\n\n functions -e bar\n # Erases the function ``bar``\n\nSEE MORE\nFor more explanation of how functions fit into fish, see Functions.", + "args": "functions [-a | --all] [-n | --names]\nfunctions [-D | --details] [-v] FUNCTION\nfunctions -c OLDNAME NEWNAME\nfunctions -d DESCRIPTION FUNCTION\nfunctions [-e | -q] FUNCTION ..." + }, + "history": { + "shortDescription": "show and manipulate command history", + "description": "history is used to search, delete, and otherwise manipulate the\nhistory of interactive commands.\n\nThe following operations (sub-commands) are available:\n\nsearch Returns history items matching the search string. If no search\n string is provided it returns all history items. This is the\n default operation if no other operation is specified. You only\n have to explicitly say history search if you wish to search\n for one of the subcommands. The --contains search option will\n be used if you don't specify a different search option.\n Entries are ordered newest to oldest unless you use the\n --reverse flag. If stdout is attached to a tty the output will\n be piped through your pager by the history function. The\n history builtin simply writes the results to stdout.\n\ndelete Deletes history items. The --contains search option will be\n used if you don't specify a different search option. If you\n don't specify --exact a prompt will be displayed before any\n items are deleted asking you which entries are to be deleted.\n You can enter the word \"all\" to delete all matching entries.\n You can enter a single ID (the number in square brackets) to\n delete just that single entry. You can enter more than one ID,\n or an ID range separated by a space to delete multiple\n entries. Just press [enter] to not delete anything. Note that\n the interactive delete behavior is a feature of the history\n function. The history builtin only supports --exact\n --case-sensitive deletion.\n\nmerge Immediately incorporates history changes from other sessions.\n Ordinarily fish ignores history changes from sessions started\n after the current one. This command applies those changes\n immediately.\n\nsave Immediately writes all changes to the history file. The shell\n automatically saves the history file; this option is provided\n for internal use and should not normally need to be used by\n the user.\n\nclear Clears the history file. A prompt is displayed before the\n history is erased asking you to confirm you really want to\n clear all history unless builtin history is used.\n\nclear-session\n Clears the history file from all activity of the current\n session. Note: If history merge or builtin history merge is\n run in a session, only the history after this will be erased.\n\nThe following options are available:\n\nThese flags can appear before or immediately after one of the\nsub-commands listed above.\n\n-C or --case-sensitive\n Does a case-sensitive search. The default is case-insensitive.\n Note that prior to fish 2.4.0 the default was case-sensitive.\n\n-c or --contains\n Searches items in the history that contain the specified text\n string. This is the default for the --search flag. This is not\n currently supported by the delete subcommand.\n\n-e or --exact\n Searches or deletes items in the history that exactly match\n the specified text string. This is the default for the delete\n subcommand. Note that the match is case-insensitive by\n default. If you really want an exact match, including letter\n case, you must use the -C or --case-sensitive flag.\n\n-p or --prefix\n Searches items in the history that begin with the specified\n text string. This is not currently supported by the delete\n subcommand.\n\n-t or --show-time\n Prepends each history entry with the date and time the entry\n was recorded. By default it uses the strftime format # %c%n.\n You can specify another format; e.g., --show-time=\"%Y-%m-%d\n %H:%M:%S \" or --show-time=\"%a%I%p\". The short option, -t,\n doesn't accept a strftime format string; it only uses the\n default format. Any strftime format is allowed, including %s\n to get the raw UNIX seconds since the epoch.\n\n-z or --null\n Causes history entries written by the search operations to be\n terminated by a NUL character rather than a newline. This\n allows the output to be processed by read -z to correctly\n handle multiline history entries.\n\n-*NUMBER* -n NUMBER or --max NUMBER\n Limits the matched history items to the first NUMBER matching\n entries. This is only valid for history search.\n\n-R or --reverse\n Causes the history search results to be ordered oldest to\n newest. Which is the order used by most shells. The default is\n newest to oldest.\n\n-h or --help\n Displays help for this command.\n\nEXAMPLE\n\n history clear\n # Deletes all history items\n\n history search --contains \"foo\"\n # Outputs a list of all previous commands containing the string \"foo\".\n\n history delete --prefix \"foo\"\n # Interactively deletes commands which start with \"foo\" from the history.\n # You can select more than one entry by entering their IDs separated by a space.\n\nCUSTOMIZING THE NAME OF THE HISTORY FILE\nBy default interactive commands are logged to\n$XDG_DATA_HOME/fish/fish_history (typically\n~/.local/share/fish/fish_history).\n\nYou can set the fish_history variable to another name for the current\nshell session. The default value (when the variable is unset) is fish\nwhich corresponds to $XDG_DATA_HOME/fish/fish_history. If you set it\nto e.g. fun, the history would be written to\n$XDG_DATA_HOME/fish/fun_history. An empty string means history will\nnot be stored at all. This is similar to the private session features\nin web browsers.\n\nYou can change fish_history at any time (by using set -x fish_history\n\"session_name\") and it will take effect right away. If you set it to\n\"default\", it will use the default session name (which is \"fish\").\n\nOther shells such as bash and zsh use a variable named HISTFILE for a\nsimilar purpose. Fish uses a different name to avoid conflicts and\nsignal that the behavior is different (session name instead of a file\npath). Also, if you set the var to anything other than fish or\ndefault it will inhibit importing the bash history. That's because\nthe most common use case for this feature is to avoid leaking private\nor sensitive history when giving a presentation.\n\nNOTES\nIf you specify both --prefix and --contains the last flag seen is\nused.\n\nNote that for backwards compatibility each subcommand can also be\nspecified as a long option. For example, rather than history search\nyou can type history --search. Those long options are deprecated and\nwill be removed in a future release.", + "args": "history [search] [--show-time] [--case-sensitive]\n[--exact | --prefix | --contains] [--max N] [--null] [--reverse]\n [SEARCH_STRING ...]\nhistory delete [--case-sensitive]\n [--exact | --prefix | --contains] SEARCH_STRING ...\nhistory merge\nhistory save\nhistory clear\nhistory clear-session" + }, + "if": { + "shortDescription": "conditionally execute a command", + "description": "if will execute the command CONDITION. If the condition's exit status\nis 0, the commands COMMANDS_TRUE will execute. If the exit status is\nnot 0 and else is given, COMMANDS_FALSE will be executed.\n\nYou can use and or or in the condition. See the second example below.\n\nThe exit status of the last foreground command to exit can always be\naccessed using the $status variable.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nThe following code will print foo.txt exists if the file foo.txt\nexists and is a regular file, otherwise it will print bar.txt exists\nif the file bar.txt exists and is a regular file, otherwise it will\nprint foo.txt and bar.txt do not exist.\n\n if test -f foo.txt\n echo foo.txt exists\n else if test -f bar.txt\n echo bar.txt exists\n else\n echo foo.txt and bar.txt do not exist\n end\n\nThe following code will print \"foo.txt exists and is readable\" if\nfoo.txt is a regular file and readable\n\n if test -f foo.txt\n and test -r foo.txt\n echo \"foo.txt exists and is readable\"\n end", + "args": "if CONDITION; COMMANDS_TRUE ...;\n[else if CONDITION2; COMMANDS_TRUE2 ...;]\n[else; COMMANDS_FALSE ...;]\nend" + }, + "jobs": { + "shortDescription": "print currently running jobs", + "description": "jobs prints a list of the currently running jobs and their status.\n\njobs accepts the following options:\n\n-c or --command\n Prints the command name for each process in jobs.\n\n-g or --group\n Only prints the group ID of each job.\n\n-l or --last\n Prints only the last job to be started.\n\n-p or --pid\n Prints the process ID for each process in all jobs.\n\n-q or --query\n Prints no output for evaluation of jobs by exit status only.\n For compatibility with old fish versions this is also --quiet\n (but this is deprecated).\n\n-h or --help\n Displays help about using this command.\n\nOn systems that support this feature, jobs will print the CPU usage\nof each job since the last command was executed. The CPU usage is\nexpressed as a percentage of full CPU activity. Note that on\nmultiprocessor systems, the total activity may be more than 100%.\n\nArguments of the form PID or %JOBID restrict the output to jobs with\nthe selected process identifiers or job numbers respectively.\n\nIf the output of jobs is redirected or if it is part of a command\nsubstitution, the column header that is usually printed is omitted,\nmaking it easier to parse.\n\nThe exit status of jobs is 0 if there are running background jobs and\n1 otherwise.\n\nEXAMPLE\njobs outputs a summary of the current jobs, such as two long-running\ntasks in this example:\n\n Job Group State Command\n 2 26012 running nc -l 55232 < /dev/random &\n 1 26011 running python tests/test_11.py &", + "args": "jobs [OPTIONS] [PID | %JOBID]" + }, + "math": { + "shortDescription": "perform mathematics calculations", + "description": "math performs mathematical calculations. It supports simple\noperations such as addition, subtraction, and so on, as well as\nfunctions like abs(), sqrt() and ln().\n\nBy default, the output shows up to 6 decimal places. To change the\nnumber of decimal places, use the --scale option, including --scale=0\nfor integer output. Trailing zeroes will always be trimmed.\n\nKeep in mind that parameter expansion happens before expressions are\nevaluated. This can be very useful in order to perform calculations\ninvolving shell variables or the output of command substitutions, but\nit also means that parenthesis (()) and the asterisk (*) glob\ncharacter have to be escaped or quoted. x can also be used to denote\nmultiplication, but it needs to be followed by whitespace to\ndistinguish it from hexadecimal numbers.\n\nParentheses for functions are optional - math sin pi prints 0.\nHowever, a comma will bind to the inner function, so math pow sin 3,\n5 is an error because it tries to give sin the arguments 3 and 5.\nWhen in doubt, use parentheses.\n\nmath ignores whitespace between arguments and takes its input as\nmultiple arguments (internally joined with a space), so math 2 +2 and\nmath \"2 + 2\" work the same. math 2 2 is an error.\n\nThe following options are available:\n\n-s N or --scale N\n Sets the scale of the result. N must be an integer or the\n word \"max\" for the maximum scale. A scale of zero causes\n results to be truncated, not rounded. Any non-integer\n component is thrown away. So 3/2 returns 1 rather than 2\n which 1.5 would normally round to. This is for compatibility\n with bc which was the basis for this command prior to fish\n 3.0.0. Scale values greater than zero causes the result to be\n rounded using the usual rules to the specified number of\n decimal places.\n\n-b BASE or --base BASE\n Sets the numeric base used for output (math always understands\n hexadecimal numbers as input). It currently understands \"hex\"\n or \"16\" for hexadecimal and \"octal\" or \"8\" for octal and\n implies a scale of 0 (other scales cause an error), so it will\n truncate the result down to an integer. This might change in\n the future. Hex numbers will be printed with a 0x prefix.\n Octal numbers will have a prefix of 0 but aren't understood by\n math as input.\n\n-h or --help\n Displays help about using this command.\n\nRETURN VALUES\nIf the expression is successfully evaluated and doesn't\nover/underflow or return NaN the return status is zero (success) else\none.\n\nSYNTAX\nmath knows some operators, constants, functions and can (obviously)\nread numbers.\n\nFor numbers, . is always the radix character regardless of locale -\n2.5, not 2,5. Scientific notation (10e5) and hexadecimal (0xFF) are\nalso available.\n\nmath allows you to use underscores as visual separators for digit\ngrouping. For example, you can write 1_000_000, 0x_89_AB_CD_EF, and\n1.234_567_e89.\n\nOPERATORS\nmath knows the following operators:\n\n+ for addition\n\n- for subtraction\n\n* or x for multiplication. * is the glob character and needs to be\n quoted or escaped, x needs to be followed by whitespace or it\n looks like 0x hexadecimal notation.\n\n/ for division\n\n^ for exponentiation\n\n% for modulo\n\n( or ) for grouping. These need to be quoted or escaped because ()\n denotes a command substitution.\n\nThey are all used in an infix manner - 5 + 2, not + 5 2.\n\nCONSTANTS\nmath knows the following constants:\n\ne Euler's number\n\npi π, you know this one. Half of Tau\n\ntau Equivalent to 2π, or the number of radians in a circle\n\nUse them without a leading $ - pi - 3 should be about 0.\n\nFUNCTIONS\nmath supports the following functions:\n\nabs the absolute value, with positive sign\n\nacos arc cosine\n\nasin arc sine\n\natan arc tangent\n\natan2 arc tangent of two variables\n\nbitand, bitor and bitxor\n perform bitwise operations. These will throw away any\n non-integer parts and interpret the rest as an int.\n\n Note: bitnot and bitnand don't exist. This is because numbers\n in math don't really have a width in terms of bits, and these\n operations necessarily care about leading zeroes.\n\n If you need to negate a specific number you can do it with an\n xor with a mask, e.g.:\n\n > math --base=hex bitxor 0x0F, 0xFF\n 0xF0\n\n > math --base=hex bitxor 0x2, 0x3\n # Here we mask with 0x3 == 0b111, so our number is 3 bits wide\n # Only the 1 bit isn't set.\n 0x1\n\nceil round number up to the nearest integer\n\ncos the cosine\n\ncosh hyperbolic cosine\n\nexp the base-e exponential function\n\nfac factorial - also known as x! (x * (x - 1) * (x - 2) * ... * 1)\n\nfloor round number down to the nearest integer\n\nln the base-e logarithm\n\nlog or log10\n the base-10 logarithm\n\nlog2 the base-2 logarithm\n\nmax returns the largest of the given numbers - this takes an\n arbitrary number of arguments (but at least one)\n\nmin returns the smallest of the given numbers - this takes an\n arbitrary number of arguments (but at least one)\n\nncr \"from n choose r\" combination function - how many subsets of\n size r can be taken from n (order doesn't matter)\n\nnpr the number of subsets of size r that can be taken from a set\n of n elements (including different order)\n\npow(x,y)\n returns x to the y (and can be written as x ^ y)\n\nround rounds to the nearest integer, away from 0\n\nsin the sine function\n\nsinh the hyperbolic sine\n\nsqrt the square root - (can also be written as x ^ 0.5)\n\ntan the tangent\n\ntanh the hyperbolic tangent\n\nAll of the trigonometric functions use radians (the pi-based scale,\nnot 360°).\n\nEXAMPLES\nmath 1+1 outputs 2.\n\nmath $status - 128 outputs the numerical exit status of the last\ncommand minus 128.\n\nmath 10 / 6 outputs 1.666667.\n\nmath -s0 10.0 / 6.0 outputs 1.\n\nmath -s3 10 / 6 outputs 1.666.\n\nmath \"sin(pi)\" outputs 0.\n\nmath 5 \\* 2 or math \"5 * 2\" or math 5 \"*\" 2 all output 10.\n\nmath 0xFF outputs 255, math 0 x 3 outputs 0 (because it computes 0\nmultiplied by 3).\n\nmath bitand 0xFE, 0x2e outputs 46.\n\nmath \"bitor(9,2)\" outputs 11.\n\nmath --base=hex 192 prints 0xc0.\n\nmath 'ncr(49,6)' prints 13983816 - that's the number of possible\npicks in 6-from-49 lotto.\n\nmath max 5,2,3,1 prints 5.\n\nCOMPATIBILITY NOTES\nFish 1.x and 2.x releases relied on the bc command for handling math\nexpressions. Starting with fish 3.0.0 fish uses the tinyexpr library\nand evaluates the expression without the involvement of any external\ncommands.\n\nYou don't need to use -- before the expression, even if it begins\nwith a minus sign which might otherwise be interpreted as an invalid\noption. If you do insert -- before the expression, it will cause\noption scanning to stop just like for every other command and it\nwon't be part of the expression.", + "args": "math [(-s | --scale) N] [(-b | --base) BASE] EXPRESSION ..." + }, + "not": { + "shortDescription": "negate the exit status of a job", + "description": "not negates the exit status of another command. If the exit status is\nzero, not returns 1. Otherwise, not returns 0.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nThe following code reports an error and exits if no file named spoon\ncan be found.\n\n if not test -f spoon\n echo There is no spoon\n exit 1\n end", + "args": "not COMMAND [OPTIONS ...]" + }, + "or": { + "shortDescription": "conditionally execute a command", + "description": "or is used to execute a command if the previous command was not\nsuccessful (returned a status of something other than 0).\n\nor statements may be used as part of the condition in an if or while\nblock.\n\nor does not change the current exit status itself, but the command it\nruns most likely will. The exit status of the last foreground command\nto exit can always be accessed using the $status variable.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nThe following code runs the make command to build a program. If the\nbuild succeeds, the program is installed. If either step fails, make\nclean is run, which removes the files created by the build process.\n\n make; and make install; or make clean\n\nSEE ALSO\n\n• and command", + "args": "COMMAND1; or COMMAND2" + }, + "path": { + "shortDescription": "manipulate and check paths", + "description": "path performs operations on paths.\n\nPATH arguments are taken from the command line unless standard input\nis connected to a pipe or a file, in which case they are read from\nstandard input, one PATH per line. It is an error to supply PATH\narguments on both the command line and on standard input.\n\nArguments starting with - are normally interpreted as switches; --\ncauses the following arguments not to be treated as switches even if\nthey begin with -. Switches and required arguments are recognized\nonly on the command line.\n\nWhen a path starts with -, path filter and path normalize will\nprepend ./ on output to avoid it being interpreted as an option\notherwise, so it's safe to pass path's output to other commands that\ncan handle relative paths.\n\nAll subcommands accept a -q or --quiet switch, which suppresses the\nusual output but exits with the documented status. In this case these\ncommands will quit early, without reading all of the available input.\n\nAll subcommands also accept a -Z or --null-out switch, which makes\nthem print output separated with NUL instead of newlines. This is for\nfurther processing, e.g. passing to another path, or xargs -0. This\nis not recommended when the output goes to the terminal or a command\nsubstitution.\n\nAll subcommands also accept a -z or --null-in switch, which makes\nthem accept arguments from stdin separated with NULL-bytes. Since\nUnix paths can't contain NULL, that makes it possible to handle all\npossible paths and read input from e.g. find -print0. If arguments\nare given on the commandline this has no effect. This should mostly\nbe unnecessary since path automatically starts splitting on NULL if\none appears in the first PATH_MAX bytes, PATH_MAX being the operating\nsystem's maximum length for a path plus a NULL byte.\n\nSome subcommands operate on the paths as strings and so work on\nnonexistent paths, while others need to access the paths themselves\nand so filter out nonexistent paths.\n\nThe following subcommands are available.\n\nBASENAME SUBCOMMAND\n\n path basename [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...]\n\npath basename returns the last path component of the given path, by\nremoving the directory prefix and removing trailing slashes. In other\nwords, it is the part that is not the dirname. For files you might\ncall it the \"filename\".\n\nIt returns 0 if there was a basename, i.e. if the path wasn't empty\nor just slashes.\n\n Examples\n\n > path basename ./foo.mp4\n foo.mp4\n\n > path basename ../banana\n banana\n\n > path basename /usr/bin/\n bin\n\n > path basename /usr/bin/*\n # This prints all files in /usr/bin/\n # A selection:\n cp\n fish\n grep\n rm\n\nDIRNAME SUBCOMMAND\n\n path dirname [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...]\n\npath dirname returns the dirname for the given path. This is the part\nbefore the last \"/\", discounting trailing slashes. In other words, it\nis the part that is not the basename (discounting superfluous\nslashes).\n\nIt returns 0 if there was a dirname, i.e. if the path wasn't empty or\njust slashes.\n\n Examples\n\n > path dirname ./foo.mp4\n .\n\n > path dirname ../banana\n ..\n\n > path dirname /usr/bin/\n /usr\n\nEXTENSION SUBCOMMAND\n\n path extension [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...]\n\npath extension returns the extension of the given path. This is the\npart after (and including) the last \".\", unless that \".\" followed a\n\"/\" or the basename is \".\" or \"..\", in which case there is no\nextension and an empty line is printed.\n\nIf the filename ends in a \".\", only a \".\" is printed.\n\nIt returns 0 if there was an extension.\n\n Examples\n\n > path extension ./foo.mp4\n .mp4\n\n > path extension ../banana\n # an empty line, status 1\n\n > path extension ~/.config\n # an empty line, status 1\n\n > path extension ~/.config.d\n .d\n\n > path extension ~/.config.\n .\n\n > set -l path (path change-extension '' ./foo.mp4)\n > set -l extension (path extension ./foo.mp4)\n > echo $path$extension\n # reconstructs the original path again.\n ./foo.mp4\n\nFILTER SUBCOMMAND\n\n path filter [-z | --null-in] [-Z | --null-out] [-q | --quiet] \\\n [-d] [-f] [-l] [-r] [-w] [-x] \\\n [-v | --invert] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH ...]\n\npath filter returns all of the given paths that match the given\nchecks. In all cases, the paths need to exist, nonexistent paths are\nalways filtered.\n\nThe available filters are:\n\n• -t or --type with the options: \"dir\", \"file\", \"link\", \"block\",\n \"char\", \"fifo\" and \"socket\", in which case the path needs to be a\n directory, file, link, block device, character device, named pipe\n or socket, respectively.\n\n• -d, -f and -l are short for --type=dir, --type=file and\n --type=link, respectively. There are no shortcuts for the other\n types.\n\n• -p or --perm with the options: \"read\", \"write\", and \"exec\", as well\n as \"suid\", \"sgid\", \"user\" (referring to the path owner) and \"group\"\n (referring to the path's group), in which case the path needs to\n have all of the given permissions for the current user.\n\n• -r, -w and -x are short for --perm=read, --perm=write and\n --perm=exec, respectively. There are no shortcuts for the other\n permissions.\n\nNote that the path needs to be any of the given types, but have all\nof the given permissions. This is because having a path that is both\nwritable and executable makes sense, but having a path that is both a\ndirectory and a file doesn't. Links will count as the type of the\nlinked-to file, so links to files count as files, links to\ndirectories count as directories.\n\nThe filter options can either be given as multiple options, or\ncomma-separated - path filter -t dir,file or path filter --type dir\n--type file are equivalent.\n\nWith --invert, the meaning of the filtering is inverted - any path\nthat wouldn't pass (including by not existing) passes, and any path\nthat would pass fails.\n\nWhen a path starts with -, path filter will prepend ./ to avoid it\nbeing interpreted as an option otherwise.\n\nIt returns 0 if at least one path passed the filter.\n\npath is is shorthand for path filter -q, i.e. just checking without\nproducing output, see The is subcommand.\n\n Examples\n\n > path filter /usr/bin /usr/argagagji\n # The (hopefully) nonexistent argagagji is filtered implicitly:\n /usr/bin\n\n > path filter --type file /usr/bin /usr/bin/fish\n # Only fish is a file\n /usr/bin/fish\n\n > path filter --type file,dir --perm exec,write /usr/bin/fish /home/me\n # fish is a file, which passes, and executable, which passes,\n # but probably not writable, which fails.\n #\n # $HOME is a directory and both writable and executable, typically.\n # So it passes.\n /home/me\n\n > path filter -fdxw /usr/bin/fish /home/me\n # This is the same as above: \"-f\" is \"--type=file\", \"-d\" is \"--type=dir\",\n # \"-x\" is short for \"--perm=exec\" and \"-w\" short for \"--perm=write\"!\n /home/me\n\n > path filter -fx $PATH/*\n # Prints all possible commands - the first entry of each name is what fish would execute!\n\nIS SUBCOMMAND\n\n path is [-z | --null-in] [-Z | --null-out] [-q | --quiet] \\\n [-d] [-f] [-l] [-r] [-w] [-x] \\\n [-v | --invert] [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH ...]\n\npath is is short for path filter -q. It returns true if any of the\ngiven files passes the filter, but does not produce any output.\n\n--quiet can still be passed for compatibility but is redundant. The\noptions are the same as for path filter.\n\n Examples\n\n > path is /usr/bin /usr/argagagji\n # /usr/bin exists, so this returns a status of 0 (true). It prints nothing.\n > path is /usr/argagagji\n # /usr/argagagji does not, so this returns a status of 1 (false). It also prints nothing.\n > path is -fx /bin/sh\n # /bin/sh is usually an executable file, so this returns true.\n\nMTIME SUBCOMMAND\n\n path mtime [-z | --null-in] [-Z | --null-out] [-q | --quiet] [-R | --relative] [PATH ...]\n\npath mtime returns the last modification time (\"mtime\" in unix\njargon) of the given paths, in seconds since the unix epoch (the\nbeginning of the 1st of January 1970).\n\nWith --relative (or -R), it prints the number of seconds since the\nmodification time. It only reads the current time once at start, so\nin case multiple paths are given the times are all relative to the\nstart of path mtime -R running.\n\nIf you want to know if a file is newer or older than another file,\nconsider using test -nt instead. See the test documentation.\n\nIt returns 0 if reading mtime for any path succeeded.\n\n Examples\n\n > date +%s\n # This prints the current time as seconds since the epoch\n 1657217847\n\n > path mtime /etc/\n 1657213796\n\n > path mtime -R /etc/\n 4078\n # So /etc/ on this system was last modified a little over an hour ago\n\n # This is the same as\n > math (date +%s) - (path mtime /etc/)\n\nNORMALIZE SUBCOMMAND\n\n path normalize [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...]\n\npath normalize returns the normalized versions of all paths. That\nmeans it squashes duplicate \"/\" (except for two leading \"//\"),\ncollapses \"../\" with earlier components and removes \".\" components.\n\nUnlike realpath or path resolve, it does not make the paths absolute.\nIt also does not resolve any symlinks. As such it can operate on\nnon-existent paths.\n\nBecause it operates on paths as strings and doesn't resolve symlinks,\nit works sort of like pwd -L and cd. E.g. path normalize link/.. will\nreturn ., just like cd link; cd .. would return to the current\ndirectory. For a physical view of the filesystem, see path resolve.\n\nLeading \"./\" components are usually removed. But when a path starts\nwith -, path normalize will add it instead to avoid confusion with\noptions.\n\nIt returns 0 if any normalization was done, i.e. any given path\nwasn't in canonical form.\n\n Examples\n\n > path normalize /usr/bin//../../etc/fish\n # The \"//\" is squashed and the \"..\" components neutralize the components before\n /etc/fish\n\n > path normalize /bin//bash\n # The \"//\" is squashed, but /bin isn't resolved even if your system links it to /usr/bin.\n /bin/bash\n\n > path normalize ./my/subdirs/../sub2\n my/sub2\n\n > path normalize -- -/foo\n ./-/foo\n\nRESOLVE SUBCOMMAND\n\n path resolve [-z | --null-in] [-Z | --null-out] [-q | --quiet] [PATH ...]\n\npath resolve returns the normalized, physical and absolute versions\nof all paths. That means it resolves symlinks and does what path\nnormalize does: it squashes duplicate \"/\", collapses \"../\" with\nearlier components and removes \".\" components. Then it turns that\npath into the absolute path starting from the filesystem root \"/\".\n\nIt is similar to realpath, as it creates the \"real\", canonical\nversion of the path. However, for paths that can't be resolved, e.g.\nif they don't exist or form a symlink loop, it will resolve as far as\nit can and normalize the rest.\n\nBecause it resolves symlinks, it works sort of like pwd -P. E.g. path\nresolve link/.. will return the parent directory of what the link\npoints to, just like cd link; cd (pwd -P)/.. would go to it. For a\nlogical view of the filesystem, see path normalize.\n\nIt returns 0 if any normalization or resolution was done, i.e. any\ngiven path wasn't in canonical form.\n\n Examples\n\n > path resolve /bin//sh\n # The \"//\" is squashed, and /bin is resolved if your system links it to /usr/bin.\n # sh here is bash (this is common on linux systems)\n /usr/bin/bash\n\n > path resolve /bin/foo///bar/../baz\n # Assuming /bin exists and is a symlink to /usr/bin, but /bin/foo doesn't.\n # This resolves the /bin/ and normalizes the nonexistent rest:\n /usr/bin/foo/baz\n\nCHANGE-EXTENSION SUBCOMMAND\n\n path change-extension [-z | --null-in] [-Z | --null-out] \\\n [-q | --quiet] EXTENSION [PATH ...]\n\npath change-extension returns the given paths, with their extension\nchanged to the given new extension. The extension is the part after\n(and including) the last \".\", unless that \".\" followed a \"/\" or the\nbasename is \".\" or \"..\", in which case there is no previous extension\nand the new one is simply added.\n\nIf the extension is empty, any previous extension is stripped, along\nwith the \".\". This is, of course, the inverse of path extension.\n\nOne leading dot on the extension is ignored, so \".mp3\" and \"mp3\" are\ntreated the same.\n\nIt returns 0 if it was given any paths.\n\n Examples\n\n > path change-extension mp4 ./foo.wmv\n ./foo.mp4\n\n > path change-extension .mp4 ./foo.wmv\n ./foo.mp4\n\n > path change-extension '' ../banana\n ../banana\n # but status 1, because there was no extension.\n\n > path change-extension '' ~/.config\n /home/alfa/.config\n # status 1\n\n > path change-extension '' ~/.config.d\n /home/alfa/.config\n # status 0\n\n > path change-extension '' ~/.config.\n /home/alfa/.config\n # status 0\n\nSORT SUBCOMMAND\n\n path sort [-z | --null-in] [-Z | --null-out] \\\n [-q | --quiet] [-r | --reverse] \\\n [--key=basename|dirname|path] [PATH ...]\n\npath sort returns the given paths in sorted order. They are sorted in\nthe same order as globs - alphabetically, but with runs of numerical\ndigits compared numerically.\n\nWith --reverse or -r the sort is reversed.\n\nWith --key= only the given part of the path is compared, e.g.\n--key=dirname causes only the dirname to be compared, --key=basename\nonly the basename and --key=path causes the entire path to be\ncompared (this is the default).\n\nWith --unique or -u the sort is deduplicated, meaning only the first\nof a run that have the same key is kept. So if you are sorting by\nbasename, then only the first of each basename is used.\n\nThe sort used is stable, so sorting first by basename and then by\ndirname works and causes the files to be grouped according to\ndirectory.\n\nIt currently returns 0 if it was given any paths.\n\n Examples\n\n > path sort 10-foo 2-bar\n 2-bar\n 10-foo\n\n > path sort --reverse 10-foo 2-bar\n 10-foo\n 2-bar\n\n > path sort --unique --key=basename $fish_function_path/*.fish\n # prints a list of all function files fish would use, sorted by name.\n\nCOMBINING PATH\npath is meant to be easy to combine with itself, other tools and\nfish.\n\nThis is why\n\n• path's output is automatically split by fish if it goes into a\n command substitution, so just doing (path ...) handles all paths,\n even those containing newlines, correctly\n\n• path has --null-in to handle null-delimited input (typically\n automatically detected!), and --null-out to pass on null-delimited\n output\n\nSome examples of combining path:\n\n # Expand all paths in the current directory, leave only executable files, and print their resolved path\n path filter -zZ -xf -- * | path resolve -z\n\n # The same thing, but using find (note -maxdepth needs to come first or find will scream)\n # (this also depends on your particular version of find)\n # Note the `-z` is unnecessary for any sensible version of find - if `path` sees a NULL,\n # it will split on NULL automatically.\n find . -maxdepth 1 -type f -executable -print0 | path resolve -z\n\n set -l paths (path filter -p exec $PATH/fish -Z | path resolve)", + "args": "path basename GENERAL_OPTIONS [PATH ...]\npath dirname GENERAL_OPTIONS [PATH ...]\npath extension GENERAL_OPTIONS [PATH ...]\npath filter GENERAL_OPTIONS [-v | --invert]\n [-d] [-f] [-l] [-r] [-w] [-x]\n [(-t | --type) TYPE] [(-p | --perm) PERMISSION] [PATH ...]\npath is GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE]\n [-d] [-f] [-l] [-r] [-w] [-x]\n [(-p | --perm) PERMISSION] [PATH ...]\npath mtime GENERAL_OPTIONS [(-R | --relative)] [PATH ...]\npath normalize GENERAL_OPTIONS [PATH ...]\npath resolve GENERAL_OPTIONS [PATH ...]\npath change-extension GENERAL_OPTIONS EXTENSION [PATH ...]\npath sort GENERAL_OPTIONS [-r | --reverse]\n [-u | --unique] [--key=basename|dirname|path] [PATH ...]\n\nGENERAL_OPTIONS\n [-z | --null-in] [-Z | --null-out] [-q | --quiet]" + }, + "printf": { + "shortDescription": "Display formatted text", + "description": "The `printf` command formats and prints text according to a specified format string. Unlike `echo`, `printf` does not append a newline unless explicitly included in the format.", + "args": "FORMAT [ARGUMENT...]" + }, + "pwd": { + "shortDescription": "output the current working directory", + "description": "NOTE: This page documents the fish builtin pwd. To see the\ndocumentation on the pwd command you might have, use command man pwd.\n\npwd outputs (prints) the current working directory.\n\nThe following options are available:\n\n-L or --logical\n Output the logical working directory, without resolving\n symlinks (default behavior).\n\n-P or --physical\n Output the physical working directory, with symlinks resolved.\n\n-h or --help\n Displays help about using this command.\n\nSEE ALSO\nNavigate directories using the directory history or the directory\nstack", + "args": "pwd [-P | --physical]\npwd [-L | --logical]" + }, + "random": { + "shortDescription": "generate random number", + "description": "random generates a pseudo-random integer from a uniform distribution.\nThe range (inclusive) depends on the arguments.\n\nNo arguments indicate a range of 0 to 32767 (inclusive).\n\nIf one argument is specified, the internal engine will be seeded with\nthe argument for future invocations of random and no output will be\nproduced.\n\nTwo arguments indicate a range from START to END (both START and END\nincluded).\n\nThree arguments indicate a range from START to END with a spacing of\nSTEP between possible outputs.\n\nrandom choice will select one random item from the succeeding\narguments.\n\nThe -h or --help option displays help about using this command.\n\nNote that seeding the engine will NOT give the same result across\ndifferent systems.\n\nYou should not consider random cryptographically secure, or even\nstatistically accurate.\n\nEXAMPLE\nThe following code will count down from a random even number between\n10 and 20 to 1:\n\n for i in (seq (random 10 2 20) -1 1)\n echo $i\n end\n\nAnd this will open a random picture from any of the subdirectories:\n\n open (random choice **.jpg)\n\nOr, to only get even numbers from 2 to 20:\n\n random 2 2 20\n\nOr odd numbers from 1 to 3:\n\n random 1 2 3 # or 1 2 4", + "args": "random\nrandom SEED\nrandom START END\nrandom START STEP END\nrandom choice [ITEMS ...]" + }, + "read": { + "shortDescription": "read line of input into variables", + "description": "read reads from standard input and either writes the result back to\nstandard output (for use in command substitution), or stores the\nresult in one or more shell variables. By default, read reads a\nsingle line and splits it into variables on spaces or tabs.\nAlternatively, a null character or a maximum number of characters can\nbe used to terminate the input, and other delimiters can be given.\nUnlike other shells, there is no default variable (such as REPLY) for\nstoring the result - instead, it is printed on standard output.\n\nThe following options are available:\n\n-c CMD or --command CMD\n Sets the initial string in the interactive mode command buffer\n to CMD.\n\n-d or --delimiter DELIMITER\n Splits on DELIMITER. DELIMITER will be used as an entire\n string to split on, not a set of characters.\n\n-g or --global\n Makes the variables global.\n\n-s or --silent\n Masks characters written to the terminal, replacing them with\n asterisks. This is useful for reading things like passwords or\n other sensitive information.\n\n-f or --function\n Scopes the variable to the currently executing function. It is\n erased when the function ends.\n\n-l or --local\n Scopes the variable to the currently executing block. It is\n erased when the block ends. Outside of a block, this is the\n same as --function.\n\n-n or --nchars NCHARS\n Makes read return after reading NCHARS characters or the end\n of the line, whichever comes first.\n\n-p or --prompt PROMPT_CMD\n Uses the output of the shell command PROMPT_CMD as the prompt\n for the interactive mode. The default prompt command is\n set_color green; echo read; set_color normal; echo \"> \"\n\n-P or --prompt-str PROMPT_STR\n Uses the PROMPT_STR as the prompt for the interactive mode. It\n is equivalent to echo $PROMPT_STR and is provided solely to\n avoid the need to frame the prompt as a command. All special\n characters in the string are automatically escaped before\n being passed to the echo command.\n\n-R or --right-prompt RIGHT_PROMPT_CMD\n Uses the output of the shell command RIGHT_PROMPT_CMD as the\n right prompt for the interactive mode. There is no default\n right prompt command.\n\n-S or --shell\n Enables syntax highlighting, tab completions and command\n termination suitable for entering shellscript code in the\n interactive mode. NOTE: Prior to fish 3.0, the short opt for\n --shell was -s, but it has been changed for compatibility with\n bash's -s short opt for --silent.\n\n-t -or --tokenize\n Causes read to split the input into variables by the shell's\n tokenization rules. This means it will honor quotes and\n escaping. This option is of course incompatible with other\n options to control splitting like --delimiter and does not\n honor IFS (like fish's tokenizer). It saves the tokens in the\n manner they'd be passed to commands on the commandline, so\n e.g. a\\ b is stored as a b. Note that currently it leaves\n command substitutions intact along with the parentheses.\n\n-u or --unexport\n Prevents the variables from being exported to child processes\n (default behaviour).\n\n-U or --universal\n Causes the specified shell variable to be made universal.\n\n-x or --export\n Exports the variables to child processes.\n\n-a or --list\n Stores the result as a list in a single variable. This option\n is also available as --array for backwards compatibility.\n\n-z or --null\n Marks the end of the line with the NUL character, instead of\n newline. This also disables interactive mode.\n\n-L or --line\n Reads each line into successive variables, and stops after\n each variable has been filled. This cannot be combined with\n the --delimiter option.\n\nWithout the --line option, read reads a single line of input from\nstandard input, breaks it into tokens, and then assigns one token to\neach variable specified in VARIABLES. If there are more tokens than\nvariables, the complete remainder is assigned to the last variable.\n\nIf no option to determine how to split like --delimiter, --line or\n--tokenize is given, the variable IFS is used as a list of characters\nto split on. Relying on the use of IFS is deprecated and this\nbehaviour will be removed in future versions. The default value of\nIFS contains space, tab and newline characters. As a special case, if\nIFS is set to the empty string, each character of the input is\nconsidered a separate token.\n\nWith the --line option, read reads a line of input from standard\ninput into each provided variable, stopping when each variable has\nbeen filled. The line is not tokenized.\n\nIf no variable names are provided, read enters a special case that\nsimply provides redirection from standard input to standard output,\nuseful for command substitution. For instance, the fish shell command\nbelow can be used to read data that should be provided via a command\nline argument from the console instead of hardcoding it in the\ncommand itself, allowing the command to both be reused as-is in\nvarious contexts with different input values and preventing possibly\nsensitive text from being included in the shell history:\n\n mysql -uuser -p(read)\n\nWhen running in this mode, read does not split the input in any way\nand text is redirected to standard output without any further\nprocessing or manipulation.\n\nIf -a or --array is provided, only one variable name is allowed and\nthe tokens are stored as a list in this variable.\n\nSee the documentation for set for more details on the scoping rules\nfor variables.\n\nWhen read reaches the end-of-file (EOF) instead of the terminator,\nthe exit status is set to 1. Otherwise, it is set to 0.\n\nIn order to protect the shell from consuming too many system\nresources, read will only consume a maximum of 100 MiB (104857600\nbytes); if the terminator is not reached before this limit then\nVARIABLE is set to empty and the exit status is set to 122. This\nlimit can be altered with the fish_read_limit variable. If set to 0\n(zero), the limit is removed.\n\nEXAMPLE\nread has a few separate uses.\n\nThe following code stores the value 'hello' in the shell variable\nfoo.\n\n echo hello|read foo\n\nThe while command is a neat way to handle command output\nline-by-line:\n\n printf '%s\\n' line1 line2 line3 line4 | while read -l foo\n echo \"This is another line: $foo\"\n end\n\nDelimiters given via \"-d\" are taken as one string:\n\n echo a==b==c | read -d == -l a b c\n echo $a # a\n echo $b # b\n echo $c # c\n\n--tokenize honors quotes and escaping like the shell's argument\npassing:\n\n echo 'a\\ b' | read -t first second\n echo $first # outputs \"a b\", $second is empty\n\n echo 'a\"foo bar\"b (command echo wurst)*\" \"{a,b}' | read -lt -l a b c\n echo $a # outputs 'afoo barb' (without the quotes)\n echo $b # outputs '(command echo wurst)* {a,b}' (without the quotes)\n echo $c # nothing\n\nFor an example on interactive use, see Querying for user input.", + "args": "read [OPTIONS] [VARIABLE ...]" + }, + "realpath": { + "shortDescription": "Resolve and print the absolute path", + "description": "Convert each provided path to its absolute, canonical form by resolving symbolic links and relative path components.", + "args": "PATH..." + }, + "return": { + "shortDescription": "stop the current inner function", + "description": "return halts a currently running function. The exit status is set to\nN if it is given. If return is invoked outside of a function or dot\nscript it is equivalent to exit.\n\nIt is often added inside of a conditional block such as an if\nstatement or a switch statement to conditionally stop the executing\nfunction and return to the caller; it can also be used to specify the\nexit status of a function.\n\nIf at the top level of a script, it exits with the given status, like\nexit. If at the top level in an interactive session, it will set\nstatus, but not exit the shell.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\nAn implementation of the false command as a fish function:\n\n function false\n return 1\n end", + "args": "return [N]" + }, + "set": { + "shortDescription": "display and change shell variables", + "description": "set manipulates shell variables.\n\nIf both NAME and VALUE are provided, set assigns any values to\nvariable NAME. Variables in fish are lists, multiple values are\nallowed. One or more variable INDEX can be specified including\nranges (not for all options.)\n\nIf no VALUE is given, the variable will be set to the empty list.\n\nIf set is ran without arguments, it prints the names and values of\nall shell variables in sorted order. Passing scope or export flags\nallows filtering this to only matching variables, so set --local\nwould only show local variables.\n\nWith --erase and optionally a scope flag set will erase the matching\nvariable (or the variable of that name in the smallest possible\nscope).\n\nWith --show, set will describe the given variable names, explaining\nhow they have been defined - in which scope with which values and\noptions.\n\nThe following options control variable scope:\n\n-U or --universal\n Sets a universal variable. The variable will be immediately\n available to all the user's fish instances on the machine, and\n will be persisted across restarts of the shell.\n\n-f or --function\n Sets a variable scoped to the executing function. It is\n erased when the function ends.\n\n-l or --local\n Sets a locally-scoped variable in this block. It is erased\n when the block ends. Outside of a block, this is the same as\n --function.\n\n-g or --global\n Sets a globally-scoped variable. Global variables are\n available to all functions running in the same shell. They\n can be modified or erased.\n\nThese options modify how variables operate:\n\n--export or -x\n Causes the specified shell variable to be exported to child\n processes (making it an \"environment variable\").\n\n--unexport or -u\n Causes the specified shell variable to NOT be exported to\n child processes.\n\n--path Treat specified variable as a path variable; variable will be\n split on colons (:) and will be displayed joined by colons\n when quoted (echo \"$PATH\") or exported.\n\n--unpath\n Causes variable to no longer be treated as a path variable.\n Note: variables ending in \"PATH\" are automatically path\n variables.\n\nFurther options:\n\n-a or --append NAME VALUE ...\n Appends VALUES to the current set of values for variable NAME.\n Can be used with --prepend to both append and prepend at the\n same time. This cannot be used when assigning to a variable\n slice.\n\n-p or --prepend NAME VALUE ...\n Prepends VALUES to the current set of values for variable\n NAME. This can be used with --append to both append and\n prepend at the same time. This cannot be used when assigning\n to a variable slice.\n\n-e or --erase NAME*[*INDEX]\n Causes the specified shell variables to be erased. Supports\n erasing from multiple scopes at once. Individual items in a\n variable at INDEX in brackets can be specified.\n\n-q or --query NAME*[*INDEX]\n Test if the specified variable names are defined. If an INDEX\n is provided, check for items at that slot. Does not output\n anything, but the shell status is set to the number of\n variables specified that were not defined, up to a maximum of\n 255. If no variable was given, it also returns 255.\n\n-n or --names\n List only the names of all defined variables, not their value.\n The names are guaranteed to be sorted.\n\n-S or --show\n Shows information about the given variables. If no variable\n names are given then all variables are shown in sorted order.\n It shows the scopes the given variables are set in, along with\n the values in each and whether or not it is exported. No\n other flags can be used with this option.\n\n-L or --long\n Do not abbreviate long values when printing set variables.\n\n-h or --help\n Displays help about using this command.\n\nIf a variable is set to more than one value, the variable will be a\nlist with the specified elements. If a variable is set to zero\nelements, it will become a list with zero elements.\n\nIf the variable name is one or more list elements, such as PATH[1 3\n7], only those list elements specified will be changed. If you\nspecify a negative index when expanding or assigning to a list\nvariable, the index will be calculated from the end of the list. For\nexample, the index -1 means the last index of a list.\n\nThe scoping rules when creating or updating a variable are:\n\n• Variables may be explicitly set as universal, global, function, or\n local. Variables with the same name but in a different scope will\n not be changed.\n\n• If the scope of a variable is not explicitly set but a variable by\n that name has been previously defined, the scope of the existing\n variable is used. If the variable is already defined in multiple\n scopes, the variable with the narrowest scope will be updated.\n\n• If a variable's scope is not explicitly set and there is no\n existing variable by that name, the variable will be local to the\n currently executing function. Note that this is different from\n using the -l or --local flag, in which case the variable will be\n local to the most-inner currently executing block, while without\n them the variable will be local to the function as a whole. If no\n function is executing, the variable will be set in the global\n scope.\n\nThe exporting rules when creating or updating a variable are\nidentical to the scoping rules for variables:\n\n• Variables may be explicitly set to either exported or not exported.\n When an exported variable goes out of scope, it is unexported.\n\n• If a variable is not explicitly set to be exported or not exported,\n but has been previously defined, the previous exporting rule for\n the variable is kept.\n\n• If a variable is not explicitly set to be either exported or\n unexported and has never before been defined, the variable will not\n be exported.\n\nIn query mode, the scope to be examined can be specified. Whether\nthe variable has to be a path variable or exported can also be\nspecified.\n\nIn erase mode, if variable indices are specified, only the specified\nslices of the list variable will be erased.\n\nset requires all options to come before any other arguments. For\nexample, set flags -l will have the effect of setting the value of\nthe variable flags to '-l', not making the variable local.\n\nEXIT STATUS\nIn assignment mode, set does not modify the exit status, but passes\nalong whatever status was set, including by command substitutions.\nThis allows capturing the output and exit status of a subcommand,\nlike in if set output (command).\n\nIn query mode, the exit status is the number of variables that were\nnot found.\n\nIn erase mode, set exits with a zero exit status in case of success,\nwith a non-zero exit status if the commandline was invalid, if any of\nthe variables did not exist or was a special read-only variable.\n\nEXAMPLES\nPrint all global, exported variables:\n\n > set -gx\n\nSet the value of the variable $foo to be 'hi'.:\n\n > set foo hi\n\nAppend the value \"there\" to the variable $foo:\n\n > set -a foo there\n\nRemove $smurf from the scope:\n\n > set -e smurf\n\nRemove $smurf from the global and universal scopes:\n\n > set -e -Ug smurf\n\nChange the fourth element of the $PATH list to ~/bin:\n\n > set PATH[4] ~/bin\n\nOutputs the path to Python if type -p returns true:\n\n if set python_path (type -p python)\n echo \"Python is at $python_path\"\n end\n\nSetting a variable doesn't modify $status; a command substitution\nstill will, though:\n\n > echo $status\n 0\n > false\n > set foo bar\n > echo $status\n 1\n > true\n > set foo banana (false)\n > echo $status\n 1\n\nVAR=VALUE command sets a variable for just one command, like other\nshells. This runs fish with a temporary home directory:\n\n > HOME=(mktemp -d) fish\n\n(which is essentially the same as):\n\n > begin; set -lx HOME (mktemp -d); fish; end\n\nNOTES\n\n• Fish versions prior to 3.0 supported the syntax set PATH[1] PATH[4]\n /bin /sbin, which worked like set PATH[1 4] /bin /sbin.", + "args": "set\nset (-f | --function) (-l | local) (-g | --global) (-U | --universal)\nset [-Uflg] NAME [VALUE ...]\nset [-Uflg] NAME[[INDEX ...]] [VALUE ...]\nset (-a | --append) [-flgU] NAME VALUE ...\nset (-q | --query) (-e | --erase) [-flgU] [NAME][[INDEX]] ...]\nset (-S | --show) [NAME ...]" + }, + "set_color": { + "shortDescription": "set the terminal color", + "description": "set_color is used to control the color and styling of text in the\nterminal. VALUE describes that styling. VALUE can be a reserved color\nname like red or an RGB color value given as 3 or 6 hexadecimal\ndigits (\"F27\" or \"FF2277\"). A special keyword normal resets text\nformatting to terminal defaults.\n\nValid colors include:\n\n • black, red, green, yellow, blue, magenta, cyan, white\n\n • brblack, brred, brgreen, bryellow, brblue, brmagenta, brcyan,\n brwhite\n\nThe br- (as in 'bright') forms are full-brightness variants of the 8\nstandard-brightness colors on many terminals. brblack has higher\nbrightness than black - towards gray.\n\nAn RGB value with three or six hex digits, such as A0FF33 or f2f can\nbe used. Fish will choose the closest supported color. A three digit\nvalue is equivalent to specifying each digit twice; e.g., set_color\n2BC is the same as set_color 22BBCC. Hexadecimal RGB values can be in\nlower or uppercase. Depending on the capabilities of your terminal\n(and the level of support set_color has for it) the actual color may\nbe approximated by a nearby matching reserved color name or set_color\nmay not have an effect on color.\n\nA second color may be given as a desired fallback color. e.g.\nset_color 124212 brblue will instruct set_color to use brblue if a\nterminal is not capable of the exact shade of grey desired. This is\nvery useful when an 8 or 16 color terminal might otherwise not use a\ncolor.\n\nThe following options are available:\n\n-b or --background COLOR\n Sets the background color.\n\n-c or --print-colors\n Prints the given colors or a colored list of the 16 named\n colors.\n\n-o or --bold\n Sets bold mode.\n\n-d or --dim\n Sets dim mode.\n\n-i or --italics\n Sets italics mode.\n\n-r or --reverse\n Sets reverse mode.\n\n-u or --underline\n Sets underlined mode.\n\n-h or --help\n Displays help about using this command.\n\nUsing the normal keyword will reset foreground, background, and all\nformatting back to default.\n\nNOTES\n\n1. Using the normal keyword will reset both background and foreground\n colors to whatever is the default for the terminal.\n\n2. Setting the background color only affects subsequently written\n characters. Fish provides no way to set the background color for\n the entire terminal window. Configuring the window background\n color (and other attributes such as its opacity) has to be done\n using whatever mechanisms the terminal provides. Look for a config\n option.\n\n3. Some terminals use the --bold escape sequence to switch to a\n brighter color set rather than increasing the weight of text.\n\n4. set_color works by printing sequences of characters to standard\n output. If used in command substitution or a pipe, these\n characters will also be captured. This may or may not be\n desirable. Checking the exit status of isatty stdout before using\n set_color can be useful to decide not to colorize output in a\n script.\n\nEXAMPLES\n\n set_color red; echo \"Roses are red\"\n set_color blue; echo \"Violets are blue\"\n set_color 62A; echo \"Eggplants are dark purple\"\n set_color normal; echo \"Normal is nice\" # Resets the background too\n\nTERMINAL CAPABILITY DETECTION\nFish uses some heuristics to determine what colors a terminal\nsupports to avoid sending sequences that it won't understand.\n\nIn particular it will:\n\n• Enable 256 colors if TERM contains \"xterm\", except for known\n exceptions (like MacOS 10.6 Terminal.app)\n\n• Enable 24-bit (\"true-color\") even if the $TERM entry only reports\n 256 colors. This includes modern xterm, VTE-based terminals like\n Gnome Terminal, Konsole and iTerm2.\n\n• Detect support for italics, dim, reverse and other modes.\n\nIf terminfo reports 256 color support for a terminal, 256 color\nsupport will always be enabled.\n\nTo force true-color support on or off, set fish_term24bit to \"1\" for\non and 0 for off - set -g fish_term24bit 1.\n\nTo debug color palette problems, tput colors may be useful to see the\nnumber of colors in terminfo for a terminal. Fish launched as fish -d\nterm_support will include diagnostic messages that indicate the color\nsupport mode in use.\n\nThe set_color command uses the terminfo database to look up how to\nchange terminal colors on whatever terminal is in use. Some systems\nhave old and incomplete terminfo databases, and lack color\ninformation for terminals that support it. Fish assumes that all\nterminals can use the [ANSI\nX3.64](https://en.wikipedia.org/wiki/ANSI_escape_code) escape\nsequences if the terminfo definition indicates a color below 16 is\nnot supported.", + "args": "set_color [OPTIONS] VALUE" + }, + "source": { + "shortDescription": "evaluate contents of file", + "description": "source evaluates the commands of the specified FILE in the current\nshell as a new block of code. This is different from starting a new\nprocess to perform the commands (i.e. fish < FILE) since the commands\nwill be evaluated by the current shell, which means that changes in\nshell variables will affect the current shell. If additional\narguments are specified after the file name, they will be inserted\ninto the argv variable. The argv variable will not include the name\nof the sourced file.\n\nfish will search the working directory to resolve relative paths but\nwill not search PATH .\n\nIf no file is specified and stdin is not the terminal, or if the file\nname - is used, stdin will be read.\n\nThe exit status of source is the exit status of the last job to\nexecute. If something goes wrong while opening or reading the file,\nsource exits with a non-zero status.\n\n. (a single period) is an alias for the source command. The use of .\nis deprecated in favour of source, and . will be removed in a future\nversion of fish.\n\nsource creates a new local scope; set --local within a sourced block\nwill not affect variables in the enclosing scope.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\n\n source ~/.config/fish/config.fish\n # Causes fish to re-read its initialization file.\n\nCAVEATS\nIn fish versions prior to 2.3.0, the argv variable would have a\nsingle element (the name of the sourced file) if no arguments are\npresent. Otherwise, it would contain arguments without the name of\nthe sourced file. That behavior was very confusing and unlike other\nshells such as bash and zsh.", + "args": "source FILE [ARGUMENTS ...]\nSOMECOMMAND | source" + }, + "status": { + "shortDescription": "query fish runtime information", + "description": "With no arguments, status displays a summary of the current login and\njob control status of the shell.\n\nThe following operations (subcommands) are available:\n\nis-command-substitution, -c or --is-command-substitution\n Returns 0 if fish is currently executing a command\n substitution.\n\nis-block, -b or --is-block\n Returns 0 if fish is currently executing a block of code.\n\nis-breakpoint\n Returns 0 if fish is currently showing a prompt in the context\n of a breakpoint command. See also the fish_breakpoint_prompt\n function.\n\nis-interactive, -i or --is-interactive\n Returns 0 if fish is interactive - that is, connected to a\n keyboard.\n\nis-login, -l or --is-login\n Returns 0 if fish is a login shell - that is, if fish should\n perform login tasks such as setting up PATH.\n\nis-full-job-control or --is-full-job-control\n Returns 0 if full job control is enabled.\n\nis-interactive-job-control or --is-interactive-job-control\n Returns 0 if interactive job control is enabled.\n\nis-no-job-control or --is-no-job-control\n Returns 0 if no job control is enabled.\n\ncurrent-command\n Prints the name of the currently-running function or command,\n like the deprecated variable.\n\ncurrent-commandline\n Prints the entirety of the currently-running commandline,\n inclusive of all jobs and operators.\n\nfilename, current-filename, -f or --current-filename\n Prints the filename of the currently-running script. If the\n current script was called via a symlink, this will return the\n symlink. If the current script was received by piping into\n source, then this will return -.\n\nbasename\n Prints just the filename of the running script, without any\n path components before.\n\ndirname\n Prints just the path to the running script, without the actual\n filename itself. This can be relative to PWD (including just\n \".\"), depending on how the script was called. This is the same\n as passing the filename to dirname(3). It's useful if you want\n to use other files in the current script's directory or\n similar.\n\nfish-path\n Prints the absolute path to the currently executing instance\n of fish. This is a best-effort attempt and the exact output is\n down to what the platform gives fish. In some cases you might\n only get \"fish\".\n\nfunction or current-function\n Prints the name of the currently called function if able, when\n missing displays \"Not a function\" (or equivalent translated\n string).\n\nline-number, current-line-number, -n or --current-line-number\n Prints the line number of the currently running script.\n\nstack-trace, print-stack-trace, -t or --print-stack-trace\n Prints a stack trace of all function calls on the call stack.\n\njob-control, -j or --job-control CONTROL_TYPE\n Sets the job control type to CONTROL_TYPE, which can be none,\n full, or interactive.\n\nfeatures\n Lists all available feature flags.\n\ntest-feature FEATURE\n Returns 0 when FEATURE is enabled, 1 if it is disabled, and 2\n if it is not recognized.\n\nNOTES\nFor backwards compatibility most subcommands can also be specified as\na long or short option. For example, rather than status is-login you\ncan type status --is-login. The flag forms are deprecated and may be\nremoved in a future release (but not before fish 4.0).\n\nYou can only specify one subcommand per invocation even if you use\nthe flag form of the subcommand.", + "args": "status\nstatus is-login\nstatus is-interactive\nstatus is-block\nstatus is-breakpoint\nstatus is-command-substitution\nstatus is-no-job-control\nstatus is-full-job-control\nstatus is-interactive-job-control\nstatus current-command\nstatus current-commandline\nstatus filename\nstatus basename\nstatus dirname\nstatus fish-path\nstatus function\nstatus line-number\nstatus stack-trace\nstatus job-control CONTROL_TYPE\nstatus features\nstatus test-feature FEATURE" + }, + "string": { + "shortDescription": "manipulate strings", + "description": "string performs operations on strings.\n\nSTRING arguments are taken from the command line unless standard\ninput is connected to a pipe or a file, in which case they are read\nfrom standard input, one STRING per line. It is an error to supply\nSTRING arguments on the command line and on standard input.\n\nArguments beginning with - are normally interpreted as switches; --\ncauses the following arguments not to be treated as switches even if\nthey begin with -. Switches and required arguments are recognized\nonly on the command line.\n\nMost subcommands accept a -q or --quiet switch, which suppresses the\nusual output but exits with the documented status. In this case these\ncommands will quit early, without reading all of the available input.\n\nThe following subcommands are available.\n\nCOLLECT SUBCOMMAND\nstring collect [-a | --allow-empty] [-N | --no-trim-newlines] [STRING ...]\n\nstring collect collects its input into a single output argument,\nwithout splitting the output when used in a command substitution.\nThis is useful when trying to collect multiline output from another\ncommand into a variable. Exit status: 0 if any output argument is\nnon-empty, or 1 otherwise.\n\nA command like echo (cmd | string collect) is mostly equivalent to a\nquoted command substitution (echo \"$(cmd)\"). The main difference is\nthat the former evaluates to zero or one elements whereas the quoted\ncommand substitution always evaluates to one element due to string\ninterpolation.\n\nIf invoked with multiple arguments instead of input, string collect\npreserves each argument separately, where the number of output\narguments is equal to the number of arguments given to string\ncollect.\n\nAny trailing newlines on the input are trimmed, just as with \"$(cmd)\"\nsubstitution. Use --no-trim-newlines to disable this behavior, which\nmay be useful when running a command such as set contents (cat\nfilename | string collect -N).\n\nWith --allow-empty, string collect always prints one (empty)\nargument. This can be used to prevent an argument from disappearing.\n\n Examples\n\n > echo \"zero $(echo one\\ntwo\\nthree) four\"\n zero one\n two\n three four\n\n > echo \\\"(echo one\\ntwo\\nthree | string collect)\\\"\n \"one\n two\n three\"\n\n > echo \\\"(echo one\\ntwo\\nthree | string collect -N)\\\"\n \"one\n two\n three\n \"\n\n > echo foo(true | string collect --allow-empty)bar\n foobar\n\nESCAPE AND UNESCAPE SUBCOMMANDS\nstring escape [-n | --no-quoted] [--style=] [STRING ...]\nstring unescape [--style=] [STRING ...]\n\nstring escape escapes each STRING in one of three ways. The first is\n--style=script. This is the default. It alters the string such that\nit can be passed back to eval to produce the original argument again.\nBy default, all special characters are escaped, and quotes are used\nto simplify the output when possible. If -n or --no-quoted is given,\nthe simplifying quoted format is not used. Exit status: 0 if at least\none string was escaped, or 1 otherwise.\n\n--style=var ensures the string can be used as a variable name by hex\nencoding any non-alphanumeric characters. The string is first\nconverted to UTF-8 before being encoded.\n\n--style=url ensures the string can be used as a URL by hex encoding\nany character which is not legal in a URL. The string is first\nconverted to UTF-8 before being encoded.\n\n--style=regex escapes an input string for literal matching within a\nregex expression. The string is first converted to UTF-8 before being\nencoded.\n\nstring unescape performs the inverse of the string escape command. If\nthe string to be unescaped is not properly formatted it is ignored.\nFor example, doing string unescape --style=var (string escape\n--style=var $str) will return the original string. There is no\nsupport for unescaping --style=regex.\n\n Examples\n\n > echo \\x07 | string escape\n \\cg\n\n > string escape --style=var 'a1 b2'\\u6161\n a1_20_b2_E6_85_A1\n\nJOIN AND JOIN0 SUBCOMMANDS\nstring join [-q | --quiet] SEP [STRING ...]\nstring join0 [-q | --quiet] [STRING ...]\n\nstring join joins its STRING arguments into a single string separated\nby SEP, which can be an empty string. Exit status: 0 if at least one\njoin was performed, or 1 otherwise. If -n or --no-empty is specified,\nempty strings are excluded from consideration (e.g. string join -n +\na b \"\" c would expand to a+b+c not a+b++c).\n\nstring join0 joins its STRING arguments into a single string\nseparated by the zero byte (NUL), and adds a trailing NUL. This is\nmost useful in conjunction with tools that accept NUL-delimited\ninput, such as sort -z. Exit status: 0 if at least one join was\nperformed, or 1 otherwise.\n\nBecause Unix uses NUL as the string terminator, passing the output of\nstring join0 as an argument to a command (via a command substitution)\nwon't actually work. Fish will pass the correct bytes along, but the\ncommand won't be able to tell where the argument ends. This is a\nlimitation of Unix' argument passing.\n\n Examples\n\n > seq 3 | string join ...\n 1...2...3\n\n # Give a list of NUL-separated filenames to du (this is a GNU extension)\n > string join0 file1 file2 file\\nwith\\nmultiple\\nlines | du --files0-from=-\n\n # Just put the strings together without a separator\n > string join '' a b c\n abc\n\nLENGTH SUBCOMMAND\nstring length [-q | --quiet] [-V | --visible] [STRING ...]\n\nstring length reports the length of each string argument in\ncharacters. Exit status: 0 if at least one non-empty STRING was\ngiven, or 1 otherwise.\n\nWith -V or --visible, it uses the visible width of the arguments.\nThat means it will discount escape sequences fish knows about,\naccount for $fish_emoji_width and $fish_ambiguous_width. It will also\ncount each line (separated by \\n) on its own, and with a carriage\nreturn (\\r) count only the widest stretch on a line. The intent is to\nmeasure the number of columns the STRING would occupy in the current\nterminal.\n\n Examples\n\n > string length 'hello, world'\n 12\n\n > set str foo\n > string length -q $str; echo $status\n 0\n # Equivalent to test -n \"$str\"\n\n > string length --visible (set_color red)foobar\n # the set_color is discounted, so this is the width of \"foobar\"\n 6\n\n > string length --visible 🐟🐟🐟🐟\n # depending on $fish_emoji_width, this is either 4 or 8\n # in new terminals it should be\n 8\n\n > string length --visible abcdef\\r123\n # this displays as \"123def\", so the width is 6\n 6\n\n > string length --visible a\\nbc\n # counts \"a\" and \"bc\" as separate lines, so it prints width for each\n 1\n 2\n\nLOWER SUBCOMMAND\nstring lower [-q | --quiet] [STRING ...]\n\nstring lower converts each string argument to lowercase. Exit status:\n0 if at least one string was converted to lowercase, else 1. This\nmeans that in conjunction with the -q flag you can readily test\nwhether a string is already lowercase.\n\nMATCH SUBCOMMAND\nstring match [-a | --all] [-e | --entire] [-i | --ignore-case]\n [-g | --groups-only] [-r | --regex] [-n | --index]\n [-q | --quiet] [-v | --invert]\n PATTERN [STRING ...]\n\nstring match tests each STRING against PATTERN and prints matching\nsubstrings. Only the first match for each STRING is reported unless\n-a or --all is given, in which case all matches are reported.\n\nIf you specify the -e or --entire then each matching string is\nprinted including any prefix or suffix not matched by the pattern\n(equivalent to grep without the -o flag). You can, obviously, achieve\nthe same result by prepending and appending * or .* depending on\nwhether or not you have specified the --regex flag. The --entire flag\nis simply a way to avoid having to complicate the pattern in that\nfashion and make the intent of the string match clearer. Without\n--entire and --regex, a PATTERN will need to match the entire STRING\nbefore it will be reported.\n\nMatching can be made case-insensitive with --ignore-case or -i.\n\nIf --groups-only or -g is given, only the capturing groups will be\nreported - meaning the full match will be skipped. This is\nincompatible with --entire and --invert, and requires --regex. It is\nuseful as a simple cutting tool instead of string replace, so you can\nsimply choose \"this part\" of a string.\n\nIf --index or -n is given, each match is reported as a 1-based start\nposition and a length. By default, PATTERN is interpreted as a glob\npattern matched against each entire STRING argument. A glob pattern\nis only considered a valid match if it matches the entire STRING.\n\nIf --regex or -r is given, PATTERN is interpreted as a\nPerl-compatible regular expression, which does not have to match the\nentire STRING. For a regular expression containing capturing groups,\nmultiple items will be reported for each match, one for the entire\nmatch and one for each capturing group. With this, only the matching\npart of the STRING will be reported, unless --entire is given.\n\nWhen matching via regular expressions, string match automatically\nsets variables for all named capturing groups ((?expression)).\nIt will create a variable with the name of the group, in the default\nscope, for each named capturing group, and set it to the value of the\ncapturing group in the first matched argument. If a named capture\ngroup matched an empty string, the variable will be set to the empty\nstring (like set var \"\"). If it did not match, the variable will be\nset to nothing (like set var). When --regex is used with --all, this\nbehavior changes. Each named variable will contain a list of matches,\nwith the first match contained in the first element, the second match\nin the second, and so on. If the group was empty or did not match,\nthe corresponding element will be an empty string.\n\nIf --invert or -v is used the selected lines will be only those which\ndo not match the given glob pattern or regular expression.\n\nExit status: 0 if at least one match was found, or 1 otherwise.\n\n Match Glob Examples\n\n > string match '?' a\n a\n\n > string match 'a*b' axxb\n axxb\n\n > string match -i 'a??B' Axxb\n Axxb\n\n > string match -- '-*' -h foo --version bar\n # To match things that look like options, we need a `--`\n # to tell string its options end there.\n -h\n --version\n\n > echo 'ok?' | string match '*\\?'\n ok?\n\n # Note that only the second STRING will match here.\n > string match 'foo' 'foo1' 'foo' 'foo2'\n foo\n\n > string match -e 'foo' 'foo1' 'foo' 'foo2'\n foo1\n foo\n foo2\n\n > string match 'foo?' 'foo1' 'foo' 'foo2'\n foo1\n foo2\n\n Match Regex Examples\n\n > string match -r 'cat|dog|fish' 'nice dog'\n dog\n\n > string match -r -v \"c.*[12]\" {cat,dog}(seq 1 4)\n dog1\n dog2\n cat3\n dog3\n cat4\n dog4\n\n > string match -r -- '-.*' -h foo --version bar\n # To match things that look like options, we need a `--`\n # to tell string its options end there.\n -h\n --version\n\n > string match -r '(\\d\\d?):(\\d\\d):(\\d\\d)' 2:34:56\n 2:34:56\n 2\n 34\n 56\n\n > string match -r '^(\\w{2,4})\\1$' papa mud murmur\n papa\n pa\n murmur\n mur\n\n > string match -r -a -n at ratatat\n 2 2\n 4 2\n 6 2\n\n > string match -r -i '0x[0-9a-f]{1,8}' 'int magic = 0xBadC0de;'\n 0xBadC0de\n\n > echo $version\n 3.1.2-1575-ga2ff32d90\n > string match -rq '(?\\d+).(?\\d+).(?\\d+)' -- $version\n > echo \"You are using fish $major!\"\n You are using fish 3!\n\n > string match -raq ' *(?[^.!?]+)(?[.!?])?' \"hello, friend. goodbye\"\n > printf \"%s\\n\" -- $sentence\n hello, friend\n goodbye\n > printf \"%s\\n\" -- $punctuation\n .\n\n > string match -rq '(?hello)' 'hi'\n > count $word\n 0\n\nPAD AND SHORTEN SUBCOMMANDS\nstring pad [-r | --right] [(-c | --char) CHAR] [(-w | --width) INTEGER]\n [STRING ...]\n\nstring pad extends each STRING to the given visible width by adding\nCHAR to the left. That means the width of all visible characters\nadded together, excluding escape sequences and accounting for\nfish_emoji_width and fish_ambiguous_width. It is the amount of\ncolumns in a terminal the STRING occupies.\n\nThe escape sequences reflect what fish knows about, and how it\ncomputes its output. Your terminal might support more escapes, or not\nsupport escape sequences that fish knows about.\n\nIf -r or --right is given, add the padding after a string.\n\nIf -c or --char is given, pad with CHAR instead of whitespace.\n\nThe output is padded to the maximum width of all input strings. If -w\nor --width is given, use at least that.\n\n > string pad -w 10 abc abcdef\n abc\n abcdef\n\n > string pad --right --char=🐟 \"fish are pretty\" \"rich. \"\n fish are pretty\n rich. 🐟🐟🐟🐟\n\n > string pad -w$COLUMNS (date)\n # Prints the current time on the right edge of the screen.\n\nSEE ALSO\n\n• The printf command can do simple padding, for example printf %10s\\n\n works like string pad -w10.\n\n• string length with the --visible option can be used to show what\n fish thinks the width is.\nstring shorten [(-c | --char) CHARS] [(-m | --max) INTEGER]\n [-N | --no-newline] [-l | --left] [-q | --quiet] [STRING ...]\n\nstring shorten truncates each STRING to the given visible width and\nadds an ellipsis to indicate it. \"Visible width\" means the width of\nall visible characters added together, excluding escape sequences and\naccounting for fish_emoji_width and fish_ambiguous_width. It is the\namount of columns in a terminal the STRING occupies.\n\nThe escape sequences reflect what fish knows about, and how it\ncomputes its output. Your terminal might support more escapes, or not\nsupport escape sequences that fish knows about.\n\nIf -m or --max is given, truncate at the given width. Otherwise, the\nlowest non-zero width of all input strings is used. A max of 0 means\nno shortening takes place, all STRINGs are printed as-is.\n\nIf -N or --no-newline is given, only the first line (or last line\nwith --left) of each STRING is used, and an ellipsis is added if it\nwas multiline. This only works for STRINGs being given as arguments,\nmultiple lines given on stdin will be interpreted as separate STRINGs\ninstead.\n\nIf -c or --char is given, add CHAR instead of an ellipsis. This can\nalso be empty or more than one character.\n\nIf -l or --left is given, remove text from the left on instead, so\nthis prints the longest suffix of the string that fits. With\n--no-newline, this will take from the last line instead of the first.\n\nIf -q or --quiet is given, string shorten only runs for the return\nvalue - if anything would be shortened, it returns 0, else 1.\n\nThe default ellipsis is …. If fish thinks your system is incapable\nbecause of your locale, it will use ... instead.\n\nThe return value is 0 if any shortening occured, 1 otherwise.\n\n > string shorten foo foobar\n # No width was given, we infer, and \"foo\" is the shortest.\n foo\n fo…\n\n > string shorten --char=\"...\" foo foobar\n # The target width is 3 because of \"foo\",\n # and our ellipsis is 3 too, so we can't really show anything.\n # This is the default ellipsis if your locale doesn't allow \"…\".\n foo\n ...\n\n > string shorten --char=\"\" --max 4 abcdef 123456\n # Leaving the char empty makes us not add an ellipsis\n # So this truncates at 4 columns:\n abcd\n 1234\n\n > touch \"a multiline\"\\n\"file\"\n > for file in *; string shorten -N -- $file; end\n # Shorten the multiline file so we only show one line per file:\n a multiline…\n\n > ss -p | string shorten -m$COLUMNS -c \"\"\n # `ss` from Linux' iproute2 shows socket information, but prints extremely long lines.\n # This shortens input so it fits on the screen without overflowing lines.\n\n > git branch | string match -rg '^\\* (.*)' | string shorten -m20\n # Take the current git branch and shorten it at 20 columns.\n # Here the branch is \"builtin-path-with-expand\"\n builtin-path-with-e…\n\n > git branch | string match -rg '^\\* (.*)' | string shorten -m20 --left\n # Taking 20 columns from the right instead:\n …in-path-with-expand\n\nSEE ALSO\n\n• string's pad subcommand does the inverse of this command, adding\n padding to a specific width instead.\n\n• The printf command can do simple padding, for example printf %10s\\n\n works like string pad -w10.\n\n• string length with the --visible option can be used to show what\n fish thinks the width is.\n\nREPEAT SUBCOMMAND\nstring repeat [(-n | --count) COUNT] [(-m | --max) MAX] [-N | --no-newline]\n [-q | --quiet] [STRING ...]\n\nstring repeat repeats the STRING -n or --count times. The -m or --max\noption will limit the number of outputted characters (excluding the\nnewline). This option can be used by itself or in conjunction with\n--count. If both --count and --max are present, max char will be\noutputed unless the final repeated string size is less than max, in\nthat case, the string will repeat until count has been reached. Both\n--count and --max will accept a number greater than or equal to zero,\nin the case of zero, nothing will be outputed. If -N or --no-newline\nis given, the output won't contain a newline character at the end.\nExit status: 0 if yielded string is not empty, 1 otherwise.\n\n Examples\n Repeat Examples\n\n > string repeat -n 2 'foo '\n foo foo\n\n > echo foo | string repeat -n 2\n foofoo\n\n > string repeat -n 2 -m 5 'foo'\n foofo\n\n > string repeat -m 5 'foo'\n foofo\n\nREPLACE SUBCOMMAND\nstring replace [-a | --all] [-f | --filter] [-i | --ignore-case]\n [-r | --regex] [-q | --quiet] PATTERN REPLACEMENT [STRING ...]\n\nstring replace is similar to string match but replaces\nnon-overlapping matching substrings with a replacement string and\nprints the result. By default, PATTERN is treated as a literal\nsubstring to be matched.\n\nIf -r or --regex is given, PATTERN is interpreted as a\nPerl-compatible regular expression, and REPLACEMENT can contain\nC-style escape sequences like t as well as references to capturing\ngroups by number or name as $n or ${n}.\n\nIf you specify the -f or --filter flag then each input string is\nprinted only if a replacement was done. This is useful where you\nwould otherwise use this idiom: a_cmd | string match pattern | string\nreplace pattern new_pattern. You can instead just write a_cmd |\nstring replace --filter pattern new_pattern.\n\nExit status: 0 if at least one replacement was performed, or 1\notherwise.\n\n Replace Literal Examples\n\n > string replace is was 'blue is my favorite'\n blue was my favorite\n\n > string replace 3rd last 1st 2nd 3rd\n 1st\n 2nd\n last\n\n > string replace -a ' ' 'spaces to underscores'\n spaces_to_underscores\n\n Replace Regex Examples\n\n > string replace -r -a '[^\\d.]+' ' ' '0 one two 3.14 four 5x'\n 0 3.14 5\n\n > string replace -r '(\\w+)\\s+(\\w+)' '$2 $1 $$' 'left right'\n right left $\n\n > string replace -r '\\s*newline\\s*' '\\n' 'put a newline here'\n put a\n here\n\nSPLIT AND SPLIT0 SUBCOMMANDS\nstring split [(-f | --fields) FIELDS] [(-m | --max) MAX] [-n | --no-empty]\n [-q | --quiet] [-r | --right] SEP [STRING ...]\nstring split0 [(-f | --fields) FIELDS] [(-m | --max) MAX] [-n | --no-empty]\n [-q | --quiet] [-r | --right] [STRING ...]\n\nstring split splits each STRING on the separator SEP, which can be an\nempty string. If -m or --max is specified, at most MAX splits are\ndone on each STRING. If -r or --right is given, splitting is\nperformed right-to-left. This is useful in combination with -m or\n--max. With -n or --no-empty, empty results are excluded from\nconsideration (e.g. hello\\n\\nworld would expand to two strings and\nnot three). Exit status: 0 if at least one split was performed, or 1\notherwise.\n\nUse -f or --fields to print out specific fields. FIELDS is a\ncomma-separated string of field numbers and/or spans. Each field is\none-indexed, and will be printed on separate lines. If a given field\ndoes not exist, then the command exits with status 1 and does not\nprint anything, unless --allow-empty is used.\n\nSee also the --delimiter option of the read command.\n\nstring split0 splits each STRING on the zero byte (NUL). Options are\nthe same as string split except that no separator is given.\n\nsplit0 has the important property that its output is not further\nsplit when used in a command substitution, allowing for the command\nsubstitution to produce elements containing newlines. This is most\nuseful when used with Unix tools that produce zero bytes, such as\nfind -print0 or sort -z. See split0 examples below.\n\n Examples\n\n > string split . example.com\n example\n com\n\n > string split -r -m1 / /usr/local/bin/fish\n /usr/local/bin\n fish\n\n > string split '' abc\n a\n b\n c\n\n > string split --allow-empty -f1,3-4,5 '' abcd\n a\n c\n d\n\n NUL Delimited Examples\n\n > # Count files in a directory, without being confused by newlines.\n > count (find . -print0 | string split0)\n 42\n\n > # Sort a list of elements which may contain newlines\n > set foo beta alpha\\ngamma\n > set foo (string join0 $foo | sort -z | string split0)\n > string escape $foo[1]\n alpha\\ngamma\n\nSUB SUBCOMMAND\nstring sub [(-s | --start) START] [(-e | --end) END] [(-l | --length) LENGTH]\n [-q | --quiet] [STRING ...]\n\nstring sub prints a substring of each string argument. The start/end\nof the substring can be specified with -s/-e or --start/--end\nfollowed by a 1-based index value. Positive index values are relative\nto the start of the string and negative index values are relative to\nthe end of the string. The default start value is 1. The length of\nthe substring can be specified with -l or --length. If the length or\nend is not specified, the substring continues to the end of each\nSTRING. Exit status: 0 if at least one substring operation was\nperformed, 1 otherwise. --length is mutually exclusive with --end.\n\n Examples\n\n > string sub --length 2 abcde\n ab\n\n > string sub -s 2 -l 2 abcde\n bc\n\n > string sub --start=-2 abcde\n de\n\n > string sub --end=3 abcde\n abc\n\n > string sub -e -1 abcde\n abcd\n\n > string sub -s 2 -e -1 abcde\n bcd\n\n > string sub -s -3 -e -2 abcde\n c\n\nTRIM SUBCOMMAND\nstring trim [-l | --left] [-r | --right] [(-c | --chars) CHARS]\n [-q | --quiet] [STRING ...]\n\nstring trim removes leading and trailing whitespace from each STRING.\nIf -l or --left is given, only leading whitespace is removed. If -r\nor --right is given, only trailing whitespace is trimmed. The -c or\n--chars switch causes the characters in CHARS to be removed instead\nof whitespace. Exit status: 0 if at least one character was trimmed,\nor 1 otherwise.\n\n Examples\n\n > string trim ' abc '\n abc\n\n > string trim --right --chars=yz xyzzy zany\n x\n zan\n\nUPPER SUBCOMMAND\nstring upper [-q | --quiet] [STRING ...]\n\nstring upper converts each string argument to uppercase. Exit status:\n0 if at least one string was converted to uppercase, else 1. This\nmeans that in conjunction with the -q flag you can readily test\nwhether a string is already uppercase.\n\nREGULAR EXPRESSIONS\nBoth the match and replace subcommand support regular expressions\nwhen used with the -r or --regex option. The dialect is that of\nPCRE2.\n\nIn general, special characters are special by default, so a+ matches\none or more \"a\"s, while a\\+ matches an \"a\" and then a \"+\". (a+)\nmatches one or more \"a\"s in a capturing group ((?:XXXX) denotes a\nnon-capturing group). For the replacement parameter of replace, $n\nrefers to the n-th group of the match. In the match parameter, \\n\n(e.g. \\1) refers back to groups.\n\nSome features include repetitions:\n\n• * refers to 0 or more repetitions of the previous expression\n\n• + 1 or more\n\n• ? 0 or 1.\n\n• {n} to exactly n (where n is a number)\n\n• {n,m} at least n, no more than m.\n\n• {n,} n or more\n\nCharacter classes, some of the more important:\n\n• . any character except newline\n\n• \\d a decimal digit and \\D, not a decimal digit\n\n• \\s whitespace and \\S, not whitespace\n\n• \\w a \"word\" character and \\W, a \"non-word\" character\n\n• [...] (where \"...\" is some characters) is a character set\n\n• [^...] is the inverse of the given character set\n\n• [x-y] is the range of characters from x-y\n\n• [[:xxx:]] is a named character set\n\n• [[:^xxx:]] is the inverse of a named character set\n\n• [[:alnum:]] : \"alphanumeric\"\n\n• [[:alpha:]] : \"alphabetic\"\n\n• [[:ascii:]] : \"0-127\"\n\n• [[:blank:]] : \"space or tab\"\n\n• [[:cntrl:]] : \"control character\"\n\n• [[:digit:]] : \"decimal digit\"\n\n• [[:graph:]] : \"printing, excluding space\"\n\n• [[:lower:]] : \"lower case letter\"\n\n• [[:print:]] : \"printing, including space\"\n\n• [[:punct:]] : \"printing, excluding alphanumeric\"\n\n• [[:space:]] : \"white space\"\n\n• [[:upper:]] : \"upper case letter\"\n\n• [[:word:]] : \"same as w\"\n\n• [[:xdigit:]] : \"hexadecimal digit\"\n\nGroups:\n\n• (...) is a capturing group\n\n• (?:...) is a non-capturing group\n\n• \\n is a backreference (where n is the number of the group, starting\n with 1)\n\n• $n is a reference from the replacement expression to a group in the\n match expression.\n\nAnd some other things:\n\n• \\b denotes a word boundary, \\B is not a word boundary.\n\n• ^ is the start of the string or line, $ the end.\n\n• | is \"alternation\", i.e. the \"or\".\n\nCOMPARISON TO OTHER TOOLS\nMost operations string supports can also be done by external tools.\nSome of these include grep, sed and cut.\n\nIf you are familiar with these, it is useful to know how string\ndiffers from them.\n\nIn contrast to these classics, string reads input either from stdin\nor as arguments. string also does not deal with files, so it requires\nredirections to be used with them.\n\nIn contrast to grep, string's match defaults to glob-mode, while\nreplace defaults to literal matching. If set to regex-mode, they use\nPCRE regular expressions, which is comparable to grep's -P option.\nmatch defaults to printing just the match, which is like grep with -o\n(use --entire to enable grep-like behavior).\n\nLike sed's s/old/new/ command, string replace still prints strings\nthat don't match. sed's -n in combination with a /p modifier or\ncommand is like string replace -f.\n\nstring split somedelimiter is a replacement for tr somedelimiter \\n.", + "args": "string collect [-a | --allow-empty] [-N | --no-trim-newlines] [STRING ...]\nstring escape [-n | --no-quoted] [--style=] [STRING ...]\nstring join [-q | --quiet] [-n | --no-empty] SEP [STRING ...]\nstring join0 [-q | --quiet] [STRING ...]\nstring length [-q | --quiet] [STRING ...]\nstring lower [-q | --quiet] [STRING ...]\nstring match [-a | --all] [-e | --entire] [-i | --ignore-case]\n [-g | --groups-only] [-r | --regex] [-n | --index]\n [-q | --quiet] [-v | --invert]\n PATTERN [STRING ...]\nstring pad [-r | --right] [(-c | --char) CHAR] [(-w | --width) INTEGER]\n [STRING ...]\nstring repeat [(-n | --count) COUNT] [(-m | --max) MAX] [-N | --no-newline]\n [-q | --quiet] [STRING ...]\nstring replace [-a | --all] [-f | --filter] [-i | --ignore-case]\n [-r | --regex] [-q | --quiet] PATTERN REPLACE [STRING ...]\nstring shorten [(-c | --char) CHARS] [(-m | --max) INTEGER]\n [-N | --no-newline] [-l | --left] [-q | --quiet] [STRING ...]\nstring split [(-f | --fields) FIELDS] [(-m | --max) MAX] [-n | --no-empty]\n [-q | --quiet] [-r | --right] SEP [STRING ...]\nstring split0 [(-f | --fields) FIELDS] [(-m | --max) MAX] [-n | --no-empty]\n [-q | --quiet] [-r | --right] [STRING ...]\nstring sub [(-s | --start) START] [(-e | --end) END] [(-l | --length) LENGTH]\n [-q | --quiet] [STRING ...]\nstring trim [-l | --left] [-r | --right] [(-c | --chars) CHARS]\n [-q | --quiet] [STRING ...]\nstring unescape [--style=] [STRING ...]\nstring upper [-q | --quiet] [STRING ...]" + }, + "switch": { + "shortDescription": "conditionally execute a block of commands", + "description": "switch performs one of several blocks of commands, depending on\nwhether a specified value equals one of several globbed values. case\nis used together with the switch statement in order to determine\nwhich block should be executed.\n\nEach case command is given one or more parameters. The first case\ncommand with a parameter that matches the string specified in the\nswitch command will be evaluated. case parameters may contain globs.\nThese need to be escaped or quoted in order to avoid regular glob\nexpansion using filenames.\n\nNote that fish does not fall through on case statements. Only the\nfirst matching case is executed.\n\nNote that break cannot be used to exit a case/switch block early like\nin other languages. It can only be used in loops.\n\nNote that command substitutions in a case statement will be evaluated\neven if its body is not taken. All substitutions, including command\nsubstitutions, must be performed before the value can be compared\nagainst the parameter.\n\nEXAMPLE\nIf the variable $animal contains the name of an animal, the following\ncode would attempt to classify it:\n\n switch $animal\n case cat\n echo evil\n case wolf dog human moose dolphin whale\n echo mammal\n case duck goose albatross\n echo bird\n case shark trout stingray\n echo fish\n case '*'\n echo I have no idea what a $animal is\n end\n\nIf the above code was run with $animal set to whale, the output would\nbe mammal.", + "args": "switch VALUE; [case [GLOB ...]; [COMMANDS ...]; ...] end" + }, + "test": { + "shortDescription": "Evaluate conditional expressions", + "description": "The `test` command evaluates conditional expressions and sets the exit status to 0 if the expression is true, and 1 if it is false. It supports various operators to evaluate expressions related to strings, numbers, and file attributes.", + "args": "EXPRESSION" + }, + "time": { + "shortDescription": "measure how long a command or block takes", + "description": "NOTE: This page documents the fish keyword time. To see the\ndocumentation on the time command you might have, use command man\ntime.\n\ntime causes fish to measure how long a command takes and print the\nresults afterwards. The command can be a simple fish command or a\nblock. The results can not currently be redirected.\n\nFor checking timing after a command has completed, check\n$CMD_DURATION.\n\nYour system most likely also has a time command. To use that use\nsomething like command time, as in command time sleep 10. Because\nit's not inside fish, it won't have access to fish functions and\nwon't be able to time blocks and such.\n\nHOW TO INTERPRET THE OUTPUT\nTime outputs a few different values. Let's look at an example:\n\n > time string repeat -n 10000000 y\\n | command grep y >/dev/null\n _______________________________________________________\n Executed in 805.98 millis fish external\n usr time 798.88 millis 763.88 millis 34.99 millis\n sys time 141.22 millis 40.20 millis 101.02 millis\n\nThe time after \"Executed in\" is what is known as the \"wall-clock\ntime\". It is simply a measure of how long it took from the start of\nthe command until it finished. Typically it is reasonably close to\nCMD_DURATION, except for a slight skew because the two are taken at\nslightly different times.\n\nThe other times are all measures of CPU time. That means they measure\nhow long the CPU was used in this part, and they count multiple cores\nseparately. So a program with four threads using all CPU for a second\nwill have a time of 4 seconds.\n\nThe \"usr\" time is how much CPU time was spent inside the program\nitself, the \"sys\" time is how long was spent in the kernel on behalf\nof that program.\n\nThe \"fish\" time is how much CPU was spent in fish, the \"external\"\ntime how much was spent in external commands.\n\nSo in this example, since string is a builtin, everything that string\nrepeat did is accounted to fish. Any time it spends doing syscalls\nlike write() is accounted for in the fish/sys time.\n\nAnd grep here is explicitly invoked as an external command, so its\ntimes will be counted in the \"external\" column.\n\nNote that, as in this example, the CPU times can add up to more than\nthe execution time. This is because things can be done in parallel -\ngrep can match while string repeat writes.\n\nEXAMPLE\n(for obvious reasons exact results will vary on your system)\n\n > time sleep 1s\n\n _______________________________________________________\n Executed in 1,01 secs fish external\n usr time 2,32 millis 0,00 micros 2,32 millis\n sys time 0,88 millis 877,00 micros 0,00 millis\n\n > time for i in 1 2 3; sleep 1s; end\n\n _______________________________________________________\n Executed in 3,01 secs fish external\n usr time 9,16 millis 2,94 millis 6,23 millis\n sys time 0,23 millis 0,00 millis 0,23 millis\n\nInline variable assignments need to follow the time keyword:\n\n > time a_moment=1.5m sleep $a_moment\n\n _______________________________________________________\n Executed in 90.00 secs fish external\n usr time 4.62 millis 4.62 millis 0.00 millis\n sys time 2.35 millis 0.41 millis 1.95 millis", + "args": "time COMMAND" + }, + "true": { + "shortDescription": "Return a successful result", + "description": "The `true` command always returns a successful (zero) exit status. It is often used in scripts and conditional statements where an unconditional success result is needed." + }, + "type": { + "shortDescription": "locate a command and describe its type", + "description": "With no options, type indicates how each NAME would be interpreted if\nused as a command name.\n\nThe following options are available:\n\n-a or --all\n Prints all of possible definitions of the specified names.\n\n-s or --short\n Suppresses function expansion when used with no options or\n with -a/--all.\n\n-f or --no-functions\n Suppresses function and builtin lookup.\n\n-t or --type\n Prints function, builtin, or file if NAME is a shell function,\n builtin, or disk file, respectively.\n\n-p or --path\n Prints the path to NAME if NAME resolves to an executable file\n in PATH, the path to the script containing the definition of\n the function NAME if NAME resolves to a function loaded from a\n file on disk (i.e. not interactively defined at the prompt),\n or nothing otherwise.\n\n-P or --force-path\n Returns the path to the executable file NAME, presuming NAME\n is found in the PATH environment variable, or nothing\n otherwise. --force-path explicitly resolves only the path to\n executable files in PATH, regardless of whether NAME is\n shadowed by a function or builtin with the same name.\n\n-q or --query\n Suppresses all output; this is useful when testing the exit\n status. For compatibility with old fish versions this is also\n --quiet.\n\n-h or --help\n Displays help about using this command.\n\nThe -q, -p, -t and -P flags (and their long flag aliases) are\nmutually exclusive. Only one can be specified at a time.\n\ntype returns 0 if at least one entry was found, 1 otherwise, and 2\nfor invalid options or option combinations.\n\nEXAMPLE\n\n > type fg\n fg is a builtin", + "args": "type [OPTIONS] NAME [...]" + }, + "ulimit": { + "shortDescription": "set or get resource usage limits", + "description": "ulimit sets or outputs the resource usage limits of the shell and any\nprocesses spawned by it. If a new limit value is omitted, the current\nvalue of the limit of the resource is printed; otherwise, the\nspecified limit is set to the new value.\n\nUse one of the following switches to specify which resource limit to\nset or report:\n\n-b or --socket-buffers\n The maximum size of socket buffers.\n\n-c or --core-size\n The maximum size of core files created. By setting this limit\n to zero, core dumps can be disabled.\n\n-d or --data-size\n The maximum size of a process' data segment.\n\n-e or --nice\n Controls the maximum nice value; on Linux, this value is\n subtracted from 20 to give the effective value.\n\n-f or --file-size\n The maximum size of files created by a process.\n\n-i or --pending-signals\n The maximum number of signals that may be queued.\n\n-l or --lock-size\n The maximum size that may be locked into memory.\n\n-m or --resident-set-size\n The maximum resident set size.\n\n-n or --file-descriptor-count\n The maximum number of open file descriptors.\n\n-q or --queue-size\n The maximum size of data in POSIX message queues.\n\n-r or --realtime-priority\n The maximum realtime scheduling priority.\n\n-s or --stack-size\n The maximum stack size.\n\n-t or --cpu-time\n The maximum amount of CPU time in seconds.\n\n-u or --process-count\n The maximum number of processes available to the current user.\n\n-w or --swap-size\n The maximum swap space available to the current user.\n\n-v or --virtual-memory-size\n The maximum amount of virtual memory available to the shell.\n\n-y or --realtime-maxtime\n The maximum contiguous realtime CPU time in microseconds.\n\n-K or --kernel-queues\n The maximum number of kqueues (kernel queues) for the current\n user.\n\n-P or --ptys\n The maximum number of pseudo-terminals for the current user.\n\n-T or --threads\n The maximum number of simultaneous threads for the current\n user.\n\nNote that not all these limits are available in all operating\nsystems; consult the documentation for setrlimit in your operating\nsystem.\n\nThe value of limit can be a number in the unit specified for the\nresource or one of the special values hard, soft, or unlimited, which\nstand for the current hard limit, the current soft limit, and no\nlimit, respectively.\n\nIf limit is given, it is the new value of the specified resource. If\nno option is given, then -f is assumed. Values are in kilobytes,\nexcept for -t, which is in seconds and -n and -u, which are unscaled\nvalues. The exit status is 0 unless an invalid option or argument is\nsupplied, or an error occurs while setting a new limit.\n\nulimit also accepts the following options that determine what type of\nlimit to set:\n\n-H or --hard\n Sets hard resource limit.\n\n-S or --soft\n Sets soft resource limit.\n\nA hard limit can only be decreased. Once it is set it cannot be\nincreased; a soft limit may be increased up to the value of the hard\nlimit. If neither -H nor -S is specified, both the soft and hard\nlimits are updated when assigning a new limit value, and the soft\nlimit is used when reporting the current value.\n\nThe following additional options are also understood by ulimit:\n\n-a or --all\n Prints all current limits.\n\n-h or --help\n Displays help about using this command.\n\nThe fish implementation of ulimit should behave identically to the\nimplementation in bash, except for these differences:\n\n• Fish ulimit supports GNU-style long options for all switches.\n\n• Fish ulimit does not support the -p option for getting the pipe\n size. The bash implementation consists of a compile-time check that\n empirically guesses this number by writing to a pipe and waiting\n for SIGPIPE. Fish does not do this because this method of\n determining pipe size is unreliable. Depending on bash version,\n there may also be further additional limits to set in bash that do\n not exist in fish.\n\n• Fish ulimit does not support getting or setting multiple limits in\n one command, except reporting all values using the -a switch.\n\nEXAMPLE\nulimit -Hs 64 sets the hard stack size limit to 64 kB.", + "args": "ulimit [OPTIONS] [LIMIT]" + }, + "wait": { + "shortDescription": "wait for jobs to complete", + "description": "wait waits for child jobs to complete.\n\nIf a PID is specified, the command waits for the job that the process\nwith that process ID belongs to.\n\nIf a PROCESS_NAME is specified, the command waits for the jobs that\nthe matched processes belong to.\n\nIf neither a pid nor a process name is specified, the command waits\nfor all background jobs.\n\nIf the -n or --any flag is provided, the command returns as soon as\nthe first job completes. If it is not provided, it returns after all\njobs complete.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\n\n sleep 10 &\n wait $last_pid\n\nspawns sleep in the background, and then waits until it finishes.\n\n for i in (seq 1 5); sleep 10 &; end\n wait\n\nspawns five jobs in the background, and then waits until all of them\nfinishes.\n\n for i in (seq 1 5); sleep 10 &; end\n hoge &\n wait sleep\n\nspawns five jobs and hoge in the background, and then waits until all\nsleeps finish, and doesn't wait for hoge finishing.", + "args": "wait [-n | --any] [PID | PROCESS_NAME] ..." + }, + "while": { + "shortDescription": "perform a set of commands multiple times", + "description": "while repeatedly executes CONDITION, and if the exit status is 0,\nthen executes COMMANDS.\n\nThe exit status of the while loop is the exit status of the last\niteration of the COMMANDS executed, or 0 if none were executed. (This\nmatches other shells and is POSIX-compatible.)\n\nYou can use and or or for complex conditions. Even more complex\ncontrol can be achieved with while true containing a break.\n\nThe -h or --help option displays help about using this command.\n\nEXAMPLE\n\n while test -f foo.txt; or test -f bar.txt ; echo file exists; sleep 10; end\n # outputs 'file exists' at 10 second intervals,\n # as long as the file foo.txt or bar.txt exists.", + "args": "while CONDITION; COMMANDS; end" + } +} as const; \ No newline at end of file diff --git a/extensions/terminal-suggest/src/shell/zsh.ts b/extensions/terminal-suggest/src/shell/zsh.ts index 989d49a5..64ba4ed3 100644 --- a/extensions/terminal-suggest/src/shell/zsh.ts +++ b/extensions/terminal-suggest/src/shell/zsh.ts @@ -19,7 +19,8 @@ export async function getZshGlobals(options: ExecOptionsWithStringEncoding, exis } async function getAliases(options: ExecOptionsWithStringEncoding): Promise { - return getAliasesHelper('zsh', ['-ic', 'alias'], /^(?[a-zA-Z0-9\._:-]+)=(?['"]?)(?.+?)\k$/, options); + const args = process.platform === 'darwin' ? ['-icl', 'alias'] : ['-ic', 'alias']; + return getAliasesHelper('zsh', args, /^(?[a-zA-Z0-9\._:-]+)=(?['"]?)(?.+?)\k$/, options); } async function getBuiltins( diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 3f181bcd..8ca8f977 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -10,9 +10,10 @@ import * as vscode from 'vscode'; import cdSpec from './completions/cd'; import codeCompletionSpec from './completions/code'; import codeInsidersCompletionSpec from './completions/code-insiders'; +import npxCompletionSpec from './completions/npx'; import setLocationSpec from './completions/set-location'; import { upstreamSpecs } from './constants'; -import { PathExecutableCache } from './env/pathExecutableCache'; +import { ITerminalEnvironment, PathExecutableCache, watchPathDirectories } from './env/pathExecutableCache'; import { osIsWindows } from './helpers/os'; import { getFriendlyResourcePath } from './helpers/uri'; import { getBashGlobals } from './shell/bash'; @@ -24,22 +25,16 @@ import type { ICompletionResource } from './types'; import { createCompletionItem } from './helpers/completionItem'; import { getFigSuggestions } from './fig/figInterface'; import { executeCommand, executeCommandTimeout, IFigExecuteExternals } from './fig/execute'; +import { createTimeoutPromise } from './helpers/promise'; +import codeTunnelCompletionSpec from './completions/code-tunnel'; +import codeTunnelInsidersCompletionSpec from './completions/code-tunnel-insiders'; -// TODO: remove once API is finalized export const enum TerminalShellType { - Sh = 1, - Bash = 2, - Fish = 3, - Csh = 4, - Ksh = 5, - Zsh = 6, - CommandPrompt = 7, - GitBash = 8, - PowerShell = 9, - Python = 10, - Julia = 11, - NuShell = 12, - Node = 13 + Bash = 'bash', + Fish = 'fish', + Zsh = 'zsh', + PowerShell = 'pwsh', + Python = 'python' } const isWindows = osIsWindows(); @@ -50,6 +45,9 @@ export const availableSpecs: Fig.Spec[] = [ cdSpec, codeInsidersCompletionSpec, codeCompletionSpec, + codeTunnelCompletionSpec, + codeTunnelInsidersCompletionSpec, + npxCompletionSpec, setLocationSpec, ]; for (const spec of upstreamSpecs) { @@ -70,11 +68,10 @@ async function getShellGlobals(shellType: TerminalShellType, existingCommands?: if (cachedCommands) { return cachedCommands; } - const shell = getShell(shellType); - if (!shell) { + if (!shellType) { return; } - const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; + const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell: shellType }; const mixedCommands: (string | ICompletionResource)[] | undefined = await getShellSpecificGlobals.get(shellType)?.(options, existingCommands); const normalizedCommands = mixedCommands?.map(command => typeof command === 'string' ? ({ label: command }) : command); cachedGlobals.set(shellType, normalizedCommands); @@ -86,33 +83,56 @@ async function getShellGlobals(shellType: TerminalShellType, existingCommands?: } } + export async function activate(context: vscode.ExtensionContext) { pathExecutableCache = new PathExecutableCache(); context.subscriptions.push(pathExecutableCache); - + let currentTerminalEnv: ITerminalEnvironment = process.env; context.subscriptions.push(vscode.window.registerTerminalCompletionProvider({ id: 'terminal-suggest', async provideTerminalCompletions(terminal: vscode.Terminal, terminalContext: vscode.TerminalCompletionContext, token: vscode.CancellationToken): Promise { + currentTerminalEnv = terminal.shellIntegration?.env?.value ?? process.env; if (token.isCancellationRequested) { + console.debug('#terminalCompletions token cancellation requested'); return; } - const shellType: TerminalShellType | undefined = 'shellType' in terminal.state ? terminal.state.shellType as TerminalShellType : undefined; - if (!shellType) { + const shellType: string | undefined = 'shell' in terminal.state ? terminal.state.shell as string : undefined; + const terminalShellType = getTerminalShellType(shellType); + if (!terminalShellType) { + console.debug('#terminalCompletions No shell type found for terminal'); return; } const commandsInPath = await pathExecutableCache.getExecutablesInPath(terminal.shellIntegration?.env?.value); - const shellGlobals = await getShellGlobals(shellType, commandsInPath?.labels) ?? []; + const shellGlobals = await getShellGlobals(terminalShellType, commandsInPath?.labels) ?? []; if (!commandsInPath?.completionResources) { + console.debug('#terminalCompletions No commands found in path'); return; } // Order is important here, add shell globals first so they are prioritized over path commands const commands = [...shellGlobals, ...commandsInPath.completionResources]; const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); const pathSeparator = isWindows ? '\\' : '/'; - const tokenType = getTokenType(terminalContext, shellType); - const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, tokenType, terminal.shellIntegration?.cwd, getEnvAsRecord(terminal.shellIntegration?.env?.value), terminal.name, token); + const tokenType = getTokenType(terminalContext, terminalShellType); + const result = await Promise.race([ + getCompletionItemsFromSpecs( + availableSpecs, + terminalContext, + commands, + prefix, + tokenType, + terminal.shellIntegration?.cwd, + getEnvAsRecord(currentTerminalEnv), + terminal.name, + token + ), + createTimeoutPromise(300, undefined) + ]); + if (!result) { + return; + } + if (terminal.shellIntegration?.env) { const homeDirCompletion = result.items.find(i => i.label === '~'); if (homeDirCompletion && terminal.shellIntegration.env?.value?.HOME) { @@ -127,6 +147,7 @@ export async function activate(context: vscode.ExtensionContext) { return result.items; } }, '/', '\\')); + await watchPathDirectories(context, currentTerminalEnv, pathExecutableCache); } /** @@ -169,7 +190,7 @@ export async function resolveCwdFromPrefix(prefix: string, currentCwd?: vscode.U // Ignore errors } - // If the prefix is not a folder, return the current cwd + // No valid path found return undefined; } @@ -250,7 +271,8 @@ export async function getCompletionItemsFromSpecs( prefix, command, command.detail, - command.documentation + command.documentation, + vscode.TerminalCompletionItemKind.Method )); labels.add(commandTextLabel); } else { @@ -260,6 +282,7 @@ export async function getCompletionItemsFromSpecs( } const preferredItem = compareItems(existingItem, command); if (preferredItem) { + preferredItem.kind = vscode.TerminalCompletionItemKind.Method; items.splice(items.indexOf(existingItem), 1, preferredItem); } } @@ -297,30 +320,56 @@ function compareItems(existingItem: vscode.TerminalCompletionItem, command: ICom } } -function getShell(shellType: TerminalShellType): string | undefined { - switch (shellType) { - case TerminalShellType.Bash: - return 'bash'; - case TerminalShellType.Fish: - return 'fish'; - case TerminalShellType.Zsh: - return 'zsh'; - case TerminalShellType.PowerShell: - return 'pwsh'; - default: { - return undefined; +function getEnvAsRecord(shellIntegrationEnv: ITerminalEnvironment): Record { + const env: Record = {}; + for (const [key, value] of Object.entries(shellIntegrationEnv ?? process.env)) { + if (typeof value === 'string') { + env[key] = value; } } -} - -function getEnvAsRecord(shellIntegrationEnv: { [key: string]: string | undefined } | undefined): Record { - const env: Record = {}; - if (shellIntegrationEnv) { - for (const [key, value] of Object.entries(shellIntegrationEnv)) { - if (typeof value === 'string') { - env[key] = value; - } - } + if (!shellIntegrationEnv) { + sanitizeProcessEnvironment(env); } return env; } + +function getTerminalShellType(shellType: string | undefined): TerminalShellType | undefined { + switch (shellType) { + case 'bash': + return TerminalShellType.Bash; + case 'zsh': + return TerminalShellType.Zsh; + case 'pwsh': + return TerminalShellType.PowerShell; + case 'fish': + return TerminalShellType.Fish; + case 'python': + return TerminalShellType.Python; + default: + return undefined; + } +} + +export function sanitizeProcessEnvironment(env: Record, ...preserve: string[]): void { + const set = preserve.reduce>((set, key) => { + set[key] = true; + return set; + }, {}); + const keysToRemove = [ + /^ELECTRON_.$/, + /^VSCODE_(?!(PORTABLE|SHELL_LOGIN|ENV_REPLACE|ENV_APPEND|ENV_PREPEND)).$/, + /^SNAP(|_.*)$/, + /^GDK_PIXBUF_.$/, + ]; + const envKeys = Object.keys(env); + envKeys + .filter(key => !set[key]) + .forEach(envKey => { + for (let i = 0; i < keysToRemove.length; i++) { + if (envKey.search(keysToRemove[i]) !== -1) { + delete env[envKey]; + break; + } + } + }); +} diff --git a/extensions/terminal-suggest/src/test/completions/code.test.ts b/extensions/terminal-suggest/src/test/completions/code.test.ts index e90d3120..b327e3e4 100644 --- a/extensions/terminal-suggest/src/test/completions/code.test.ts +++ b/extensions/terminal-suggest/src/test/completions/code.test.ts @@ -8,8 +8,57 @@ import codeCompletionSpec from '../../completions/code'; import { testPaths, type ISuiteSpec, type ITestSpec } from '../helpers'; import codeInsidersCompletionSpec from '../../completions/code-insiders'; -export const codeSpecOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; - +export const codeSpecOptionsAndSubcommands = [ + '-a ', + '-d ', + '-g ', + '-h', + '-m ', + '-n', + '-r', + '-s', + '-v', + '-w', + '-', + '--add ', + '--category ', + '--diff ', + '--disable-extension ', + '--disable-extensions', + '--disable-gpu', + '--enable-proposed-api', + '--extensions-dir

', + '--goto ', + '--help', + '--inspect-brk-extensions ', + '--inspect-extensions ', + '--install-extension ', + '--list-extensions', + '--locale ', + '--locate-shell-integration-path ', + '--log ', + '--max-memory ', + '--merge ', + '--new-window', + '--pre-release', + '--prof-startup', + '--profile ', + '--reuse-window', + '--show-versions', + '--status', + '--sync ', + '--telemetry', + '--uninstall-extension ', + '--user-data-dir ', + '--verbose', + '--version', + '--wait', + 'tunnel', + 'serve-web', + 'help', + 'status', + 'version' +]; export function createCodeTestSpecs(executable: string): ITestSpec[] { const localeOptions = ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW']; @@ -29,7 +78,7 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] { ...typingTests, // Basic arguments - { input: `${executable} |`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} |`, expectedCompletions: codeSpecOptionsAndSubcommands, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, { input: `${executable} --locale |`, expectedCompletions: localeOptions }, { input: `${executable} --diff |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, { input: `${executable} --diff ./file1 |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, @@ -39,18 +88,19 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] { { input: `${executable} --goto |`, expectedResourceRequests: { type: 'files', cwd: testPaths.cwd } }, { input: `${executable} --user-data-dir |`, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, { input: `${executable} --profile |` }, - { input: `${executable} --install-extension |` }, - { input: `${executable} --uninstall-extension |` }, + { input: `${executable} --install-extension |`, expectedCompletions: [executable], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} --uninstall-extension |`, expectedCompletions: [executable] }, + { input: `${executable} --disable-extension |`, expectedCompletions: [executable] }, { input: `${executable} --log |`, expectedCompletions: logOptions }, { input: `${executable} --sync |`, expectedCompletions: syncOptions }, { input: `${executable} --extensions-dir |`, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } }, - { input: `${executable} --list-extensions |`, expectedCompletions: codeSpecOptions.filter(c => c !== '--list-extensions'), expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, - { input: `${executable} --show-versions |`, expectedCompletions: codeSpecOptions.filter(c => c !== '--show-versions'), expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} --list-extensions |`, expectedCompletions: codeSpecOptionsAndSubcommands.filter(c => c !== '--list-extensions'), expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} --show-versions |`, expectedCompletions: codeSpecOptionsAndSubcommands.filter(c => c !== '--show-versions'), expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, { input: `${executable} --category |`, expectedCompletions: categoryOptions }, { input: `${executable} --category a|`, expectedCompletions: categoryOptions }, // Middle of command - { input: `${executable} | --locale`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, + { input: `${executable} | --locale`, expectedCompletions: codeSpecOptionsAndSubcommands, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } }, ]; } diff --git a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts index 63ada99f..aab14e3d 100644 --- a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts +++ b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts @@ -10,7 +10,7 @@ import lsSpec from '../../../completions/upstream/ls'; const allOptions = [ '-%', '-,', - '--color', + '--color ', '-1', '-@', '-A', diff --git a/extensions/terminal-suggest/src/test/completions/upstream/mkdir.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/mkdir.test.ts index 3935191e..5daeeb25 100644 --- a/extensions/terminal-suggest/src/test/completions/upstream/mkdir.test.ts +++ b/extensions/terminal-suggest/src/test/completions/upstream/mkdir.test.ts @@ -8,14 +8,14 @@ import { testPaths, type ISuiteSpec } from '../../helpers'; import mkdirSpec from '../../../completions/upstream/mkdir'; const allOptions = [ - '--context', + '--context ', '--help', - '--mode', + '--mode ', '--parents', '--verbose', '--version', - '-Z', - '-m', + '-Z ', + '-m ', '-p', '-v', ]; diff --git a/extensions/terminal-suggest/src/test/completions/upstream/touch.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/touch.test.ts index 4373025c..dd03b062 100644 --- a/extensions/terminal-suggest/src/test/completions/upstream/touch.test.ts +++ b/extensions/terminal-suggest/src/test/completions/upstream/touch.test.ts @@ -8,14 +8,14 @@ import { testPaths, type ISuiteSpec } from '../../helpers'; import touchSpec from '../../../completions/upstream/touch'; const allOptions = [ - '-A', + '-A "}' ] }, - 'Before you report an issue here please review the guidance we provide.' + 'Before you report an issue here please review the guidance we provide. Please complete the form in English.' ); export default (): string => ` +
@@ -154,5 +160,11 @@ export default (): string => `
+ `; diff --git a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css index 14ee09be..23f3be9a 100644 --- a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css +++ b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css @@ -97,7 +97,6 @@ line-height: 1.5; color: #495057; background-color: #fff; - border: none; } .issue-reporter * { @@ -184,7 +183,7 @@ .issue-reporter body { margin: 0; - overflow-y: scroll; + overflow-y: auto; height: 100%; } @@ -213,12 +212,11 @@ padding-bottom: 2em; display: flex; flex-direction: column; - min-height: 100%; overflow: visible; } .issue-reporter .description-section { - flex-grow: 1; + flex-grow: 0; display: flex; flex-direction: column; flex-shrink: 0; @@ -226,12 +224,13 @@ .issue-reporter textarea { flex-grow: 1; - min-height: 150px; + height: 200px; } .issue-reporter .block-info-text { display: flex; - flex-grow: 1; + flex-grow: 0; + flex-direction: column; } .issue-reporter #github-submit-btn { @@ -320,7 +319,6 @@ .issue-reporter select, .issue-reporter textarea { background-color: #3c3c3c; - border: none; color: #cccccc; } @@ -456,3 +454,4 @@ .issue-reporter a { color: var(--vscode-textLink-foreground); } + diff --git a/src/vs/workbench/contrib/issue/common/issue.contribution.ts b/src/vs/workbench/contrib/issue/common/issue.contribution.ts index a2805613..0c1effb4 100644 --- a/src/vs/workbench/contrib/issue/common/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/common/issue.contribution.ts @@ -3,16 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../nls.js'; import { ICommandAction } from '../../../../platform/action/common/action.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { CommandsRegistry, ICommandMetadata } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IWorkbenchIssueService, IssueReporterData } from './issue.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IssueReporterData, IWorkbenchIssueService } from './issue.js'; const OpenIssueReporterActionId = 'workbench.action.openIssueReporter'; const OpenIssueReporterApiId = 'vscode.openIssueReporter'; @@ -65,6 +66,18 @@ export class BaseIssueContribution extends Disposable implements IWorkbenchContr ) { super(); + if (!configurationService.getValue('telemetry.feedback.enabled')) { + this._register(CommandsRegistry.registerCommand({ + id: 'workbench.action.openIssueReporter', + handler: function (accessor) { + const data = accessor.get(INotificationService); + data.info('Feedback is disabled.'); + + }, + })); + return; + } + if (!productService.reportIssueUrl) { return; } diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts index 4ff3e642..547921c9 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issue.contribution.ts @@ -3,25 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IDisposable } from '../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../nls.js'; -import { registerAction2, Action2 } from '../../../../platform/actions/common/actions.js'; -import { IWorkbenchIssueService, IssueType, IIssueFormService } from '../common/issue.js'; -import { BaseIssueContribution } from '../common/issue.contribution.js'; +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from '../../../../platform/quickinput/common/quickAccess.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Extensions, IWorkbenchContributionsRegistry } from '../../../common/contributions.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; -import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from '../../../../platform/quickinput/common/quickAccess.js'; import { IssueQuickAccess } from '../browser/issueQuickAccess.js'; -import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; -import { NativeIssueService } from './issueService.js'; -import './processMainService.js'; import '../browser/issueTroubleshoot.js'; +import { BaseIssueContribution } from '../common/issue.contribution.js'; +import { IIssueFormService, IWorkbenchIssueService, IssueType } from '../common/issue.js'; +import { NativeIssueService } from './issueService.js'; import { NativeIssueFormService } from './nativeIssueFormService.js'; +import './processMainService.js'; //#region Issue Contribution registerSingleton(IWorkbenchIssueService, NativeIssueService, InstantiationType.Delayed); @@ -35,6 +35,10 @@ class NativeIssueContribution extends BaseIssueContribution { ) { super(productService, configurationService); + if (!configurationService.getValue('telemetry.feedback.enabled')) { + return; + } + if (productService.reportIssueUrl) { this._register(registerAction2(ReportPerformanceIssueUsingReporterAction)); } diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts index e6be6f94..bb27e35e 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts @@ -16,6 +16,7 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; import { IProcessMainService } from '../../../../platform/process/common/process.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; import { applyZoom } from '../../../../platform/window/electron-sandbox/window.js'; import { BaseIssueReporterService } from '../browser/baseIssueReporterService.js'; import { IssueReporterData as IssueReporterModelData } from '../browser/issueReporterModel.js'; @@ -47,7 +48,8 @@ export class IssueReporter extends BaseIssueReporterService { @IProcessMainService processMainService: IProcessMainService, @IThemeService themeService: IThemeService, @IFileService fileService: IFileService, - @IFileDialogService fileDialogService: IFileDialogService + @IFileDialogService fileDialogService: IFileDialogService, + @IUpdateService private readonly updateService: IUpdateService ) { super(disableExtensions, data, os, product, window, false, issueFormService, themeService, fileService, fileDialogService); this.processMainService = processMainService; @@ -64,6 +66,7 @@ export class IssueReporter extends BaseIssueReporterService { }); } + this.checkForUpdates(); this.setEventHandlers(); applyZoom(this.data.zoomLevel, this.window); this.updateExperimentsInfo(this.data.experiments); @@ -71,6 +74,20 @@ export class IssueReporter extends BaseIssueReporterService { this.updateUnsupportedMode(this.data.isUnsupported); } + private async checkForUpdates(): Promise { + const updateState = this.updateService.state; + if (updateState.type === StateType.Ready || updateState.type === StateType.Downloaded) { + this.needsUpdate = true; + const includeAcknowledgement = this.getElementById('version-acknowledgements'); + const updateBanner = this.getElementById('update-banner'); + if (updateBanner && includeAcknowledgement) { + includeAcknowledgement.classList.remove('hidden'); + updateBanner.classList.remove('hidden'); + updateBanner.textContent = localize('updateAvailable', "A new version of {0} is available.", this.product.nameLong); + } + } + } + public override setEventHandlers(): void { super.setEventHandlers(); diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css b/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css index c88a95a9..aebbe980 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css +++ b/src/vs/workbench/contrib/issue/electron-sandbox/media/issueReporter.css @@ -175,7 +175,7 @@ body.issue-reporter-body { margin: 0 !important; - overflow-y: scroll !important; + overflow-y: auto !important; height: 100% !important; } @@ -204,25 +204,25 @@ body.issue-reporter-body { padding-bottom: 2em; display: flex; flex-direction: column; - min-height: 100%; overflow: visible; } .issue-reporter-body .description-section { - flex-grow: 1; + flex-grow: 0; display: flex; flex-direction: column; flex-shrink: 0; } .issue-reporter-body textarea { - flex-grow: 1; - min-height: 150px; + flex-grow: 0; + height: 200px; } .issue-reporter-body .block-info-text { display: flex; - flex-grow: 1; + flex-grow: 0; + flex-direction: column; } .issue-reporter-body #github-submit-btn { @@ -468,3 +468,13 @@ body.issue-reporter-body { .issue-reporter-body .issues-container > .issue .issue-icon { padding-top: 2px; } + +.issue-reporter-update-banner { + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + padding: 10px; + text-align: center; + position: sticky; + top: 0; + z-index: 1000; +} diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts index c2022340..f6ae1519 100644 --- a/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts +++ b/src/vs/workbench/contrib/issue/electron-sandbox/nativeIssueFormService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/issueReporter.css'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { IMenuService } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; @@ -12,13 +12,16 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; import product from '../../../../platform/product/common/product.js'; +import { IAuxiliaryWindowService } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; +import { IHostService } from '../../../services/host/browser/host.js'; import { IssueFormService } from '../browser/issueFormService.js'; import { IIssueFormService, IssueReporterData } from '../common/issue.js'; import { IssueReporter } from './issueReporterService.js'; -import { IAuxiliaryWindowService } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js'; -import { IHostService } from '../../../services/host/browser/host.js'; +import './media/issueReporter.css'; export class NativeIssueFormService extends IssueFormService implements IIssueFormService { + private readonly store = new DisposableStore(); + constructor( @IInstantiationService instantiationService: IInstantiationService, @IAuxiliaryWindowService auxiliaryWindowService: IAuxiliaryWindowService, @@ -53,8 +56,10 @@ export class NativeIssueFormService extends IssueFormService implements IIssueFo // create issue reporter and instantiate if (this.issueReporterWindow) { - const issueReporter = this.instantiationService.createInstance(IssueReporter, !!this.environmentService.disableExtensions, data, { type: this.type, arch: this.arch, release: this.release }, product, this.issueReporterWindow); + const issueReporter = this.store.add(this.instantiationService.createInstance(IssueReporter, !!this.environmentService.disableExtensions, data, { type: this.type, arch: this.arch, release: this.release }, product, this.issueReporterWindow)); issueReporter.render(); + } else { + this.store.dispose(); } } } diff --git a/src/vs/workbench/contrib/limitIndicator/browser/limitIndicator.contribution.ts b/src/vs/workbench/contrib/limitIndicator/browser/limitIndicator.contribution.ts index 538e67d5..0640e070 100644 --- a/src/vs/workbench/contrib/limitIndicator/browser/limitIndicator.contribution.ts +++ b/src/vs/workbench/contrib/limitIndicator/browser/limitIndicator.contribution.ts @@ -75,7 +75,7 @@ interface LanguageFeatureAccessor { class ColorDecorationAccessor implements LanguageFeatureAccessor { readonly id = 'decoratorsLimitInfo'; readonly name = nls.localize('colorDecoratorsStatusItem.name', 'Color Decorator Status'); - readonly label = nls.localize('status.limitedColorDecorators.short', 'Color Decorators'); + readonly label = nls.localize('status.limitedColorDecorators.short', 'Color decorators'); readonly source = nls.localize('colorDecoratorsStatusItem.source', 'Color Decorators'); readonly settingsId = 'editor.colorDecoratorsLimit'; @@ -87,7 +87,7 @@ class ColorDecorationAccessor implements LanguageFeatureAccessor { class FoldingRangeAccessor implements LanguageFeatureAccessor { readonly id = 'foldingLimitInfo'; readonly name = nls.localize('foldingRangesStatusItem.name', 'Folding Status'); - readonly label = nls.localize('status.limitedFoldingRanges.short', 'Folding Ranges'); + readonly label = nls.localize('status.limitedFoldingRanges.short', 'Folding ranges'); readonly source = nls.localize('foldingRangesStatusItem.source', 'Folding'); readonly settingsId = 'editor.foldingMaximumRegions'; diff --git a/src/vs/workbench/contrib/markers/browser/markersTable.ts b/src/vs/workbench/contrib/markers/browser/markersTable.ts index af8e6eaf..3a10bafb 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTable.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTable.ts @@ -13,7 +13,7 @@ import { IOpenEvent, IWorkbenchTableOptions, WorkbenchTable } from '../../../../ import { HighlightedLabel } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; import { compareMarkersByUri, Marker, MarkerTableItem, ResourceMarkers } from './markersModel.js'; import { MarkerSeverity } from '../../../../platform/markers/common/markers.js'; -import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { FilterOptions } from './markersFilterOptions.js'; diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 6e639a98..4ed27e35 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -26,7 +26,7 @@ import { Event, Emitter } from '../../../../base/common/event.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { isUndefinedOrNull } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import { Action, IAction } from '../../../../base/common/actions.js'; +import { Action, IAction, toAction } from '../../../../base/common/actions.js'; import { localize } from '../../../../nls.js'; import { CancelablePromise, createCancelablePromise, Delayer } from '../../../../base/common/async.js'; import { IModelService } from '../../../../editor/common/services/model.js'; @@ -35,7 +35,7 @@ import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from '../../.. import { CodeActionKind, CodeActionSet, CodeActionTriggerSource } from '../../../../editor/contrib/codeAction/common/types.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IEditorService, ACTIVE_GROUP } from '../../../services/editor/common/editorService.js'; -import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { CodeActionTriggerType } from '../../../../editor/common/languages.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { Progress } from '../../../../platform/progress/common/progress.js'; @@ -645,15 +645,14 @@ export class MarkerViewModel extends Disposable { } private toActions(codeActions: CodeActionSet): IAction[] { - return codeActions.validActions.map(item => new Action( - item.action.command ? item.action.command.id : item.action.title, - item.action.title, - undefined, - true, - () => { - return this.openFileAtMarker(this.marker) - .then(() => this.instantiationService.invokeFunction(applyCodeAction, item, ApplyCodeActionReason.FromProblemsView)); - })); + return codeActions.validActions.map(item => toAction({ + id: item.action.command ? item.action.command.id : item.action.title, + label: item.action.title, + run: async () => { + await this.openFileAtMarker(this.marker); + return await this.instantiationService.invokeFunction(applyCodeAction, item, ApplyCodeActionReason.FromProblemsView); + } + })); } private openFileAtMarker(element: Marker): Promise { diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts new file mode 100644 index 00000000..1e5cef15 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import * as jsonContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { mcpSchemaId } from '../../../services/configuration/common/configuration.js'; +import { ConfigMcpDiscovery } from '../common/discovery/configMcpDiscovery.js'; +import { ExtensionMcpDiscovery } from '../common/discovery/extensionMcpDiscovery.js'; +import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js'; +import { RemoteNativeMpcDiscovery } from '../common/discovery/nativeMcpRemoteDiscovery.js'; +import { CursorWorkspaceMcpDiscoveryAdapter } from '../common/discovery/workspaceMcpDiscoveryAdapter.js'; +import { IMcpConfigPathsService, McpConfigPathsService } from '../common/mcpConfigPathsService.js'; +import { mcpServerSchema } from '../common/mcpConfiguration.js'; +import { McpContextKeysController } from '../common/mcpContextKeys.js'; +import { McpRegistry } from '../common/mcpRegistry.js'; +import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; +import { McpService } from '../common/mcpService.js'; +import { IMcpService } from '../common/mcpTypes.js'; +import { AddConfigurationAction, EditStoredInput, InstallFromActivation, ListMcpServerCommand, MCPServerActionRendering, McpServerOptionsCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; +import { McpDiscovery } from './mcpDiscovery.js'; +import { McpLanguageFeatures } from './mcpLanguageFeatures.js'; +import { McpUrlHandler } from './mcpUrlHandler.js'; + +registerSingleton(IMcpRegistry, McpRegistry, InstantiationType.Delayed); +registerSingleton(IMcpService, McpService, InstantiationType.Delayed); +registerSingleton(IMcpConfigPathsService, McpConfigPathsService, InstantiationType.Delayed); + +mcpDiscoveryRegistry.register(new SyncDescriptor(RemoteNativeMpcDiscovery)); +mcpDiscoveryRegistry.register(new SyncDescriptor(ConfigMcpDiscovery)); +mcpDiscoveryRegistry.register(new SyncDescriptor(ExtensionMcpDiscovery)); +mcpDiscoveryRegistry.register(new SyncDescriptor(CursorWorkspaceMcpDiscoveryAdapter)); + +registerWorkbenchContribution2('mcpDiscovery', McpDiscovery, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2('mcpContextKeys', McpContextKeysController, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2('mcpLanguageFeatures', McpLanguageFeatures, WorkbenchPhase.Eventually); +registerWorkbenchContribution2('mcpUrlHandler', McpUrlHandler, WorkbenchPhase.BlockRestore); + +registerAction2(ListMcpServerCommand); +registerAction2(McpServerOptionsCommand); +registerAction2(ResetMcpTrustCommand); +registerAction2(ResetMcpCachedTools); +registerAction2(AddConfigurationAction); +registerAction2(RemoveStoredInput); +registerAction2(EditStoredInput); +registerAction2(StartServer); +registerAction2(StopServer); +registerAction2(ShowOutput); +registerAction2(InstallFromActivation); +registerAction2(RestartServer); + +registerWorkbenchContribution2('mcpActionRendering', MCPServerActionRendering, WorkbenchPhase.BlockRestore); + +const jsonRegistry = Registry.as(jsonContributionRegistry.Extensions.JSONContribution); +jsonRegistry.registerSchema(mcpSchemaId, mcpServerSchema); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts new file mode 100644 index 00000000..6f536d3a --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -0,0 +1,559 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h } from '../../../../base/browser/dom.js'; +import { assertNever } from '../../../../base/common/assert.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { groupBy } from '../../../../base/common/collections.js'; +import { Event } from '../../../../base/common/event.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun, derived } from '../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ILocalizedString, localize, localize2 } from '../../../../nls.js'; +import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; +import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { Action2, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; +import { StorageScope } from '../../../../platform/storage/common/storage.js'; +import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { ActiveEditorContext, ResourceContextKey } from '../../../common/contextkeys.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { ChatMode } from '../../chat/common/constants.js'; +import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js'; +import { McpContextKeys } from '../common/mcpContextKeys.js'; +import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; +import { IMcpServer, IMcpService, LazyCollectionState, McpConnectionState, McpServerToolsState } from '../common/mcpTypes.js'; +import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js'; +import { McpUrlHandler } from './mcpUrlHandler.js'; + +// acroynms do not get localized +const category: ILocalizedString = { + original: 'MCP', + value: 'MCP', +}; + +export class ListMcpServerCommand extends Action2 { + public static readonly id = 'workbench.mcp.listServer'; + constructor() { + super({ + id: ListMcpServerCommand.id, + title: localize2('mcp.list', 'List Servers'), + icon: Codicon.server, + category, + f1: true, + menu: { + when: ContextKeyExpr.and( + ContextKeyExpr.or(McpContextKeys.hasUnknownTools, McpContextKeys.hasServersWithErrors), + ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent) + ), + id: MenuId.ChatInputAttachmentToolbar, + group: 'navigation', + order: 0 + }, + }); + } + + override async run(accessor: ServicesAccessor) { + const mcpService = accessor.get(IMcpService); + const commandService = accessor.get(ICommandService); + const quickInput = accessor.get(IQuickInputService); + + type ItemType = { id: string } & IQuickPickItem; + + const store = new DisposableStore(); + const pick = quickInput.createQuickPick({ useSeparators: true }); + pick.placeholder = localize('mcp.selectServer', 'Select an MCP Server'); + + store.add(pick); + + store.add(autorun(reader => { + const servers = groupBy(mcpService.servers.read(reader).slice().sort((a, b) => (a.collection.presentation?.order || 0) - (b.collection.presentation?.order || 0)), s => s.collection.id); + const firstRun = pick.items.length === 0; + pick.items = [ + { id: '$add', label: localize('mcp.addServer', 'Add Server'), description: localize('mcp.addServer.description', 'Add a new server configuration'), alwaysShow: true, iconClass: ThemeIcon.asClassName(Codicon.add) }, + ...Object.values(servers).filter(s => s.length).flatMap((servers): (ItemType | IQuickPickSeparator)[] => [ + { type: 'separator', label: servers[0].collection.label, id: servers[0].collection.id }, + ...servers.map(server => ({ + id: server.definition.id, + label: server.definition.label, + description: McpConnectionState.toString(server.connectionState.read(reader)), + })), + ]), + ]; + + if (firstRun && pick.items.length > 3) { + pick.activeItems = pick.items.slice(2, 3) as ItemType[]; // select the first server by default + } + })); + + + const picked = await new Promise(resolve => { + store.add(pick.onDidAccept(() => { + resolve(pick.activeItems[0]); + })); + store.add(pick.onDidHide(() => { + resolve(undefined); + })); + pick.show(); + }); + + store.dispose(); + + if (!picked) { + // no-op + } else if (picked.id === '$add') { + commandService.executeCommand(AddConfigurationAction.ID); + } else { + commandService.executeCommand(McpServerOptionsCommand.id, picked.id); + } + } +} + + +export class McpServerOptionsCommand extends Action2 { + + static readonly id = 'workbench.mcp.serverOptions'; + + constructor() { + super({ + id: McpServerOptionsCommand.id, + title: localize2('mcp.options', 'Server Options'), + category, + f1: false, + }); + } + + override async run(accessor: ServicesAccessor, id: string): Promise { + const mcpService = accessor.get(IMcpService); + const quickInputService = accessor.get(IQuickInputService); + const mcpRegistry = accessor.get(IMcpRegistry); + const editorService = accessor.get(IEditorService); + const server = mcpService.servers.get().find(s => s.definition.id === id); + if (!server) { + return; + } + + + const collection = mcpRegistry.collections.get().find(c => c.id === server.collection.id); + const serverDefinition = collection?.serverDefinitions.get().find(s => s.id === server.definition.id); + + interface ActionItem extends IQuickPickItem { + action: 'start' | 'stop' | 'restart' | 'showOutput' | 'config'; + } + + const items: ActionItem[] = []; + const serverState = server.connectionState.get(); + + // Only show start when server is stopped or in error state + if (McpConnectionState.canBeStarted(serverState.state)) { + items.push({ + label: localize('mcp.start', 'Start Server'), + action: 'start' + }); + } else { + items.push({ + label: localize('mcp.stop', 'Stop Server'), + action: 'stop' + }); + items.push({ + label: localize('mcp.restart', 'Restart Server'), + action: 'restart' + }); + } + + items.push({ + label: localize('mcp.showOutput', 'Show Output'), + action: 'showOutput' + }); + + const configTarget = serverDefinition?.presentation?.origin || collection?.presentation?.origin; + if (configTarget) { + items.push({ + label: localize('mcp.config', 'Show Configuration'), + action: 'config', + }); + } + + const pick = await quickInputService.pick(items, { + title: server.definition.label, + placeHolder: localize('mcp.selectAction', 'Select Server Action') + }); + + if (!pick) { + return; + } + + switch (pick.action) { + case 'start': + await server.start(true); + server.showOutput(); + break; + case 'stop': + await server.stop(); + break; + case 'restart': + await server.stop(); + await server.start(true); + break; + case 'showOutput': + server.showOutput(); + break; + case 'config': + editorService.openEditor({ + resource: URI.isUri(configTarget) ? configTarget : configTarget!.uri, + options: { selection: URI.isUri(configTarget) ? undefined : configTarget!.range } + }); + break; + default: + assertNever(pick.action); + } + } +} + +export class MCPServerActionRendering extends Disposable implements IWorkbenchContribution { + public static readonly ID = 'workbench.contrib.mcp.discovery'; + + constructor( + @IActionViewItemService actionViewItemService: IActionViewItemService, + @IMcpService mcpService: IMcpService, + @IInstantiationService instaService: IInstantiationService, + @ICommandService commandService: ICommandService, + ) { + super(); + + const enum DisplayedState { + None, + NewTools, + Error, + Refreshing, + } + + + + const displayedState = derived((reader) => { + const servers = mcpService.servers.read(reader); + const serversPerState: IMcpServer[][] = []; + for (const server of servers) { + let thisState = DisplayedState.None; + switch (server.toolsState.read(reader)) { + case McpServerToolsState.Unknown: + if (server.trusted.read(reader) === false) { + thisState = DisplayedState.None; + } else { + thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.NewTools; + } + break; + case McpServerToolsState.RefreshingFromUnknown: + thisState = DisplayedState.Refreshing; + break; + default: + thisState = server.connectionState.read(reader).state === McpConnectionState.Kind.Error ? DisplayedState.Error : DisplayedState.None; + break; + } + + serversPerState[thisState] ??= []; + serversPerState[thisState].push(server); + } + + const unknownServerStates = mcpService.lazyCollectionState.read(reader); + if (unknownServerStates === LazyCollectionState.LoadingUnknown) { + serversPerState[DisplayedState.Refreshing] ??= []; + } else if (unknownServerStates === LazyCollectionState.HasUnknown) { + serversPerState[DisplayedState.NewTools] ??= []; + } + + const maxState = (serversPerState.length - 1) as DisplayedState; + return { state: maxState, servers: serversPerState[maxState] || [] }; + }); + + this._store.add(actionViewItemService.register(MenuId.ChatInputAttachmentToolbar, ListMcpServerCommand.id, (action, options) => { + if (!(action instanceof MenuItemAction)) { + return undefined; + } + + return instaService.createInstance(class extends MenuEntryActionViewItem { + + override render(container: HTMLElement): void { + + super.render(container); + container.classList.add('chat-mcp'); + + const action = h('button.chat-mcp-action', [h('span@icon')]); + + this._register(autorun(r => { + const { state } = displayedState.read(r); + const { root, icon } = action; + this.updateTooltip(); + container.classList.toggle('chat-mcp-has-action', state !== DisplayedState.None); + + if (!root.parentElement) { + container.appendChild(root); + } + + root.ariaLabel = this.getLabelForState(displayedState.read(r)); + root.className = 'chat-mcp-action'; + icon.className = ''; + if (state === DisplayedState.NewTools) { + root.classList.add('chat-mcp-action-new'); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.refresh)); + } else if (state === DisplayedState.Error) { + root.classList.add('chat-mcp-action-error'); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.warning)); + } else if (state === DisplayedState.Refreshing) { + root.classList.add('chat-mcp-action-refreshing'); + icon.classList.add(...ThemeIcon.asClassNameArray(spinningLoading)); + } else { + root.remove(); + } + })); + } + + override async onClick(e: MouseEvent): Promise { + e.preventDefault(); + e.stopPropagation(); + + const { state, servers } = displayedState.get(); + if (state === DisplayedState.NewTools) { + servers.forEach(server => server.start()); + mcpService.activateCollections(); + } else if (state === DisplayedState.Refreshing) { + servers.at(-1)?.showOutput(); + } else if (state === DisplayedState.Error) { + const server = servers.at(-1); + if (server) { + commandService.executeCommand(McpServerOptionsCommand.id, server.definition.id); + } + } else { + commandService.executeCommand(ListMcpServerCommand.id); + } + } + + protected override getTooltip(): string { + return this.getLabelForState() || super.getTooltip(); + } + + private getLabelForState({ state, servers } = displayedState.get()) { + if (state === DisplayedState.NewTools) { + return localize('mcp.newTools', "New tools available ({0})", servers.length || 1); + } else if (state === DisplayedState.Error) { + return localize('mcp.toolError', "Error loading {0} tool(s)", servers.length || 1); + } else if (state === DisplayedState.Refreshing) { + return localize('mcp.toolRefresh', "Discovering tools..."); + } else { + return null; + } + } + + + }, action, { ...options, keybindingNotRenderedWithLabel: true }); + + }, Event.fromObservable(displayedState))); + } +} + +export class ResetMcpTrustCommand extends Action2 { + static readonly ID = 'workbench.mcp.resetTrust'; + + constructor() { + super({ + id: ResetMcpTrustCommand.ID, + title: localize2('mcp.resetTrust', "Reset Trust"), + category, + f1: true, + precondition: McpContextKeys.toolsCount.greater(0), + }); + } + + run(accessor: ServicesAccessor): void { + const mcpService = accessor.get(IMcpRegistry); + mcpService.resetTrust(); + } +} + + +export class ResetMcpCachedTools extends Action2 { + static readonly ID = 'workbench.mcp.resetCachedTools'; + + constructor() { + super({ + id: ResetMcpCachedTools.ID, + title: localize2('mcp.resetCachedTools', "Reset Cached Tools"), + category, + f1: true, + precondition: McpContextKeys.toolsCount.greater(0), + }); + } + + run(accessor: ServicesAccessor): void { + const mcpService = accessor.get(IMcpService); + mcpService.resetCaches(); + } +} + +export class AddConfigurationAction extends Action2 { + static readonly ID = 'workbench.mcp.addConfiguration'; + + constructor() { + super({ + id: AddConfigurationAction.ID, + title: localize2('mcp.addConfiguration', "Add Server..."), + metadata: { + description: localize2('mcp.addConfiguration.description', "Installs a new Model Context protocol to the mcp.json settings"), + }, + category, + f1: true, + menu: { + id: MenuId.EditorContent, + when: ContextKeyExpr.and( + ContextKeyExpr.regex(ResourceContextKey.Path.key, /\.vscode[/\\]mcp\.json$/), + ActiveEditorContext.isEqualTo(TEXT_FILE_EDITOR_ID) + ) + } + }); + } + + async run(accessor: ServicesAccessor, configUri?: string): Promise { + return accessor.get(IInstantiationService).createInstance(McpAddConfigurationCommand, configUri).run(); + } +} + + +export class RemoveStoredInput extends Action2 { + static readonly ID = 'workbench.mcp.removeStoredInput'; + + constructor() { + super({ + id: RemoveStoredInput.ID, + title: localize2('mcp.resetCachedTools', "Reset Cached Tools"), + category, + f1: false, + }); + } + + run(accessor: ServicesAccessor, scope: StorageScope, id?: string): void { + accessor.get(IMcpRegistry).clearSavedInputs(scope, id); + } +} + +export class EditStoredInput extends Action2 { + static readonly ID = 'workbench.mcp.editStoredInput'; + + constructor() { + super({ + id: EditStoredInput.ID, + title: localize2('mcp.editStoredInput', "Edit Stored Input"), + category, + f1: false, + }); + } + + run(accessor: ServicesAccessor, inputId: string, uri: URI | undefined, configSection: string, target: ConfigurationTarget): void { + const workspaceFolder = uri && accessor.get(IWorkspaceContextService).getWorkspaceFolder(uri); + accessor.get(IMcpRegistry).editSavedInput(inputId, workspaceFolder || undefined, configSection, target); + } +} + +export class ShowOutput extends Action2 { + static readonly ID = 'workbench.mcp.showOutput'; + + constructor() { + super({ + id: ShowOutput.ID, + title: localize2('mcp.command.showOutput', "Show Output"), + category, + f1: false, + }); + } + + run(accessor: ServicesAccessor, serverId: string): void { + accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId)?.showOutput(); + } +} + +export class RestartServer extends Action2 { + static readonly ID = 'workbench.mcp.restartServer'; + + constructor() { + super({ + id: RestartServer.ID, + title: localize2('mcp.command.restartServer', "Restart Server"), + category, + f1: false, + }); + } + + async run(accessor: ServicesAccessor, serverId: string) { + const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId); + s?.showOutput(); + await s?.stop(); + await s?.start(); + } +} + +export class StartServer extends Action2 { + static readonly ID = 'workbench.mcp.startServer'; + + constructor() { + super({ + id: StartServer.ID, + title: localize2('mcp.command.startServer', "Start Server"), + category, + f1: false, + }); + } + + async run(accessor: ServicesAccessor, serverId: string) { + const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId); + await s?.start(); + } +} + +export class StopServer extends Action2 { + static readonly ID = 'workbench.mcp.stopServer'; + + constructor() { + super({ + id: StopServer.ID, + title: localize2('mcp.command.stopServer', "Stop Server"), + category, + f1: false, + }); + } + + async run(accessor: ServicesAccessor, serverId: string) { + const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId); + await s?.stop(); + } +} + +export class InstallFromActivation extends Action2 { + static readonly ID = 'workbench.mcp.installFromActivation'; + + constructor() { + super({ + id: InstallFromActivation.ID, + title: localize2('mcp.command.installFromActivation', "Install..."), + category, + f1: false, + menu: { + id: MenuId.EditorContent, + when: ContextKeyExpr.equals('resourceScheme', McpUrlHandler.scheme) + } + }); + } + + async run(accessor: ServicesAccessor, uri: URI) { + const addConfigHelper = accessor.get(IInstantiationService).createInstance(McpAddConfigurationCommand, undefined); + addConfigHelper.pickForUrlHandler(uri); + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts new file mode 100644 index 00000000..f61417a6 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts @@ -0,0 +1,498 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { mapFindFirst } from '../../../../base/common/arraysFind.js'; +import { assertNever } from '../../../../base/common/assert.js'; +import { disposableTimeout } from '../../../../base/common/async.js'; +import { parse as parseJsonc } from '../../../../base/common/jsonc.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { basename } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { localize } from '../../../../nls.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ConfigurationTarget, getConfigValueInTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IMcpConfiguration, IMcpConfigurationSSE, McpConfigurationServer } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { EditorsOrder } from '../../../common/editor.js'; +import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js'; +import { ConfiguredInput } from '../../../services/configurationResolver/common/configurationResolver.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { IMcpConfigurationStdio, mcpConfigurationSection, mcpStdioServerSchema } from '../common/mcpConfiguration.js'; +import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; +import { McpServerOptionsCommand } from './mcpCommands.js'; + +const enum AddConfigurationType { + Stdio, + SSE, + + NpmPackage, + PipPackage, + DockerImage, +} + +type AssistedConfigurationType = AddConfigurationType.NpmPackage | AddConfigurationType.PipPackage | AddConfigurationType.DockerImage; + +const assistedTypes = { + [AddConfigurationType.NpmPackage]: { + title: localize('mcp.npm.title', "Enter NPM Package Name"), + placeholder: localize('mcp.npm.placeholder', "Package name (e.g., @org/package)"), pickLabel: localize('mcp.serverType.npm', "NPM Package"), + pickDescription: localize('mcp.serverType.npm.description', "Install from an NPM package name") + }, + [AddConfigurationType.PipPackage]: { + title: localize('mcp.pip.title', "Enter Pip Package Name"), + placeholder: localize('mcp.pip.placeholder', "Package name (e.g., package-name)"), + pickLabel: localize('mcp.serverType.pip', "Pip Package"), + pickDescription: localize('mcp.serverType.pip.description', "Install from a Pip package name") + }, + [AddConfigurationType.DockerImage]: { + title: localize('mcp.docker.title', "Enter Docker Image Name"), + placeholder: localize('mcp.docker.placeholder', "Image name (e.g., mcp/imagename)"), + pickLabel: localize('mcp.serverType.docker', "Docker Image"), + pickDescription: localize('mcp.serverType.docker.description', "Install from a Docker image") + }, +}; + +const enum AddConfigurationCopilotCommand { + /** Returns whether MCP enhanced setup is enabled. */ + IsSupported = 'github.copilot.chat.mcp.setup.check', + + /** Takes an npm/pip package name, validates its owner. */ + ValidatePackage = 'github.copilot.chat.mcp.setup.validatePackage', + + /** Returns the resolved MCP configuration. */ + StartFlow = 'github.copilot.chat.mcp.setup.flow', +} + +type ValidatePackageResult = { state: 'ok'; publisher: string } | { state: 'error'; error: string }; + +type AddServerData = { + packageType: string; +}; +type AddServerClassification = { + owner: 'digitarald'; + comment: 'Generic details for adding a new MCP server'; + packageType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of MCP server package' }; +}; +type AddServerCompletedData = { + packageType: string; + serverType: string | undefined; + target: string; +}; +type AddServerCompletedClassification = { + owner: 'digitarald'; + comment: 'Generic details for successfully adding model-assisted MCP server'; + packageType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of MCP server package' }; + serverType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of MCP server' }; + target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The target of the MCP server configuration' }; +}; + +export class McpAddConfigurationCommand { + constructor( + private readonly _explicitConfigUri: string | undefined, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IJSONEditingService private readonly _jsonEditingService: IJSONEditingService, + @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @ICommandService private readonly _commandService: ICommandService, + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @IEditorService private readonly _openerService: IEditorService, + @IEditorService private readonly _editorService: IEditorService, + @IFileService private readonly _fileService: IFileService, + @INotificationService private readonly _notificationService: INotificationService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + ) { } + + private async getServerType(): Promise { + const items: QuickPickInput<{ kind: AddConfigurationType } & IQuickPickItem>[] = [ + { kind: AddConfigurationType.Stdio, label: localize('mcp.serverType.command', "Command (stdio)"), description: localize('mcp.serverType.command.description', "Run a local command that implements the MCP protocol") }, + { kind: AddConfigurationType.SSE, label: localize('mcp.serverType.http', "HTTP (server-sent events)"), description: localize('mcp.serverType.http.description', "Connect to a remote HTTP server that implements the MCP protocol") } + ]; + + let aiSupported: boolean | undefined; + try { + aiSupported = await this._commandService.executeCommand(AddConfigurationCopilotCommand.IsSupported); + } catch { + // ignored + } + + if (aiSupported) { + items.unshift({ type: 'separator', label: localize('mcp.serverType.manual', "Manual Install") }); + items.push( + { type: 'separator', label: localize('mcp.serverType.copilot', "Model-Assisted") }, + ...Object.entries(assistedTypes).map(([type, { pickLabel, pickDescription }]) => ({ + kind: Number(type) as AddConfigurationType, + label: pickLabel, + description: pickDescription, + })) + ); + } + + const result = await this._quickInputService.pick<{ kind: AddConfigurationType } & IQuickPickItem>(items, { + placeHolder: localize('mcp.serverType.placeholder', "Choose the type of MCP server to add"), + }); + + return result?.kind; + } + + private async getStdioConfig(): Promise { + const command = await this._quickInputService.input({ + title: localize('mcp.command.title', "Enter Command"), + placeHolder: localize('mcp.command.placeholder', "Command to run (with optional arguments)"), + ignoreFocusLost: true, + }); + + if (!command) { + return undefined; + } + + this._telemetryService.publicLog2('mcp.addserver', { + packageType: 'stdio' + }); + + // Split command into command and args, handling quotes + const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g)!; + return { + type: 'stdio', + command: parts[0].replace(/"/g, ''), + + args: parts.slice(1).map(arg => arg.replace(/"/g, '')) + }; + } + + private async getSSEConfig(): Promise { + const url = await this._quickInputService.input({ + title: localize('mcp.url.title', "Enter Server URL"), + placeHolder: localize('mcp.url.placeholder', "URL of the MCP server (e.g., http://localhost:3000)"), + ignoreFocusLost: true, + }); + + if (!url) { + return undefined; + } + + this._telemetryService.publicLog2('mcp.addserver', { + packageType: 'sse' + }); + + return { + type: 'sse', + url + }; + } + + private async getServerId(suggestion = `my-mcp-server-${generateUuid().split('-')[0]}`): Promise { + const id = await this._quickInputService.input({ + title: localize('mcp.serverId.title', "Enter Server ID"), + placeHolder: localize('mcp.serverId.placeholder', "Unique identifier for this server"), + value: suggestion, + ignoreFocusLost: true, + }); + + return id; + } + + private async getConfigurationTarget(): Promise { + const options: (IQuickPickItem & { target: ConfigurationTarget })[] = [ + { target: ConfigurationTarget.USER, label: localize('mcp.target.user', "User Settings"), description: localize('mcp.target.user.description', "Available in all workspaces") } + ]; + + if (!!this._environmentService.remoteAuthority) { + options.push({ target: ConfigurationTarget.USER_REMOTE, label: localize('mcp.target.remote', "Remote Settings"), description: localize('mcp.target..remote.description', "Available on this remote machine") }); + } + + if (this._workspaceService.getWorkspace().folders.length > 0) { + options.push({ target: ConfigurationTarget.WORKSPACE, label: localize('mcp.target.workspace', "Workspace Settings"), description: localize('mcp.target.workspace.description', "Available in this workspace") }); + } + + if (options.length === 1) { + return options[0].target; + } + + + const targetPick = await this._quickInputService.pick(options, { + title: localize('mcp.target.title', "Choose where to save the configuration"), + }); + + return targetPick?.target; + } + + private async getAssistedConfig(type: AssistedConfigurationType): Promise<{ name: string; config: McpConfigurationServer } | undefined> { + const packageName = await this._quickInputService.input({ + ignoreFocusLost: true, + title: assistedTypes[type].title, + placeHolder: assistedTypes[type].placeholder, + }); + + if (!packageName) { + return undefined; + } + + const enum LoadAction { + Retry = 'retry', + Cancel = 'cancel', + Allow = 'allow' + } + + const loadingQuickPickStore = new DisposableStore(); + const loadingQuickPick = loadingQuickPickStore.add(this._quickInputService.createQuickPick()); + loadingQuickPick.title = localize('mcp.loading.title', "Loading package details..."); + loadingQuickPick.busy = true; + loadingQuickPick.ignoreFocusOut = true; + + const packageType = this.getPackageType(type); + + this._telemetryService.publicLog2('mcp.addserver', { + packageType: packageType! + }); + + this._commandService.executeCommand( + AddConfigurationCopilotCommand.ValidatePackage, + { + type: packageType, + name: packageName, + targetConfig: { + ...mcpStdioServerSchema, + properties: { + ...mcpStdioServerSchema.properties, + name: { + type: 'string', + description: 'Suggested name of the server, alphanumeric and hyphen only', + } + }, + required: [...(mcpStdioServerSchema.required || []), 'name'], + }, + } + ).then(result => { + if (!result || result.state === 'error') { + loadingQuickPick.title = result?.error || 'Unknown error loading package'; + loadingQuickPick.items = [{ id: LoadAction.Retry, label: localize('mcp.error.retry', 'Try a different package') }, { id: LoadAction.Cancel, label: localize('cancel', 'Cancel') }]; + } else { + loadingQuickPick.title = localize('mcp.confirmPublish', 'Install {0} from {1}?', packageName, result.publisher); + loadingQuickPick.items = [ + { id: LoadAction.Allow, label: localize('allow', "Allow") }, + { id: LoadAction.Cancel, label: localize('cancel', 'Cancel') } + ]; + } + loadingQuickPick.busy = false; + }); + + const loadingAction = await new Promise(resolve => { + loadingQuickPick.onDidAccept(() => resolve(loadingQuickPick.selectedItems[0]?.id)); + loadingQuickPick.onDidHide(() => resolve(undefined)); + loadingQuickPick.show(); + }).finally(() => loadingQuickPick.dispose()); + + switch (loadingAction) { + case LoadAction.Retry: + return this.getAssistedConfig(type); + case LoadAction.Allow: + break; + case LoadAction.Cancel: + default: + return undefined; + } + + const configWithName = await this._commandService.executeCommand( + AddConfigurationCopilotCommand.StartFlow, + { + name: packageName, + type: packageType + } + ); + + if (!configWithName) { + return undefined; + } + + const { name, ...config } = configWithName; + return { name, config }; + } + + /** Shows the location of a server config once it's discovered. */ + private showOnceDiscovered(name: string) { + const store = new DisposableStore(); + store.add(autorun(reader => { + const colls = this._mcpRegistry.collections.read(reader); + const match = mapFindFirst(colls, collection => mapFindFirst(collection.serverDefinitions.read(reader), + server => server.label === name ? { server, collection } : undefined)); + if (match) { + if (match.collection.presentation?.origin) { + this._openerService.openEditor({ + resource: match.collection.presentation.origin, + options: { + selection: match.server.presentation?.origin?.range, + preserveFocus: true, + } + }); + } else { + this._commandService.executeCommand(McpServerOptionsCommand.id, name); + } + + store.dispose(); + } + })); + + store.add(disposableTimeout(() => store.dispose(), 5000)); + } + + private writeToUserSetting(name: string, config: McpConfigurationServer, target: ConfigurationTarget, inputs?: ConfiguredInput[]) { + const settings: IMcpConfiguration = { ...getConfigValueInTarget(this._configurationService.inspect(mcpConfigurationSection), target) }; + settings.servers = { ...settings.servers, [name]: config }; + if (inputs) { + settings.inputs = [...(settings.inputs || []), ...inputs]; + } + return this._configurationService.updateValue(mcpConfigurationSection, settings, target); + } + + public async run(): Promise { + // Step 1: Choose server type + const serverType = await this.getServerType(); + if (serverType === undefined) { + return; + } + + // Step 2: Get server details based on type + let serverConfig: McpConfigurationServer | undefined; + let suggestedName: string | undefined; + switch (serverType) { + case AddConfigurationType.Stdio: + serverConfig = await this.getStdioConfig(); + break; + case AddConfigurationType.SSE: + serverConfig = await this.getSSEConfig(); + break; + case AddConfigurationType.NpmPackage: + case AddConfigurationType.PipPackage: + case AddConfigurationType.DockerImage: { + const r = await this.getAssistedConfig(serverType); + serverConfig = r?.config; + suggestedName = r?.name; + break; + } + default: + assertNever(serverType); + } + + if (!serverConfig) { + return; + } + + // Step 3: Get server ID + const serverId = await this.getServerId(suggestedName); + if (!serverId) { + return; + } + + // Step 4: Choose configuration target if no configUri provided + let target: ConfigurationTarget | undefined; + const workspace = this._workspaceService.getWorkspace(); + if (!this._explicitConfigUri) { + target = await this.getConfigurationTarget(); + if (!target) { + return; + } + } + + // Step 5: Update configuration + const writeToUriDirect = this._explicitConfigUri + ? URI.parse(this._explicitConfigUri) + : target === ConfigurationTarget.WORKSPACE && workspace.folders.length === 1 + ? URI.joinPath(workspace.folders[0].uri, '.vscode', 'mcp.json') + : undefined; + + if (writeToUriDirect) { + await this._jsonEditingService.write(writeToUriDirect, [{ + path: ['servers', serverId], + value: serverConfig + }], true); + } else { + await this.writeToUserSetting(serverId, serverConfig, target!); + } + + const packageType = this.getPackageType(serverType); + if (packageType) { + this._telemetryService.publicLog2('mcp.addserver.completed', { + packageType, + serverType: serverConfig.type, + target: target === ConfigurationTarget.WORKSPACE ? 'workspace' : 'user' + }); + } + + this.showOnceDiscovered(serverId); + } + + public async pickForUrlHandler(resource: URI, showIsPrimary = false): Promise { + const name = decodeURIComponent(basename(resource)).replace(/\.json$/, ''); + const placeHolder = localize('install.title', 'Install MCP server {0}', name); + + const items: IQuickPickItem[] = [ + { id: 'install', label: localize('install.start', 'Install Server'), description: localize('install.description', 'Install in your user settings') }, + { id: 'show', label: localize('install.show', 'Show Configuration', name) }, + { id: 'rename', label: localize('install.rename', 'Rename "{0}"', name) }, + { id: 'cancel', label: localize('cancel', 'Cancel') }, + ]; + if (showIsPrimary) { + [items[0], items[1]] = [items[1], items[0]]; + } + + const pick = await this._quickInputService.pick(items, { placeHolder, ignoreFocusLost: true }); + const getEditors = () => this._editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE) + .filter(e => e.editor.resource?.toString() === resource.toString()); + + switch (pick?.id) { + case 'show': + await this._editorService.openEditor({ resource }); + break; + case 'install': + await this._editorService.save(getEditors()); + try { + const contents = await this._fileService.readFile(resource); + const { inputs, ...config }: McpConfigurationServer & { inputs?: ConfiguredInput[] } = parseJsonc(contents.value.toString()); + await this.writeToUserSetting(name, config, ConfigurationTarget.USER_LOCAL, inputs); + this._editorService.closeEditors(getEditors()); + this.showOnceDiscovered(name); + } catch (e) { + this._notificationService.error(localize('install.error', 'Error installing MCP server {0}: {1}', name, e.message)); + await this._editorService.openEditor({ resource }); + } + break; + case 'rename': { + const newName = await this._quickInputService.input({ placeHolder: localize('install.newName', 'Enter new name'), value: name }); + if (newName) { + const newURI = resource.with({ path: `/${encodeURIComponent(newName)}.json` }); + await this._editorService.save(getEditors()); + await this._fileService.move(resource, newURI); + return this.pickForUrlHandler(newURI, showIsPrimary); + } + break; + } + } + } + + private getPackageType(serverType: AddConfigurationType): string | undefined { + switch (serverType) { + case AddConfigurationType.NpmPackage: + return 'npm'; + case AddConfigurationType.PipPackage: + return 'pip'; + case AddConfigurationType.DockerImage: + return 'docker'; + case AddConfigurationType.Stdio: + return 'stdio'; + case AddConfigurationType.SSE: + return 'sse'; + default: + return undefined; + } + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpDiscovery.ts b/src/vs/workbench/contrib/mcp/browser/mcpDiscovery.ts new file mode 100644 index 00000000..dc07ad2a --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpDiscovery.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js'; +import { mcpEnabledSection } from '../common/mcpConfiguration.js'; + +export class McpDiscovery extends Disposable implements IWorkbenchContribution { + public static readonly ID = 'workbench.contrib.mcp.discovery'; + + constructor( + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + + const enabled = observableConfigValue(mcpEnabledSection, true, configurationService); + const store = this._register(new DisposableStore()); + + this._register(autorun(reader => { + if (enabled.read(reader)) { + for (const discovery of mcpDiscoveryRegistry.getAll()) { + const inst = store.add(instantiationService.createInstance(discovery)); + inst.start(); + } + } else { + store.clear(); + } + })); + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts b/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts new file mode 100644 index 00000000..8f81edd8 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts @@ -0,0 +1,372 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { computeLevenshteinDistance } from '../../../../base/common/diff/diff.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { markdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { findNodeAtLocation, Node, parseTree } from '../../../../base/common/json.js'; +import { Disposable, DisposableStore, dispose, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../base/common/observable.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { CodeLensList, CodeLensProvider, InlayHint, InlayHintList } from '../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { localize } from '../../../../nls.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js'; +import { ConfigurationResolverExpression, IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; +import { IMcpConfigPath, IMcpConfigPathsService } from '../common/mcpConfigPathsService.js'; +import { mcpConfigurationSection } from '../common/mcpConfiguration.js'; +import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; +import { IMcpService, McpConnectionState } from '../common/mcpTypes.js'; +import { EditStoredInput, RemoveStoredInput, RestartServer, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; + +const diagnosticOwner = 'vscode.mcp'; + +export class McpLanguageFeatures extends Disposable implements IWorkbenchContribution { + private readonly _cachedMcpSection = this._register(new MutableDisposable<{ model: ITextModel; inConfig: IMcpConfigPath; tree: Node } & IDisposable>()); + + constructor( + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @IMcpConfigPathsService private readonly _mcpConfigPathsService: IMcpConfigPathsService, + @IMcpService private readonly _mcpService: IMcpService, + @IMarkerService private readonly _markerService: IMarkerService, + @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, + ) { + super(); + + const patterns = [ + { pattern: '**/.vscode/mcp.json' }, + { pattern: '**/settings.json' }, + { pattern: '**/workspace.json' }, + ]; + + const onDidChangeCodeLens = this._register(new Emitter()); + const codeLensProvider: CodeLensProvider = { + onDidChange: onDidChangeCodeLens.event, + provideCodeLenses: (model, range) => this._provideCodeLenses(model, () => onDidChangeCodeLens.fire(codeLensProvider)), + }; + this._register(languageFeaturesService.codeLensProvider.register(patterns, codeLensProvider)); + + this._register(languageFeaturesService.inlayHintsProvider.register(patterns, { + onDidChangeInlayHints: _mcpRegistry.onDidChangeInputs, + provideInlayHints: (model, range) => this._provideInlayHints(model, range), + })); + } + + /** Simple mechanism to avoid extra json parsing for hints+lenses */ + private _parseModel(model: ITextModel) { + if (this._cachedMcpSection.value?.model === model) { + return this._cachedMcpSection.value; + } + + const uri = model.uri; + const inConfig = this._mcpConfigPathsService.paths.get().find(u => isEqual(u.uri, uri)); + if (!inConfig) { + return undefined; + } + + const value = model.getValue(); + const tree = parseTree(value); + const listeners = [ + model.onDidChangeContent(() => this._cachedMcpSection.clear()), + model.onWillDispose(() => this._cachedMcpSection.clear()), + ]; + this._addDiagnostics(model, value, tree, inConfig); + + return this._cachedMcpSection.value = { + model, + tree, + inConfig, + dispose: () => { + this._markerService.remove(diagnosticOwner, [uri]); + dispose(listeners); + } + }; + } + + private _addDiagnostics(tm: ITextModel, value: string, tree: Node, inConfig: IMcpConfigPath) { + const serversNode = findNodeAtLocation(tree, inConfig.section ? [...inConfig.section, 'servers'] : ['servers']); + if (!serversNode) { + return; + } + + const getClosestMatchingVariable = (name: string) => { + let bestValue = ''; + let bestDistance = Infinity; + for (const variable of this._configurationResolverService.resolvableVariables) { + const distance = computeLevenshteinDistance(name, variable); + if (distance < bestDistance) { + bestDistance = distance; + bestValue = variable; + } + } + return bestValue; + }; + + const diagnostics: IMarkerData[] = []; + forEachPropertyWithReplacement(serversNode, node => { + const expr = ConfigurationResolverExpression.parse(node.value); + + for (const { id, name, arg } of expr.unresolved()) { + if (!this._configurationResolverService.resolvableVariables.has(name)) { + const position = value.indexOf(id, node.offset); + if (position === -1) { continue; } // unreachable? + + const start = tm.getPositionAt(position); + const end = tm.getPositionAt(position + id.length); + diagnostics.push({ + severity: MarkerSeverity.Warning, + message: localize('mcp.variableNotFound', 'Variable `{0}` not found, did you mean ${{1}}?', name, getClosestMatchingVariable(name) + (arg ? `:${arg}` : '')), + startLineNumber: start.lineNumber, + startColumn: start.column, + endLineNumber: end.lineNumber, + endColumn: end.column, + modelVersionId: tm.getVersionId(), + }); + } + } + }); + + if (diagnostics.length) { + this._markerService.changeOne(diagnosticOwner, tm.uri, diagnostics); + } else { + this._markerService.remove(diagnosticOwner, [tm.uri]); + } + } + + private _provideCodeLenses(model: ITextModel, onDidChangeCodeLens: () => void): CodeLensList | undefined { + const parsed = this._parseModel(model); + if (!parsed) { + return undefined; + } + + const { tree, inConfig } = parsed; + const serversNode = findNodeAtLocation(tree, inConfig.section ? [...inConfig.section, 'servers'] : ['servers']); + if (!serversNode) { + return undefined; + } + + const store = new DisposableStore(); + const lenses: CodeLensList = { lenses: [], dispose: () => store.dispose() }; + const read = (observable: IObservable): T => { + store.add(Event.fromObservableLight(observable)(onDidChangeCodeLens)); + return observable.get(); + }; + + const collection = read(this._mcpRegistry.collections).find(c => isEqual(c.presentation?.origin, model.uri)); + if (!collection) { + return lenses; + } + + const mcpServers = read(this._mcpService.servers).filter(s => s.collection.id === collection.id); + for (const node of serversNode.children || []) { + if (node.type !== 'property' || node.children?.[0]?.type !== 'string') { + continue; + } + + const name = node.children[0].value as string; + const server = mcpServers.find(s => s.definition.label === name); + if (!server) { + continue; + } + + const range = Range.fromPositions(model.getPositionAt(node.children[0].offset)); + switch (read(server.connectionState).state) { + case McpConnectionState.Kind.Error: + lenses.lenses.push({ + range, + command: { + id: ShowOutput.ID, + title: '$(error) ' + localize('server.error', 'Error'), + arguments: [server.definition.id], + }, + }, { + range, + command: { + id: RestartServer.ID, + title: localize('mcp.restart', "Restart"), + arguments: [server.definition.id], + }, + }); + break; + case McpConnectionState.Kind.Starting: + lenses.lenses.push({ + range, + command: { + id: ShowOutput.ID, + title: '$(loading~spin) ' + localize('server.starting', 'Starting'), + arguments: [server.definition.id], + }, + }, { + range, + command: { + id: StopServer.ID, + title: localize('cancel', "Cancel"), + arguments: [server.definition.id], + }, + }); + break; + case McpConnectionState.Kind.Running: + lenses.lenses.push({ + range, + command: { + id: ShowOutput.ID, + title: '$(check) ' + localize('server.running', 'Running'), + arguments: [server.definition.id], + }, + }, { + range, + command: { + id: StopServer.ID, + title: localize('mcp.stop', "Stop"), + arguments: [server.definition.id], + }, + }, { + range, + command: { + id: RestartServer.ID, + title: localize('mcp.restart', "Restart"), + arguments: [server.definition.id], + }, + }, { + range, + command: { + id: '', + title: localize('server.toolCount', '{0} tools', read(server.tools).length), + }, + }); + break; + case McpConnectionState.Kind.Stopped: { + lenses.lenses.push({ + range, + command: { + id: StartServer.ID, + title: '$(debug-start) ' + localize('mcp.start', "Start"), + arguments: [server.definition.id], + }, + }); + const toolCount = read(server.tools).length; + if (toolCount) { + lenses.lenses.push({ + range, + command: { + id: '', + title: localize('server.toolCountCached', '{0} cached tools', toolCount), + } + }); + } + } + } + } + + return lenses; + } + + private async _provideInlayHints(model: ITextModel, range: Range): Promise { + const parsed = this._parseModel(model); + if (!parsed) { + return undefined; + } + + const { tree, inConfig } = parsed; + const mcpSection = inConfig.section ? findNodeAtLocation(tree, [...inConfig.section]) : tree; + if (!mcpSection) { + return undefined; + } + + const inputsNode = findNodeAtLocation(mcpSection, ['inputs']); + if (!inputsNode) { + return undefined; + } + + const inputs = await this._mcpRegistry.getSavedInputs(inConfig.scope); + const hints: InlayHint[] = []; + + const serversNode = findNodeAtLocation(mcpSection, ['servers']); + if (serversNode) { + annotateServers(serversNode); + } + annotateInputs(inputsNode); + + return { hints, dispose: () => { } }; + + function annotateServers(servers: Node) { + forEachPropertyWithReplacement(servers, node => { + const expr = ConfigurationResolverExpression.parse(node.value); + for (const { id } of expr.unresolved()) { + const saved = inputs[id]; + if (saved) { + pushAnnotation(id, node.offset + node.value.indexOf(id) + id.length, saved); + } + } + }); + } + + function annotateInputs(node: Node) { + if (node.type !== 'array' || !node.children) { + return; + } + + for (const input of node.children) { + if (input.type !== 'object' || !input.children) { + continue; + } + + const idProp = input.children.find(c => c.type === 'property' && c.children?.[0].value === 'id'); + if (!idProp) { + continue; + } + + const id = idProp.children![1]; + if (!id || id.type !== 'string' || !id.value) { + continue; + } + + const savedId = '${input:' + id.value + '}'; + const saved = inputs[savedId]; + if (saved) { + pushAnnotation(savedId, id.offset + 1 + id.length, saved); + } + } + } + + function pushAnnotation(savedId: string, offset: number, saved: IResolvedValue): InlayHint { + const tooltip = new MarkdownString([ + markdownCommandLink({ id: EditStoredInput.ID, title: localize('edit', 'Edit'), arguments: [savedId, model.uri, mcpConfigurationSection, inConfig!.target] }), + markdownCommandLink({ id: RemoveStoredInput.ID, title: localize('clear', 'Clear'), arguments: [inConfig!.scope, savedId] }), + markdownCommandLink({ id: RemoveStoredInput.ID, title: localize('clearAll', 'Clear All'), arguments: [inConfig!.scope] }), + ].join(' | '), { isTrusted: true }); + + const hint: InlayHint = { + label: '= ' + (saved.input?.type === 'promptString' && saved.input.password ? '*'.repeat(10) : (saved.value || '')), + position: model.getPositionAt(offset), + tooltip, + paddingLeft: true, + }; + + hints.push(hint); + return hint; + } + } +} + + + +function forEachPropertyWithReplacement(node: Node, callback: (node: Node) => void) { + if (node.type === 'string' && typeof node.value === 'string' && node.value.includes(ConfigurationResolverExpression.VARIABLE_LHS)) { + callback(node); + } else if (node.type === 'property') { + // skip the property name + node.children?.slice(1).forEach(n => forEachPropertyWithReplacement(n, callback)); + } else { + node.children?.forEach(n => forEachPropertyWithReplacement(n, callback)); + } +} + + diff --git a/src/vs/workbench/contrib/mcp/browser/mcpUrlHandler.ts b/src/vs/workbench/contrib/mcp/browser/mcpUrlHandler.ts new file mode 100644 index 00000000..b6221244 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpUrlHandler.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { Lazy } from '../../../../base/common/lazy.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { InMemoryFileSystemProvider } from '../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { McpConfigurationServer } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { IOpenURLOptions, IURLHandler, IURLService } from '../../../../platform/url/common/url.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js'; + +const providerScheme = 'mcp-install'; + +export class McpUrlHandler extends Disposable implements IWorkbenchContribution, IURLHandler { + public static readonly scheme = providerScheme; + + + private readonly _fileSystemProvider = new Lazy(() => { + return this._instaService.invokeFunction(accessor => { + const fileService = accessor.get(IFileService); + const filesystem = new InMemoryFileSystemProvider(); + this._register(fileService.registerProvider(providerScheme, filesystem)); + return providerScheme; + }); + }); + + constructor( + @IURLService urlService: IURLService, + @IInstantiationService private readonly _instaService: IInstantiationService, + @IFileService private readonly _fileService: IFileService, + ) { + super(); + this._register(urlService.registerHandler(this)); + } + + async handleURL(uri: URI, options?: IOpenURLOptions): Promise { + if (uri.path !== 'mcp/install') { + return false; + } + + let parsed: McpConfigurationServer & { name: string }; + try { + parsed = JSON.parse(decodeURIComponent(uri.query)); + } catch (e) { + return false; + } + + const { name, ...rest } = parsed; + + const scheme = this._fileSystemProvider.value; + const fileUri = URI.from({ scheme, path: `/${encodeURIComponent(name)}.json` }); + + await this._fileService.writeFile( + fileUri, + VSBuffer.fromString(JSON.stringify(rest, null, '\t')), + ); + + const addConfigHelper = this._instaService.createInstance(McpAddConfigurationCommand, undefined); + addConfigHelper.pickForUrlHandler(fileUri, true); + + return Promise.resolve(true); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts new file mode 100644 index 00000000..70969d39 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/configMcpDiscovery.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equals as arrayEquals } from '../../../../../base/common/arrays.js'; +import { Throttler } from '../../../../../base/common/async.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorunDelta, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { Location } from '../../../../../editor/common/languages.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { getMcpServerMapping } from '../mcpConfigFileUtils.js'; +import { IMcpConfigPath, IMcpConfigPathsService } from '../mcpConfigPathsService.js'; +import { IMcpConfiguration, mcpConfigurationSection } from '../mcpConfiguration.js'; +import { IMcpRegistry } from '../mcpRegistryTypes.js'; +import { McpServerDefinition, McpServerTransportType } from '../mcpTypes.js'; +import { IMcpDiscovery } from './mcpDiscovery.js'; + +interface ConfigSource { + path: IMcpConfigPath; + serverDefinitions: ISettableObservable; + disposable: MutableDisposable; + getServerToLocationMapping(uri: URI): Promise>; +} + +/** + * Discovers MCP servers based on various config sources. + */ +export class ConfigMcpDiscovery extends Disposable implements IMcpDiscovery { + private configSources: ConfigSource[] = []; + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @ITextModelService private readonly _textModelService: ITextModelService, + @IMcpConfigPathsService private readonly _mcpConfigPathsService: IMcpConfigPathsService, + ) { + super(); + } + + public start() { + const throttler = this._register(new Throttler()); + + const addPath = (path: IMcpConfigPath) => { + this.configSources.push({ + path, + serverDefinitions: observableValue(this, []), + disposable: this._register(new MutableDisposable()), + getServerToLocationMapping: (uri) => this._getServerIdMapping(uri, path.section ? [...path.section, 'servers'] : ['servers']), + }); + }; + + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(mcpConfigurationSection)) { + throttler.queue(() => this.sync()); + } + })); + + this._register(autorunDelta(this._mcpConfigPathsService.paths, ({ lastValue, newValue }) => { + for (const last of lastValue || []) { + if (!newValue.includes(last)) { + const idx = this.configSources.findIndex(src => src.path.id === last.id); + if (idx !== -1) { + this.configSources[idx].disposable.dispose(); + this.configSources.splice(idx, 1); + } + } + } + + for (const next of newValue) { + if (!lastValue || !lastValue.includes(next)) { + addPath(next); + } + } + + this.sync(); + })); + } + + private async _getServerIdMapping(resource: URI, pathToServers: string[]): Promise> { + const store = new DisposableStore(); + try { + const ref = await this._textModelService.createModelReference(resource); + store.add(ref); + const serverIdMapping = getMcpServerMapping({ model: ref.object.textEditorModel, pathToServers }); + return serverIdMapping; + } catch { + return new Map(); + } finally { + store.dispose(); + } + } + + private async sync() { + const configurationKey = this._configurationService.inspect(mcpConfigurationSection); + const configMappings = await Promise.all(this.configSources.map(src => { + const uri = src.path.uri; + return uri && src.getServerToLocationMapping(uri); + })); + + for (const [index, src] of this.configSources.entries()) { + const collectionId = `mcp.config.${src.path.id}`; + // inspect() will give the first workspace folder, and must be + // asked for explicitly for other folders. + let value = src.path.workspaceFolder + ? this._configurationService.inspect(mcpConfigurationSection, { resource: src.path.workspaceFolder.uri })[src.path.key] + : configurationKey[src.path.key]; + + // If we see there are MCP servers, migrate them automatically + if (value?.mcpServers) { + value = { ...value, servers: { ...value.servers, ...value.mcpServers }, mcpServers: undefined }; + this._configurationService.updateValue(mcpConfigurationSection, value, {}, src.path.target, { donotNotifyError: true }); + } + + const configMapping = configMappings[index]; + const nextDefinitions = Object.entries(value?.servers || {}).map(([name, value]): McpServerDefinition => ({ + id: `${collectionId}.${name}`, + label: name, + launch: 'type' in value && value.type === 'sse' ? { + type: McpServerTransportType.SSE, + uri: URI.parse(value.url), + headers: Object.entries(value.headers || {}), + } : { + type: McpServerTransportType.Stdio, + args: value.args || [], + command: value.command, + env: value.env || {}, + envFile: value.envFile, + cwd: undefined, + }, + roots: src.path.workspaceFolder ? [src.path.workspaceFolder.uri] : [], + variableReplacement: { + folder: src.path.workspaceFolder, + section: mcpConfigurationSection, + target: src.path.target, + }, + presentation: { + order: src.path.order, + origin: configMapping?.get(name), + } + })); + + if (arrayEquals(nextDefinitions, src.serverDefinitions.get(), McpServerDefinition.equals)) { + continue; + } + + if (!nextDefinitions.length) { + src.disposable.clear(); + src.serverDefinitions.set(nextDefinitions, undefined); + } else { + src.serverDefinitions.set(nextDefinitions, undefined); + src.disposable.value ??= this._mcpRegistry.registerCollection({ + id: collectionId, + label: src.path.label, + presentation: { order: src.path.order, origin: src.path.uri }, + remoteAuthority: src.path.remoteAuthority || null, + serverDefinitions: src.serverDefinitions, + isTrustedByDefault: true, + scope: src.path.scope, + }); + } + } + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts new file mode 100644 index 00000000..1c3a96b5 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/extensionMcpDiscovery.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../base/common/observable.js'; +import { isFalsyOrWhitespace } from '../../../../../base/common/strings.js'; +import { localize } from '../../../../../nls.js'; +import { IMcpCollectionContribution } from '../../../../../platform/extensions/common/extensions.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; +import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; +import { mcpActivationEvent, mcpContributionPoint } from '../mcpConfiguration.js'; +import { IMcpRegistry } from '../mcpRegistryTypes.js'; +import { extensionPrefixedIdentifier, McpServerDefinition } from '../mcpTypes.js'; +import { IMcpDiscovery } from './mcpDiscovery.js'; + +const cacheKey = 'mcp.extCachedServers'; + +interface IServerCacheEntry { + readonly servers: readonly McpServerDefinition.Serialized[]; +} + +const _mcpExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint(mcpContributionPoint); + +export class ExtensionMcpDiscovery extends Disposable implements IMcpDiscovery { + private readonly _extensionCollectionIdsToPersist = new Set(); + private readonly cachedServers: { [collcetionId: string]: IServerCacheEntry }; + + constructor( + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @IStorageService storageService: IStorageService, + @IExtensionService private readonly _extensionService: IExtensionService, + ) { + super(); + this.cachedServers = storageService.getObject(cacheKey, StorageScope.WORKSPACE, {}); + + this._register(storageService.onWillSaveState(() => { + let updated = false; + for (const collectionId of this._extensionCollectionIdsToPersist) { + const collection = this._mcpRegistry.collections.get().find(c => c.id === collectionId); + if (!collection || collection.lazy) { + continue; + } + + const defs = collection.serverDefinitions.get(); + if (defs) { + updated = true; + this.cachedServers[collectionId] = { servers: defs.map(McpServerDefinition.toSerialized) }; + } + } + + if (updated) { + storageService.store(cacheKey, this.cachedServers, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + })); + } + + public start(): void { + const extensionCollections = this._register(new DisposableMap()); + this._register(_mcpExtensionPoint.setHandler((_extensions, delta) => { + const { added, removed } = delta; + + for (const collections of removed) { + for (const coll of collections.value) { + extensionCollections.deleteAndDispose(extensionPrefixedIdentifier(collections.description.identifier, coll.id)); + } + } + + for (const collections of added) { + + if (!ExtensionMcpDiscovery._validate(collections)) { + continue; + } + + for (const coll of collections.value) { + const id = extensionPrefixedIdentifier(collections.description.identifier, coll.id); + this._extensionCollectionIdsToPersist.add(id); + + const serverDefs = this.cachedServers.hasOwnProperty(id) ? this.cachedServers[id].servers : undefined; + const dispo = this._mcpRegistry.registerCollection({ + id, + label: coll.label, + remoteAuthority: null, + isTrustedByDefault: true, + scope: StorageScope.WORKSPACE, + serverDefinitions: observableValue(this, serverDefs?.map(McpServerDefinition.fromSerialized) || []), + lazy: { + isCached: !!serverDefs, + load: () => this._activateExtensionServers(coll.id), + removed: () => extensionCollections.deleteAndDispose(id), + } + }); + + extensionCollections.set(id, dispo); + } + } + })); + } + + private async _activateExtensionServers(collectionId: string): Promise { + await this._extensionService.activateByEvent(mcpActivationEvent(collectionId)); + await Promise.all(this._mcpRegistry.delegates + .map(r => r.waitForInitialProviderPromises())); + } + + private static _validate(user: extensionsRegistry.IExtensionPointUser): boolean { + + if (!Array.isArray(user.value)) { + user.collector.error(localize('invalidData', "Expected an array of MCP collections")); + return false; + } + + for (const contribution of user.value) { + if (typeof contribution.id !== 'string' || isFalsyOrWhitespace(contribution.id)) { + user.collector.error(localize('invalidId', "Expected 'id' to be a non-empty string.")); + return false; + } + if (typeof contribution.label !== 'string' || isFalsyOrWhitespace(contribution.label)) { + user.collector.error(localize('invalidLabel', "Expected 'label' to be a non-empty string.")); + return false; + } + } + + return true; + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/mcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/mcpDiscovery.ts new file mode 100644 index 00000000..a6a091cf --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/mcpDiscovery.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { SyncDescriptor0 } from '../../../../../platform/instantiation/common/descriptors.js'; + + +export interface IMcpDiscovery extends IDisposable { + start(): void; +} + +class McpDiscoveryRegistry { + private readonly _discovery: SyncDescriptor0[] = []; + + register(discovery: SyncDescriptor0): void { + this._discovery.push(discovery); + } + + getAll(): readonly SyncDescriptor0[] { + return this._discovery; + } +} + +export const mcpDiscoveryRegistry = new McpDiscoveryRegistry(); + + + diff --git a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAbstract.ts b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAbstract.ts new file mode 100644 index 00000000..c2a7e649 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAbstract.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from '../../../../../base/common/async.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { autorunWithStore, IObservable, IReader, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { INativeMcpDiscoveryData } from '../../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; +import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { Dto } from '../../../../services/extensions/common/proxyIdentifier.js'; +import { DiscoverySource, discoverySourceLabel, mcpDiscoverySection } from '../mcpConfiguration.js'; +import { IMcpRegistry } from '../mcpRegistryTypes.js'; +import { McpCollectionDefinition, McpCollectionSortOrder, McpServerDefinition } from '../mcpTypes.js'; +import { IMcpDiscovery } from './mcpDiscovery.js'; +import { ClaudeDesktopMpcDiscoveryAdapter, CursorDesktopMpcDiscoveryAdapter, NativeMpcDiscoveryAdapter, WindsurfDesktopMpcDiscoveryAdapter } from './nativeMcpDiscoveryAdapters.js'; + +export type WritableMcpCollectionDefinition = McpCollectionDefinition & { serverDefinitions: ISettableObservable }; + +export abstract class FilesystemMcpDiscovery extends Disposable { + protected readonly _fsDiscoveryEnabled: IObservable; + + constructor( + @IConfigurationService configurationService: IConfigurationService, + @IFileService private readonly _fileService: IFileService, + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + ) { + super(); + + this._fsDiscoveryEnabled = observableConfigValue(mcpDiscoverySection, true, configurationService); + } + + protected _isDiscoveryEnabled(reader: IReader, discoverySource: DiscoverySource | undefined): boolean { + const fsDiscovery = this._fsDiscoveryEnabled.read(reader); + if (typeof fsDiscovery === 'boolean') { + return fsDiscovery; + } + if (discoverySource && fsDiscovery[discoverySource] === false) { + return false; + } + return true; + } + + protected watchFile( + file: URI, + collection: WritableMcpCollectionDefinition, + discoverySource: DiscoverySource | undefined, + adaptFile: (contents: VSBuffer) => McpServerDefinition[] | undefined, + ): IDisposable { + const store = new DisposableStore(); + const collectionRegistration = store.add(new MutableDisposable()); + const updateFile = async () => { + let definitions: McpServerDefinition[] = []; + try { + const contents = await this._fileService.readFile(file); + definitions = adaptFile(contents.value) || []; + } catch { + // ignored + } + if (!definitions.length) { + collectionRegistration.clear(); + } else { + collection.serverDefinitions.set(definitions, undefined); + if (!collectionRegistration.value) { + collectionRegistration.value = this._mcpRegistry.registerCollection(collection); + } + } + }; + + store.add(autorunWithStore((reader, store) => { + if (!this._isDiscoveryEnabled(reader, discoverySource)) { + collectionRegistration.clear(); + return; + } + + const throttler = store.add(new RunOnceScheduler(updateFile, 500)); + const watcher = store.add(this._fileService.createWatcher(file, { recursive: false, excludes: [] })); + store.add(watcher.onDidChange(() => throttler.schedule())); + updateFile(); + })); + + return store; + } +} + +/** + * Base class that discovers MCP servers on a filesystem, outside of the ones + * defined in VS Code settings. + */ +export abstract class NativeFilesystemMcpDiscovery extends FilesystemMcpDiscovery implements IMcpDiscovery { + private readonly adapters: readonly NativeMpcDiscoveryAdapter[]; + private suffix = ''; + + constructor( + remoteAuthority: string | null, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService, + @IMcpRegistry mcpRegistry: IMcpRegistry, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(configurationService, fileService, mcpRegistry); + if (remoteAuthority) { + this.suffix = ' ' + localize('onRemoteLabel', ' on {0}', labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority)); + } + + this.adapters = [ + instantiationService.createInstance(ClaudeDesktopMpcDiscoveryAdapter, remoteAuthority), + instantiationService.createInstance(CursorDesktopMpcDiscoveryAdapter, remoteAuthority), + instantiationService.createInstance(WindsurfDesktopMpcDiscoveryAdapter, remoteAuthority), + ]; + } + + public abstract start(): void; + + protected setDetails(detailsDto: Dto | undefined) { + if (!detailsDto) { + return; + } + + const details: INativeMcpDiscoveryData = { + ...detailsDto, + homedir: URI.revive(detailsDto.homedir), + xdgHome: detailsDto.xdgHome ? URI.revive(detailsDto.xdgHome) : undefined, + winAppData: detailsDto.winAppData ? URI.revive(detailsDto.winAppData) : undefined, + }; + + for (const adapter of this.adapters) { + const file = adapter.getFilePath(details); + if (!file) { + continue; + } + + const collection: WritableMcpCollectionDefinition = { + id: adapter.id, + label: discoverySourceLabel[adapter.discoverySource] + this.suffix, + remoteAuthority: adapter.remoteAuthority, + scope: StorageScope.PROFILE, + isTrustedByDefault: false, + serverDefinitions: observableValue(this, []), + presentation: { + origin: file, + order: adapter.order + (adapter.remoteAuthority ? McpCollectionSortOrder.RemoteBoost : 0), + }, + }; + + this._register(this.watchFile(file, collection, adapter.discoverySource, contents => adapter.adaptFile(contents, details))); + } + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts new file mode 100644 index 00000000..0abe6cd9 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { Platform } from '../../../../../base/common/platform.js'; +import { Mutable } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { INativeMcpDiscoveryData } from '../../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; +import { DiscoverySource } from '../mcpConfiguration.js'; +import { McpCollectionSortOrder, McpServerDefinition, McpServerTransportType } from '../mcpTypes.js'; + +export interface NativeMpcDiscoveryAdapter { + readonly remoteAuthority: string | null; + readonly id: string; + readonly order: number; + readonly discoverySource: DiscoverySource; + + getFilePath(details: INativeMcpDiscoveryData): URI | undefined; + adaptFile(contents: VSBuffer, details: INativeMcpDiscoveryData): McpServerDefinition[] | undefined; +} + +export function claudeConfigToServerDefinition(idPrefix: string, contents: VSBuffer, cwd?: URI) { + let parsed: { + mcpServers: Record; + url?: string; + }>; + }; + + try { + parsed = JSON.parse(contents.toString()); + } catch { + return; + } + + return Object.entries(parsed.mcpServers).map(([name, server]): Mutable => { + return { + id: `${idPrefix}.${name}`, + label: name, + launch: server.url ? { + type: McpServerTransportType.SSE, + uri: URI.parse(server.url), + headers: [], + } : { + type: McpServerTransportType.Stdio, + args: server.args || [], + command: server.command, + env: server.env || {}, + envFile: undefined, + cwd, + } + }; + }); +} + +export class ClaudeDesktopMpcDiscoveryAdapter implements NativeMpcDiscoveryAdapter { + public id: string; + public readonly order = McpCollectionSortOrder.Filesystem; + public readonly discoverySource: DiscoverySource = DiscoverySource.ClaudeDesktop; + + constructor(public readonly remoteAuthority: string | null) { + this.id = `claude-desktop.${this.remoteAuthority}`; + } + + getFilePath({ platform, winAppData, xdgHome, homedir }: INativeMcpDiscoveryData): URI | undefined { + if (platform === Platform.Windows) { + const appData = winAppData || URI.joinPath(homedir, 'AppData', 'Roaming'); + return URI.joinPath(appData, 'Claude', 'claude_desktop_config.json'); + } else if (platform === Platform.Mac) { + return URI.joinPath(homedir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'); + } else { + const configDir = xdgHome || URI.joinPath(homedir, '.config'); + return URI.joinPath(configDir, 'Claude', 'claude_desktop_config.json'); + } + } + + adaptFile(contents: VSBuffer, { homedir }: INativeMcpDiscoveryData): McpServerDefinition[] | undefined { + return claudeConfigToServerDefinition(this.id, contents, homedir); + } +} + +export class WindsurfDesktopMpcDiscoveryAdapter extends ClaudeDesktopMpcDiscoveryAdapter { + public override readonly discoverySource: DiscoverySource = DiscoverySource.Windsurf; + + constructor(remoteAuthority: string | null) { + super(remoteAuthority); + this.id = `windsurf.${this.remoteAuthority}`; + } + + override getFilePath({ homedir }: INativeMcpDiscoveryData): URI | undefined { + return URI.joinPath(homedir, '.codeium', 'windsurf', 'mcp_config.json'); + } +} + +export class CursorDesktopMpcDiscoveryAdapter extends ClaudeDesktopMpcDiscoveryAdapter { + public override readonly discoverySource: DiscoverySource = DiscoverySource.CursorGlobal; + + constructor(remoteAuthority: string | null) { + super(remoteAuthority); + this.id = `cursor.${this.remoteAuthority}`; + } + + override getFilePath({ homedir }: INativeMcpDiscoveryData): URI | undefined { + return URI.joinPath(homedir, '.cursor', 'mcp.json'); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpRemoteDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpRemoteDiscovery.ts new file mode 100644 index 00000000..a2473c98 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpRemoteDiscovery.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProxyChannel } from '../../../../../base/parts/ipc/common/ipc.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { INativeMcpDiscoveryHelperService, NativeMcpDiscoveryHelperChannelName } from '../../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; +import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js'; +import { IMcpRegistry } from '../mcpRegistryTypes.js'; +import { NativeFilesystemMcpDiscovery } from './nativeMcpDiscoveryAbstract.js'; + +/** + * Discovers MCP servers on the remote filesystem, if any. + */ +export class RemoteNativeMpcDiscovery extends NativeFilesystemMcpDiscovery { + constructor( + @IRemoteAgentService private readonly remoteAgent: IRemoteAgentService, + @ILogService private readonly logService: ILogService, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService, + @IMcpRegistry mcpRegistry: IMcpRegistry, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(remoteAgent.getConnection()?.remoteAuthority || null, labelService, fileService, instantiationService, mcpRegistry, configurationService); + } + + public override async start() { + const connection = this.remoteAgent.getConnection(); + if (!connection) { + return this.setDetails(undefined); + } + + await connection.withChannel(NativeMcpDiscoveryHelperChannelName, async channel => { + const service = ProxyChannel.toService(channel); + + service.load().then( + data => this.setDetails(data), + err => { + this.logService.warn('Error getting remote process MCP environment', err); + this.setDetails(undefined); + } + ); + }); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/discovery/workspaceMcpDiscoveryAdapter.ts b/src/vs/workbench/contrib/mcp/common/discovery/workspaceMcpDiscoveryAdapter.ts new file mode 100644 index 00000000..4701f10c --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/discovery/workspaceMcpDiscoveryAdapter.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableMap, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../base/common/observable.js'; +import { joinPath } from '../../../../../base/common/resources.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js'; +import { IRemoteAgentService } from '../../../../services/remote/common/remoteAgentService.js'; +import { DiscoverySource } from '../mcpConfiguration.js'; +import { IMcpRegistry } from '../mcpRegistryTypes.js'; +import { McpCollectionSortOrder } from '../mcpTypes.js'; +import { IMcpDiscovery } from './mcpDiscovery.js'; +import { FilesystemMcpDiscovery, WritableMcpCollectionDefinition } from './nativeMcpDiscoveryAbstract.js'; +import { claudeConfigToServerDefinition } from './nativeMcpDiscoveryAdapters.js'; + +export class CursorWorkspaceMcpDiscoveryAdapter extends FilesystemMcpDiscovery implements IMcpDiscovery { + private readonly _collections = this._register(new DisposableMap()); + + constructor( + @IFileService fileService: IFileService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IMcpRegistry mcpRegistry: IMcpRegistry, + @IConfigurationService configurationService: IConfigurationService, + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + ) { + super(configurationService, fileService, mcpRegistry); + } + + start(): void { + this._register(this._workspaceContextService.onDidChangeWorkspaceFolders(e => { + for (const removed of e.removed) { + this._collections.deleteAndDispose(removed.uri.toString()); + } + for (const added of e.added) { + this.watchFolder(added); + } + })); + + for (const folder of this._workspaceContextService.getWorkspace().folders) { + this.watchFolder(folder); + } + } + + private watchFolder(folder: IWorkspaceFolder) { + const configFile = joinPath(folder.uri, '.cursor', 'mcp.json'); + const collection: WritableMcpCollectionDefinition = { + id: `cursor-workspace.${folder.index}`, + label: `${folder.name}/.cursor/mcp.json`, + remoteAuthority: this._remoteAgentService.getConnection()?.remoteAuthority || null, + scope: StorageScope.WORKSPACE, + isTrustedByDefault: false, + serverDefinitions: observableValue(this, []), + presentation: { + origin: configFile, + order: McpCollectionSortOrder.WorkspaceFolder + 1, + }, + }; + + this._collections.set(folder.uri.toString(), this.watchFile( + URI.joinPath(folder.uri, '.cursor', 'mcp.json'), + collection, + DiscoverySource.CursorWorkspace, + contents => { + const defs = claudeConfigToServerDefinition(collection.id, contents, folder.uri); + defs?.forEach(d => d.roots = [folder.uri]); + return defs; + } + )); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfigFileUtils.ts b/src/vs/workbench/contrib/mcp/common/mcpConfigFileUtils.ts new file mode 100644 index 00000000..641782c5 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpConfigFileUtils.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { findNodeAtLocation, parseTree as jsonParseTree } from '../../../../base/common/json.js'; +import { Location } from '../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../editor/common/model.js'; + +export const getMcpServerMapping = (opts: { + model: ITextModel; + // Path to MCP servers in the config. + pathToServers: string[]; +}): Map => { + const tree = jsonParseTree(opts.model.getValue()); + const servers = findNodeAtLocation(tree, opts.pathToServers); + if (!servers || servers.type !== 'object') { + return new Map(); + } + + const result = new Map(); + for (const node of servers.children || []) { + if (node.type !== 'property' || node.children?.[0]?.type !== 'string') { + continue; + } + + const start = opts.model.getPositionAt(node.offset); + const end = opts.model.getPositionAt(node.offset + node.length); + result.set(node.children[0].value, { + uri: opts.model.uri, + range: { + startLineNumber: start.lineNumber, + startColumn: start.column, + endLineNumber: end.lineNumber, + endColumn: end.column, + } + }); + } + + return result; +}; diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfigPathsService.ts b/src/vs/workbench/contrib/mcp/common/mcpConfigPathsService.ts new file mode 100644 index 00000000..e86c822b --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpConfigPathsService.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { basename } from '../../../../base/common/resources.js'; +import { isDefined } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; +import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { StorageScope } from '../../../../platform/storage/common/storage.js'; +import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { FOLDER_SETTINGS_PATH, IPreferencesService } from '../../../services/preferences/common/preferences.js'; +import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; +import { mcpConfigurationSection } from './mcpConfiguration.js'; +import { McpCollectionSortOrder } from './mcpTypes.js'; + +export interface IMcpConfigPath { + /** Short, unique ID for this config. */ + id: string; + /** Configuration scope that maps to this path. */ + key: 'userLocalValue' | 'userRemoteValue' | 'workspaceValue' | 'workspaceFolderValue'; + /** Display name */ + label: string; + /** Storage where associated data should be stored. */ + scope: StorageScope; + /** Configuration target that correspond to this file */ + target: ConfigurationTarget; + /** Order in which the configuration should be displayed */ + order: number; + /** Config's remote authority */ + remoteAuthority?: string; + /** Config file URI. */ + uri: URI | undefined; + /** When MCP config is nested in a config file, the parent nested key. */ + section?: string[]; + /** Workspace folder, when the config refers to a workspace folder value. */ + workspaceFolder?: IWorkspaceFolder; +} + +export interface IMcpConfigPathsService { + _serviceBrand: undefined; + + readonly paths: IObservable; +} + +export const IMcpConfigPathsService = createDecorator('IMcpConfigPathsService'); + +export class McpConfigPathsService extends Disposable implements IMcpConfigPathsService { + _serviceBrand: undefined; + + private readonly _paths: ISettableObservable; + + public get paths(): IObservable { + return this._paths; + } + + constructor( + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, + @IProductService productService: IProductService, + @ILabelService labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IPreferencesService preferencesService: IPreferencesService, + ) { + super(); + + const workspaceConfig = workspaceContextService.getWorkspace().configuration; + const initialPaths: (IMcpConfigPath | undefined | null)[] = [ + { + id: 'usrlocal', + key: 'userLocalValue', + target: ConfigurationTarget.USER_LOCAL, + label: localize('mcp.configuration.userLocalValue', 'Global in {0}', productService.nameShort), + scope: StorageScope.PROFILE, + order: McpCollectionSortOrder.User, + uri: preferencesService.userSettingsResource, + section: [mcpConfigurationSection], + }, + workspaceConfig && { + id: 'workspace', + key: 'workspaceValue', + target: ConfigurationTarget.WORKSPACE, + label: basename(workspaceConfig), + scope: StorageScope.WORKSPACE, + order: McpCollectionSortOrder.Workspace, + remoteAuthority: _environmentService.remoteAuthority, + uri: workspaceConfig, + section: ['settings', mcpConfigurationSection], + }, + ...workspaceContextService.getWorkspace() + .folders + .map(wf => this._fromWorkspaceFolder(wf)) + ]; + + this._paths = observableValue('mcpConfigPaths', initialPaths.filter(isDefined)); + + remoteAgentService.getEnvironment().then((env) => { + const label = _environmentService.remoteAuthority ? labelService.getHostLabel(Schemas.vscodeRemote, _environmentService.remoteAuthority) : 'Remote'; + + this._paths.set([ + ...this.paths.get(), + { + id: 'usrremote', + key: 'userRemoteValue', + target: ConfigurationTarget.USER_REMOTE, + label, + scope: StorageScope.PROFILE, + order: McpCollectionSortOrder.User + McpCollectionSortOrder.RemoteBoost, + uri: env?.settingsPath, + remoteAuthority: _environmentService.remoteAuthority, + section: [mcpConfigurationSection], + } + ], undefined); + }); + + this._register(workspaceContextService.onDidChangeWorkspaceFolders(e => { + const next = this._paths.get().slice(); + for (const folder of e.added) { + next.push(this._fromWorkspaceFolder(folder)); + } + for (const folder of e.removed) { + const idx = next.findIndex(c => c.workspaceFolder === folder); + if (idx !== -1) { + next.splice(idx, 1); + } + } + this._paths.set(next, undefined); + })); + } + + private _fromWorkspaceFolder(workspaceFolder: IWorkspaceFolder): IMcpConfigPath { + return { + id: `wf${workspaceFolder.index}`, + key: 'workspaceFolderValue', + target: ConfigurationTarget.WORKSPACE_FOLDER, + label: `${workspaceFolder.name}/.vscode/mcp.json`, + scope: StorageScope.WORKSPACE, + remoteAuthority: this._environmentService.remoteAuthority, + order: McpCollectionSortOrder.WorkspaceFolder, + uri: URI.joinPath(workspaceFolder.uri, FOLDER_SETTINGS_PATH, '../mcp.json'), + workspaceFolder, + }; + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts new file mode 100644 index 00000000..cea67890 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpConfiguration.ts @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; +import { localize } from '../../../../nls.js'; +import { IMcpCollectionContribution } from '../../../../platform/extensions/common/extensions.js'; +import { mcpSchemaId } from '../../../services/configuration/common/configuration.js'; +import { inputsSchema } from '../../../services/configurationResolver/common/configurationResolverSchema.js'; +import { IExtensionPointDescriptor } from '../../../services/extensions/common/extensionsRegistry.js'; + +export type { McpConfigurationServer, IMcpConfigurationStdio, IMcpConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; + +const mcpActivationEventPrefix = 'onMcpCollection:'; + +export const mcpActivationEvent = (collectionId: string) => mcpActivationEventPrefix + collectionId; + +const mcpSchemaExampleServer = { + command: 'node', + args: ['my-mcp-server.js'], + env: {}, +}; + +export const enum DiscoverySource { + ClaudeDesktop = 'claude-desktop', + Windsurf = 'windsurf', + CursorGlobal = 'cursor-global', + CursorWorkspace = 'cursor-workspace', +} + +export const allDiscoverySources = Object.keys({ + [DiscoverySource.ClaudeDesktop]: true, + [DiscoverySource.Windsurf]: true, + [DiscoverySource.CursorGlobal]: true, + [DiscoverySource.CursorWorkspace]: true, +} satisfies Record) as DiscoverySource[]; + +export const discoverySourceLabel: Record = { + [DiscoverySource.ClaudeDesktop]: localize('mcp.discovery.source.claude-desktop', "Claude Desktop"), + [DiscoverySource.Windsurf]: localize('mcp.discovery.source.windsurf', "Windsurf"), + [DiscoverySource.CursorGlobal]: localize('mcp.discovery.source.cursor-global', "Cursor (Global)"), + [DiscoverySource.CursorWorkspace]: localize('mcp.discovery.source.cursor-workspace', "Cursor (Workspace)"), +}; + +export const mcpConfigurationSection = 'mcp'; +export const mcpDiscoverySection = 'chat.mcp.discovery.enabled'; +export const mcpEnabledSection = 'chat.mcp.enabled'; + +export const mcpSchemaExampleServers = { + 'mcp-server-time': { + command: 'python', + args: ['-m', 'mcp_server_time', '--local-timezone=America/Los_Angeles'], + env: {}, + } +}; + +export const mcpStdioServerSchema: IJSONSchema = { + type: 'object', + additionalProperties: false, + examples: [mcpSchemaExampleServer], + properties: { + type: { + type: 'string', + enum: ['stdio'], + description: localize('app.mcp.json.type', "The type of the server.") + }, + command: { + type: 'string', + description: localize('app.mcp.json.command', "The command to run the server.") + }, + args: { + type: 'array', + description: localize('app.mcp.args.command', "Arguments passed to the server."), + items: { + type: 'string' + }, + }, + envFile: { + type: 'string', + description: localize('app.mcp.envFile.command', "Path to a file containing environment variables for the server."), + examples: ['${workspaceFolder}/.env'], + }, + env: { + description: localize('app.mcp.env.command', "Environment variables passed to the server."), + additionalProperties: { + anyOf: [ + { type: 'null' }, + { type: 'string' }, + { type: 'number' }, + ] + } + }, + } +}; + +export const mcpServerSchema: IJSONSchema = { + id: mcpSchemaId, + type: 'object', + title: localize('app.mcp.json.title', "Model Context Protocol Servers"), + allowTrailingCommas: true, + allowComments: true, + additionalProperties: false, + properties: { + servers: { + examples: [mcpSchemaExampleServers], + additionalProperties: { + oneOf: [mcpStdioServerSchema, { + type: 'object', + additionalProperties: false, + required: ['url', 'type'], + examples: [{ + type: 'sse', + url: 'http://localhost:3001', + headers: {}, + }], + properties: { + type: { + type: 'string', + enum: ['sse'], + description: localize('app.mcp.json.type', "The type of the server.") + }, + url: { + type: 'string', + format: 'uri', + description: localize('app.mcp.json.url', "The URL of the server-sent-event (SSE) server.") + }, + env: { + description: localize('app.mcp.json.headers', "Additional headers sent to the server."), + additionalProperties: { type: 'string' }, + }, + } + }] + } + }, + inputs: inputsSchema.definitions!.inputs + } +}; + +export const mcpContributionPoint: IExtensionPointDescriptor = { + extensionPoint: 'modelContextServerCollections', + activationEventsGenerator(contribs, result) { + for (const contrib of contribs) { + if (contrib.id) { + result.push(mcpActivationEvent(contrib.id)); + } + } + }, + jsonSchema: { + description: localize('vscode.extension.contributes.mcp', 'Contributes Model Context Protocol servers. Users of this should also use `vscode.lm.registerMcpConfigurationProvider`.'), + type: 'array', + defaultSnippets: [{ body: [{ id: '', label: '' }] }], + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { id: '', label: '' } }], + properties: { + id: { + description: localize('vscode.extension.contributes.mcp.id', "Unique ID for the collection."), + type: 'string' + }, + label: { + description: localize('vscode.extension.contributes.mcp.label', "Display name for the collection."), + type: 'string' + } + } + } + } +}; diff --git a/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts b/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts new file mode 100644 index 00000000..0d0aff9a --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpContextKeys.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { localize } from '../../../../nls.js'; +import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { LazyCollectionState, IMcpService, McpServerToolsState, McpConnectionState } from './mcpTypes.js'; + + +export namespace McpContextKeys { + + export const serverCount = new RawContextKey('mcp.serverCount', undefined, { type: 'number', description: localize('mcp.serverCount.description', "Context key that has the number of registered MCP servers") }); + export const hasUnknownTools = new RawContextKey('mcp.hasUnknownTools', undefined, { type: 'boolean', description: localize('mcp.hasUnknownTools.description', "Indicates whether there are MCP servers with unknown tools.") }); + /** + * A context key that indicates whether there are any servers with errors. + * + * @type {boolean} + * @default undefined + * @description This key is used to track the presence of servers with errors in the MCP context. + */ + export const hasServersWithErrors = new RawContextKey('mcp.hasServersWithErrors', undefined, { type: 'boolean', description: localize('mcp.hasServersWithErrors.description', "Indicates whether there are any MCP servers with errors.") }); + export const toolsCount = new RawContextKey('mcp.toolsCount', undefined, { type: 'number', description: localize('mcp.toolsCount.description', "Context key that has the number of registered MCP tools") }); +} + + +export class McpContextKeysController extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.mcp.contextKey'; + + constructor( + @IMcpService mcpService: IMcpService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(); + + const ctxServerCount = McpContextKeys.serverCount.bindTo(contextKeyService); + const ctxToolsCount = McpContextKeys.toolsCount.bindTo(contextKeyService); + const ctxHasUnknownTools = McpContextKeys.hasUnknownTools.bindTo(contextKeyService); + + this._store.add(bindContextKey(McpContextKeys.hasServersWithErrors, contextKeyService, r => mcpService.servers.read(r).some(c => c.connectionState.read(r).state === McpConnectionState.Kind.Error))); + + this._store.add(autorun(r => { + const servers = mcpService.servers.read(r); + const serverTools = servers.map(s => s.tools.read(r)); + ctxServerCount.set(servers.length); + ctxToolsCount.set(serverTools.reduce((count, tools) => count + tools.length, 0)); + ctxHasUnknownTools.set(mcpService.lazyCollectionState.read(r) !== LazyCollectionState.AllKnown || servers.some(s => { + if (s.trusted.read(r) === false) { + return false; + } + + const toolState = s.toolsState.read(r); + return toolState === McpServerToolsState.Unknown || toolState === McpServerToolsState.RefreshingFromUnknown; + })); + })); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts new file mode 100644 index 00000000..f7577bcc --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -0,0 +1,372 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from '../../../../base/common/event.js'; +import { StringSHA1 } from '../../../../base/common/hash.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Lazy } from '../../../../base/common/lazy.js'; +import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { derived, IObservable, observableValue } from '../../../../base/common/observable.js'; +import { basename } from '../../../../base/common/resources.js'; +import { localize } from '../../../../nls.js'; +import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { observableMemento } from '../../../../platform/observable/common/observableMemento.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; +import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js'; +import { ConfigurationResolverExpression, IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { McpRegistryInputStorage } from './mcpRegistryInputStorage.js'; +import { IMcpHostDelegate, IMcpRegistry, IMcpResolveConnectionOptions } from './mcpRegistryTypes.js'; +import { McpServerConnection } from './mcpServerConnection.js'; +import { IMcpServerConnection, LazyCollectionState, McpCollectionDefinition, McpCollectionReference, McpServerDefinition, McpServerLaunch } from './mcpTypes.js'; + +const createTrustMemento = observableMemento>>({ + defaultValue: {}, + key: 'mcp.trustedCollections' +}); + +const collectionPrefixLen = 3; + +export class McpRegistry extends Disposable implements IMcpRegistry { + declare public readonly _serviceBrand: undefined; + + private readonly _trustPrompts = new Map>(); + + private readonly _collections = observableValue('collections', []); + private readonly _delegates: IMcpHostDelegate[] = []; + public readonly collections: IObservable = this._collections; + + private readonly _collectionToPrefixes = this._collections.map(c => { + // This creates tool prefixes based on a hash of the collection ID. This is + // a short prefix because tool names that are too long can cause errors (#243602). + // So we take a hash (in order for tools to be stable, because randomized + // names can cause hallicinations if present in history) and then adjust + // them if there are any collisions. + type CollectionHash = { view: number; hash: string; collection: McpCollectionDefinition }; + + const hashes = c.map((collection): CollectionHash => { + const sha = new StringSHA1(); + sha.update(collection.id); + return { view: 0, hash: sha.digest(), collection }; + }); + + const view = (h: CollectionHash) => h.hash.slice(h.view, h.view + collectionPrefixLen); + + let collided = false; + do { + hashes.sort((a, b) => view(a).localeCompare(view(b)) || a.collection.id.localeCompare(b.collection.id)); + collided = false; + for (let i = 1; i < hashes.length; i++) { + const prev = hashes[i - 1]; + const curr = hashes[i]; + if (view(prev) === view(curr) && curr.view + collectionPrefixLen < curr.hash.length) { + curr.view++; + collided = true; + } + } + } while (collided); + + return Object.fromEntries(hashes.map(h => [h.collection.id, view(h) + '.'])); + }); + + private readonly _workspaceStorage = new Lazy(() => this._register(this._instantiationService.createInstance(McpRegistryInputStorage, StorageScope.WORKSPACE, StorageTarget.USER))); + private readonly _profileStorage = new Lazy(() => this._register(this._instantiationService.createInstance(McpRegistryInputStorage, StorageScope.PROFILE, StorageTarget.USER))); + + private readonly _trustMemento = new Lazy(() => this._register(createTrustMemento(StorageScope.APPLICATION, StorageTarget.MACHINE, this._storageService))); + private readonly _lazyCollectionsToUpdate = new Set(); + private readonly _ongoingLazyActivations = observableValue(this, 0); + + public readonly lazyCollectionState = derived(reader => { + if (this._ongoingLazyActivations.read(reader) > 0) { + return LazyCollectionState.LoadingUnknown; + } + const collections = this._collections.read(reader); + return collections.some(c => c.lazy && c.lazy.isCached === false) ? LazyCollectionState.HasUnknown : LazyCollectionState.AllKnown; + }); + + public get delegates(): readonly IMcpHostDelegate[] { + return this._delegates; + } + + private readonly _onDidChangeInputs = this._register(new Emitter()); + public readonly onDidChangeInputs = this._onDidChangeInputs.event; + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, + @IDialogService private readonly _dialogService: IDialogService, + @IStorageService private readonly _storageService: IStorageService, + @IProductService private readonly _productService: IProductService, + @INotificationService private readonly _notificationService: INotificationService, + @IEditorService private readonly _editorService: IEditorService, + ) { + super(); + } + + public registerDelegate(delegate: IMcpHostDelegate): IDisposable { + this._delegates.push(delegate); + this._delegates.sort((a, b) => b.priority - a.priority); + + return { + dispose: () => { + const index = this._delegates.indexOf(delegate); + if (index !== -1) { + this._delegates.splice(index, 1); + } + } + }; + } + + public registerCollection(collection: McpCollectionDefinition): IDisposable { + const currentCollections = this._collections.get(); + const toReplace = currentCollections.find(c => c.lazy && c.id === collection.id); + + // Incoming collections replace the "lazy" versions. See `ExtensionMcpDiscovery` for an example. + if (toReplace) { + this._lazyCollectionsToUpdate.add(collection.id); + this._collections.set(currentCollections.map(c => c === toReplace ? collection : c), undefined); + } else { + this._collections.set([...currentCollections, collection], undefined); + } + + return { + dispose: () => { + const currentCollections = this._collections.get(); + this._collections.set(currentCollections.filter(c => c !== collection), undefined); + } + }; + } + + public collectionToolPrefix(collection: McpCollectionReference): IObservable { + return this._collectionToPrefixes.map(p => p[collection.id] ?? ''); + } + + public async discoverCollections(): Promise { + const toDiscover = this._collections.get().filter(c => c.lazy && !c.lazy.isCached); + + this._ongoingLazyActivations.set(this._ongoingLazyActivations.get() + 1, undefined); + await Promise.all(toDiscover.map(c => c.lazy?.load())).finally(() => { + this._ongoingLazyActivations.set(this._ongoingLazyActivations.get() - 1, undefined); + }); + + const found: McpCollectionDefinition[] = []; + const current = this._collections.get(); + for (const collection of toDiscover) { + const rec = current.find(c => c.id === collection.id); + if (!rec) { + // ignored + } else if (rec.lazy) { + rec.lazy.removed?.(); // did not get replaced by the non-lazy version + } else { + found.push(rec); + } + } + + + return found; + } + + private _getInputStorage(scope: StorageScope): McpRegistryInputStorage { + return scope === StorageScope.WORKSPACE ? this._workspaceStorage.value : this._profileStorage.value; + } + + private _getInputStorageInConfigTarget(configTarget: ConfigurationTarget): McpRegistryInputStorage { + return this._getInputStorage( + configTarget === ConfigurationTarget.WORKSPACE || configTarget === ConfigurationTarget.WORKSPACE_FOLDER + ? StorageScope.WORKSPACE + : StorageScope.PROFILE + ); + } + + public async clearSavedInputs(scope: StorageScope, inputId?: string) { + const storage = this._getInputStorage(scope); + if (inputId) { + await storage.clear(inputId); + } else { + storage.clearAll(); + } + + this._onDidChangeInputs.fire(); + } + + public async editSavedInput(inputId: string, folderData: IWorkspaceFolderData | undefined, configSection: string, target: ConfigurationTarget): Promise { + const storage = this._getInputStorageInConfigTarget(target); + const expr = ConfigurationResolverExpression.parse(inputId); + + const stored = await storage.getMap(); + const previous = stored[inputId].value; + await this._configurationResolverService.resolveWithInteraction(folderData, expr, configSection, previous ? { [inputId.slice(2, -1)]: previous } : {}, target); + await this._updateStorageWithExpressionInputs(storage, expr); + } + + public getSavedInputs(scope: StorageScope): Promise<{ [id: string]: IResolvedValue }> { + return this._getInputStorage(scope).getMap(); + } + + public resetTrust(): void { + this._trustMemento.value.set({}, undefined); + } + + public getTrust(collectionRef: McpCollectionReference): IObservable { + return derived(reader => { + const collection = this._collections.read(reader).find(c => c.id === collectionRef.id); + if (!collection || collection.isTrustedByDefault) { + return true; + } + + const memento = this._trustMemento.value.read(reader); + return memento.hasOwnProperty(collection.id) ? memento[collection.id] : undefined; + }); + } + + private _promptForTrust(collection: McpCollectionDefinition): Promise { + // Collect all trust prompts for a single config so that concurrently trying to start N + // servers in a config don't result in N different dialogs + let resultPromise = this._trustPrompts.get(collection.id); + resultPromise ??= this._promptForTrustOpenDialog(collection).finally(() => { + this._trustPrompts.delete(collection.id); + }); + this._trustPrompts.set(collection.id, resultPromise); + + return resultPromise; + } + + private async _promptForTrustOpenDialog(collection: McpCollectionDefinition): Promise { + const originURI = collection.presentation?.origin; + const labelWithOrigin = originURI ? `[\`${basename(originURI)}\`](${originURI})` : collection.label; + + const result = await this._dialogService.prompt( + { + message: localize('trustTitleWithOrigin', 'Trust MCP servers from {0}?', collection.label), + custom: { + markdownDetails: [{ + markdown: new MarkdownString(localize('mcp.trust.details', '{0} discovered Model Context Protocol servers from {1} (`{2}`). {0} can use their capabilities in Chat.\n\nDo you want to allow running MCP servers from {3}?', this._productService.nameShort, collection.label, collection.serverDefinitions.get().map(s => s.label).join('`, `'), labelWithOrigin)), + dismissOnLinkClick: true, + }] + }, + buttons: [ + { label: localize('mcp.trust.yes', 'Trust'), run: () => true }, + { label: localize('mcp.trust.no', 'Do not trust'), run: () => false } + ], + }, + ); + + return result.result; + } + + private async _updateStorageWithExpressionInputs(inputStorage: McpRegistryInputStorage, expr: ConfigurationResolverExpression): Promise { + const secrets: Record = {}; + const inputs: Record = {}; + for (const [replacement, resolved] of expr.resolved()) { + if (resolved.input?.type === 'promptString' && resolved.input.password) { + secrets[replacement.id] = resolved; + } else { + inputs[replacement.id] = resolved; + } + } + + inputStorage.setPlainText(inputs); + await inputStorage.setSecrets(secrets); + this._onDidChangeInputs.fire(); + } + + private async _replaceVariablesInLaunch(definition: McpServerDefinition, launch: McpServerLaunch) { + if (!definition.variableReplacement) { + return launch; + } + + const { section, target, folder } = definition.variableReplacement; + const inputStorage = this._getInputStorageInConfigTarget(target); + const previouslyStored = await inputStorage.getMap(); + + // pre-fill the variables we already resolved to avoid extra prompting + const expr = ConfigurationResolverExpression.parse(launch); + for (const replacement of expr.unresolved()) { + if (previouslyStored.hasOwnProperty(replacement.id)) { + expr.resolve(replacement, previouslyStored[replacement.id]); + } + } + + // resolve variables requiring user input + await this._configurationResolverService.resolveWithInteraction(folder, expr, section, undefined, target); + + await this._updateStorageWithExpressionInputs(inputStorage, expr); + + // resolve other non-interactive variables, returning the final object + return await this._configurationResolverService.resolveAsync(folder, expr); + } + + public async resolveConnection({ collectionRef, definitionRef, forceTrust, logger }: IMcpResolveConnectionOptions): Promise { + const collection = this._collections.get().find(c => c.id === collectionRef.id); + const definition = collection?.serverDefinitions.get().find(s => s.id === definitionRef.id); + if (!collection || !definition) { + throw new Error(`Collection or definition not found for ${collectionRef.id} and ${definitionRef.id}`); + } + + const delegate = this._delegates.find(d => d.canStart(collection, definition)); + if (!delegate) { + throw new Error('No delegate found that can handle the connection'); + } + + if (!collection.isTrustedByDefault) { + const memento = this._trustMemento.value.get(); + const trusted = memento.hasOwnProperty(collection.id) ? memento[collection.id] : undefined; + + if (trusted) { + // continue + } else if (trusted === undefined || forceTrust) { + const trustValue = await this._promptForTrust(collection); + if (trustValue !== undefined) { + this._trustMemento.value.set({ ...memento, [collection.id]: trustValue }, undefined); + } + if (!trustValue) { + return; + } + } else /** trusted === false && !forceTrust */ { + return undefined; + } + } + + let launch: McpServerLaunch | undefined; + try { + launch = await this._replaceVariablesInLaunch(definition, definition.launch); + } catch (e) { + this._notificationService.notify({ + severity: Severity.Error, + message: localize('mcp.launchError', 'Error starting {0}: {1}', definition.label, String(e)), + actions: { + primary: collection.presentation?.origin && [ + { + id: 'mcp.launchError.openConfig', + class: undefined, + enabled: true, + tooltip: '', + label: localize('mcp.launchError.openConfig', 'Open Configuration'), + run: () => this._editorService.openEditor({ + resource: collection.presentation!.origin, + options: { selection: definition.presentation?.origin?.range } + }), + } + ] + } + }); + return; + } + + return this._instantiationService.createInstance( + McpServerConnection, + collection, + definition, + delegate, + launch, + logger, + ); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts new file mode 100644 index 00000000..852c34a2 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistryInputStorage.ts @@ -0,0 +1,181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Sequencer } from '../../../../base/common/async.js'; +import { decodeBase64, encodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; +import { Lazy } from '../../../../base/common/lazy.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { isEmptyObject } from '../../../../base/common/types.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; + +const MCP_ENCRYPTION_KEY_NAME = 'mcpEncryptionKey'; +const MCP_ENCRYPTION_KEY_ALGORITHM = 'AES-GCM'; +const MCP_ENCRYPTION_KEY_LEN = 256; +const MCP_ENCRYPTION_IV_LENGTH = 12; // 96 bits +const MCP_DATA_STORED_VERSION = 1; +const MCP_DATA_STORED_KEY = 'mcpInputs'; + +interface IStoredData { + version: number; + values: Record; + secrets?: { value: string; iv: string }; // base64, encrypted +} + +interface IHydratedData extends IStoredData { + unsealedSecrets?: Record; +} + +export class McpRegistryInputStorage extends Disposable { + private static secretSequencer = new Sequencer(); + private readonly _secretsSealerSequencer = new Sequencer(); + + private readonly _getEncryptionKey = new Lazy(() => { + return McpRegistryInputStorage.secretSequencer.queue(async () => { + const existing = await this._secretStorageService.get(MCP_ENCRYPTION_KEY_NAME); + if (existing) { + try { + const parsed: JsonWebKey = JSON.parse(existing); + return await crypto.subtle.importKey('jwk', parsed, MCP_ENCRYPTION_KEY_ALGORITHM, false, ['encrypt', 'decrypt']); + } catch { + // fall through + } + } + + const key = await crypto.subtle.generateKey( + { name: MCP_ENCRYPTION_KEY_ALGORITHM, length: MCP_ENCRYPTION_KEY_LEN }, + true, + ['encrypt', 'decrypt'], + ); + + const exported = await crypto.subtle.exportKey('jwk', key); + await this._secretStorageService.set(MCP_ENCRYPTION_KEY_NAME, JSON.stringify(exported)); + return key; + }); + }); + + private _didChange = false; + + private _record = new Lazy(() => { + const stored = this._storageService.getObject(MCP_DATA_STORED_KEY, this._scope); + return stored?.version === MCP_DATA_STORED_VERSION ? { ...stored } : { version: MCP_DATA_STORED_VERSION, values: {} }; + }); + + + constructor( + private readonly _scope: StorageScope, + _target: StorageTarget, + @IStorageService private readonly _storageService: IStorageService, + @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + + this._register(_storageService.onWillSaveState(() => { + if (this._didChange) { + this._storageService.store(MCP_DATA_STORED_KEY, { + version: MCP_DATA_STORED_VERSION, + values: this._record.value.values, + secrets: this._record.value.secrets, + } satisfies IStoredData, this._scope, _target); + this._didChange = false; + } + })); + } + + /** Deletes all collection data from storage. */ + public clearAll() { + this._record.value.values = {}; + this._record.value.secrets = undefined; + this._record.value.unsealedSecrets = undefined; + this._didChange = true; + } + + /** Delete a single collection data from the storage. */ + public async clear(inputKey: string) { + const secrets = await this._unsealSecrets(); + delete this._record.value.values[inputKey]; + this._didChange = true; + + if (secrets.hasOwnProperty(inputKey)) { + delete secrets[inputKey]; + await this._sealSecrets(); + } + } + + /** Gets a mapping of saved input data. */ + public async getMap() { + const secrets = await this._unsealSecrets(); + return { ...this._record.value.values, ...secrets }; + } + + /** Updates the input data mapping. */ + public async setPlainText(values: Record) { + Object.assign(this._record.value.values, values); + this._didChange = true; + } + + /** Updates the input secrets mapping. */ + public async setSecrets(values: Record) { + const unsealed = await this._unsealSecrets(); + Object.assign(unsealed, values); + await this._sealSecrets(); + } + + private async _sealSecrets() { + const key = await this._getEncryptionKey.value; + return this._secretsSealerSequencer.queue(async () => { + if (!this._record.value.unsealedSecrets || isEmptyObject(this._record.value.unsealedSecrets)) { + this._record.value.secrets = undefined; + return; + } + + const toSeal = JSON.stringify(this._record.value.unsealedSecrets); + const iv = crypto.getRandomValues(new Uint8Array(MCP_ENCRYPTION_IV_LENGTH)); + const encrypted = await crypto.subtle.encrypt( + { name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer }, + key, + new TextEncoder().encode(toSeal).buffer, + ); + + const enc = encodeBase64(VSBuffer.wrap(new Uint8Array(encrypted))); + this._record.value.secrets = { iv: encodeBase64(VSBuffer.wrap(iv)), value: enc }; + this._didChange = true; + }); + } + + private async _unsealSecrets(): Promise> { + if (!this._record.value.secrets) { + return this._record.value.unsealedSecrets ??= {}; + } + + if (this._record.value.unsealedSecrets) { + return this._record.value.unsealedSecrets; + } + + try { + const key = await this._getEncryptionKey.value; + const iv = decodeBase64(this._record.value.secrets.iv); + const encrypted = decodeBase64(this._record.value.secrets.value); + + const decrypted = await crypto.subtle.decrypt( + { name: MCP_ENCRYPTION_KEY_ALGORITHM, iv: iv.buffer }, + key, + encrypted.buffer, + ); + + const unsealedSecrets = JSON.parse(new TextDecoder().decode(decrypted)); + this._record.value.unsealedSecrets = unsealedSecrets; + return unsealedSecrets; + } catch (e) { + this._logService.warn('Error unsealing MCP secrets', e); + this._record.value.secrets = undefined; + } + + return {}; + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts new file mode 100644 index 00000000..07de8bc1 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../base/common/observable.js'; +import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogger, LogLevel } from '../../../../platform/log/common/log.js'; +import { StorageScope } from '../../../../platform/storage/common/storage.js'; +import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; +import { IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; +import { IMcpServerConnection, LazyCollectionState, McpCollectionDefinition, McpCollectionReference, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerLaunch } from './mcpTypes.js'; +import { MCP } from './modelContextProtocol.js'; + +export const IMcpRegistry = createDecorator('mcpRegistry'); + +/** Message transport to a single MCP server. */ +export interface IMcpMessageTransport extends IDisposable { + readonly state: IObservable; + readonly onDidLog: Event<{ level: LogLevel; message: string }>; + readonly onDidReceiveMessage: Event; + send(message: MCP.JSONRPCMessage): void; + stop(): void; +} + +export interface IMcpHostDelegate { + /** Priority for this delegate, delegates are tested in descending priority order */ + readonly priority: number; + waitForInitialProviderPromises(): Promise; + canStart(collectionDefinition: McpCollectionDefinition, serverDefinition: McpServerDefinition): boolean; + start(collectionDefinition: McpCollectionDefinition, serverDefinition: McpServerDefinition, resolvedLaunch: McpServerLaunch): IMcpMessageTransport; +} + +export interface IMcpResolveConnectionOptions { + logger: ILogger; + collectionRef: McpCollectionReference; + definitionRef: McpDefinitionReference; + /** If set, the user will be asked to trust the collection even if they untrusted it previously */ + forceTrust?: boolean; +} + +export interface IMcpRegistry { + readonly _serviceBrand: undefined; + + /** Fired when the user provides more inputs when creating a connection. */ + readonly onDidChangeInputs: Event; + + readonly collections: IObservable; + readonly delegates: readonly IMcpHostDelegate[]; + /** Whether there are new collections that can be resolved with a discover() call */ + readonly lazyCollectionState: IObservable; + + /** Gets the prefix that should be applied to a collection's tools in order to avoid ID conflicts */ + collectionToolPrefix(collection: McpCollectionReference): IObservable; + + /** Discover new collections, returning newly-discovered ones. */ + discoverCollections(): Promise; + + registerDelegate(delegate: IMcpHostDelegate): IDisposable; + registerCollection(collection: McpCollectionDefinition): IDisposable; + + /** Resets the trust state of all collections. */ + resetTrust(): void; + + /** Gets whether the collection is trusted. */ + getTrust(collection: McpCollectionReference): IObservable; + + /** Resets any saved inputs for the input, or globally. */ + clearSavedInputs(scope: StorageScope, inputId?: string): Promise; + /** Edits a previously-saved input. */ + editSavedInput(inputId: string, folderData: IWorkspaceFolderData | undefined, configSection: string, target: ConfigurationTarget): Promise; + /** Gets saved inputs from storage. */ + getSavedInputs(scope: StorageScope): Promise<{ [id: string]: IResolvedValue }>; + /** Creates a connection for the collection and definition. */ + resolveConnection(options: IMcpResolveConnectionOptions): Promise; +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts new file mode 100644 index 00000000..e565f38c --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -0,0 +1,512 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { raceCancellationError, Sequencer } from '../../../../base/common/async.js'; +import * as json from '../../../../base/common/json.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { LRUCache } from '../../../../base/common/map.js'; +import { autorun, autorunWithStore, derived, disposableObservableValue, IObservable, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js'; +import { basename } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { IOutputService } from '../../../services/output/common/output.js'; +import { mcpActivationEvent } from './mcpConfiguration.js'; +import { IMcpRegistry } from './mcpRegistryTypes.js'; +import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; +import { extensionMcpCollectionPrefix, IMcpServer, IMcpServerConnection, IMcpTool, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; +import { MCP } from './modelContextProtocol.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { localize } from '../../../../nls.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; + +type ServerBootData = { + supportsLogging: boolean; + supportsPrompts: boolean; + supportsResources: boolean; + toolCount: number; +}; +type ServerBootClassification = { + owner: 'connor4312'; + comment: 'Details the capabilities of the MCP server'; + supportsLogging: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the server supports logging' }; + supportsPrompts: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the server supports prompts' }; + supportsResources: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the server supports resource' }; + toolCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of tools the server advertises' }; +}; + +type ServerBootState = { + state: string; + time: number; +}; +type ServerBootStateClassification = { + owner: 'connor4312'; + comment: 'Details the capabilities of the MCP server'; + state: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The server outcome' }; + time: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Duration in milliseconds to reach that state' }; +}; + +interface IToolCacheEntry { + /** Cached tools so we can show what's available before it's started */ + readonly tools: readonly IValidatedMcpTool[]; +} + +interface IServerCacheEntry { + readonly servers: readonly McpServerDefinition.Serialized[]; +} + +const toolInvalidCharRe = /[^a-z0-9_-]/gi; + +export class McpServerMetadataCache extends Disposable { + private didChange = false; + private readonly cache = new LRUCache(128); + private readonly extensionServers = new Map(); + + constructor( + scope: StorageScope, + @IStorageService storageService: IStorageService, + ) { + super(); + + type StoredType = { + extensionServers: [string, IServerCacheEntry][]; + serverTools: [string, IToolCacheEntry][]; + }; + + const storageKey = 'mcpToolCache'; + this._register(storageService.onWillSaveState(() => { + if (this.didChange) { + storageService.store(storageKey, { + extensionServers: [...this.extensionServers], + serverTools: this.cache.toJSON(), + } satisfies StoredType, scope, StorageTarget.MACHINE); + this.didChange = false; + } + })); + + try { + const cached: StoredType | undefined = storageService.getObject(storageKey, scope); + this.extensionServers = new Map(cached?.extensionServers ?? []); + cached?.serverTools?.forEach(([k, v]) => this.cache.set(k, v)); + } catch { + // ignored + } + } + + /** Resets the cache for tools and extension servers */ + reset() { + this.cache.clear(); + this.extensionServers.clear(); + this.didChange = true; + } + + /** Gets cached tools for a server (used before a server is running) */ + getTools(definitionId: string): readonly IValidatedMcpTool[] | undefined { + return this.cache.get(definitionId)?.tools; + } + + /** Sets cached tools for a server */ + storeTools(definitionId: string, tools: readonly IValidatedMcpTool[]): void { + this.cache.set(definitionId, { ...this.cache.get(definitionId), tools }); + this.didChange = true; + } + + /** Gets cached servers for a collection (used for extensions, before the extension activates) */ + getServers(collectionId: string) { + return this.extensionServers.get(collectionId); + } + + /** Sets cached servers for a collection */ + storeServers(collectionId: string, entry: IServerCacheEntry | undefined): void { + if (entry) { + this.extensionServers.set(collectionId, entry); + } else { + this.extensionServers.delete(collectionId); + } + this.didChange = true; + } +} + +interface IValidatedMcpTool extends MCP.Tool { + /** + * Tool name as published by the MCP server. This may + * be different than the one in {@link definition} due to name normalization + * in {@link McpServer._getValidatedTools}. + */ + serverToolName: string; +} + +export class McpServer extends Disposable implements IMcpServer { + private readonly _connectionSequencer = new Sequencer(); + private readonly _connection = this._register(disposableObservableValue(this, undefined)); + + public readonly connection = this._connection; + public readonly connectionState: IObservable = derived(reader => this._connection.read(reader)?.state.read(reader) ?? { state: McpConnectionState.Kind.Stopped }); + + private get toolsFromCache() { + return this._toolCache.getTools(this.definition.id); + } + private readonly toolsFromServerPromise = observableValue | undefined>(this, undefined); + private readonly toolsFromServer = derived(reader => this.toolsFromServerPromise.read(reader)?.promiseResult.read(reader)?.data); + + public readonly tools: IObservable; + + public readonly toolsState = derived(reader => { + const fromServer = this.toolsFromServerPromise.read(reader); + const connectionState = this.connectionState.read(reader); + const isIdle = McpConnectionState.canBeStarted(connectionState.state) && !fromServer; + if (isIdle) { + return this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown; + } + + const fromServerResult = fromServer?.promiseResult.read(reader); + if (!fromServerResult) { + return this.toolsFromCache ? McpServerToolsState.RefreshingFromCached : McpServerToolsState.RefreshingFromUnknown; + } + + return fromServerResult.error ? (this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown) : McpServerToolsState.Live; + }); + + private readonly _loggerId: string; + private readonly _logger: ILogger; + + public get trusted() { + return this._mcpRegistry.getTrust(this.collection); + } + + constructor( + public readonly collection: McpCollectionReference, + public readonly definition: McpDefinitionReference, + explicitRoots: URI[] | undefined, + private readonly _requiresExtensionActivation: boolean | undefined, + private readonly _toolCache: McpServerMetadataCache, + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @IWorkspaceContextService workspacesService: IWorkspaceContextService, + @IExtensionService private readonly _extensionService: IExtensionService, + @ILoggerService private readonly _loggerService: ILoggerService, + @IOutputService private readonly _outputService: IOutputService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ICommandService private readonly _commandService: ICommandService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this._loggerId = `mcpServer.${definition.id}`; + this._logger = this._register(_loggerService.createLogger(this._loggerId, { hidden: true, name: `MCP: ${definition.label}` })); + // If the logger is disposed but not deregistered, then the disposed instance + // is reused and no-ops. todo@sandy081 this seems like a bug. + this._register(toDisposable(() => _loggerService.deregisterLogger(this._loggerId))); + + // 1. Reflect workspaces into the MCP roots + const workspaces = explicitRoots + ? observableValue(this, explicitRoots.map(uri => ({ uri, name: basename(uri) }))) + : observableFromEvent( + this, + workspacesService.onDidChangeWorkspaceFolders, + () => workspacesService.getWorkspace().folders, + ); + + this._register(autorunWithStore(reader => { + const cnx = this._connection.read(reader)?.handler.read(reader); + if (!cnx) { + return; + } + + cnx.roots = workspaces.read(reader).map(wf => ({ + uri: wf.uri.toString(), + name: wf.name, + })); + })); + + // 2. Populate this.tools when we connect to a server. + this._register(autorunWithStore((reader, store) => { + const cnx = this._connection.read(reader)?.handler.read(reader); + if (cnx) { + this.populateLiveData(cnx, store); + } else { + this.resetLiveData(); + } + })); + + // 3. Update the cache when tools update + this._register(autorun(reader => { + const tools = this.toolsFromServer.read(reader); + if (tools) { + this._toolCache.storeTools(definition.id, tools); + } + })); + + // 4. Publish tools + const toolPrefix = this._mcpRegistry.collectionToolPrefix(this.collection); + this.tools = derived(reader => { + const serverTools = this.toolsFromServer.read(reader); + const definitions = serverTools ?? this.toolsFromCache ?? []; + const prefix = toolPrefix.read(reader); + return definitions.map(def => new McpTool(this, prefix, def)); + }); + } + + public showOutput(): void { + this._loggerService.setVisibility(this._loggerId, true); + this._outputService.showChannel(this._loggerId); + } + + public start(isFromInteraction?: boolean): Promise { + return this._connectionSequencer.queue(async () => { + const activationEvent = mcpActivationEvent(this.collection.id.slice(extensionMcpCollectionPrefix.length)); + if (this._requiresExtensionActivation && !this._extensionService.activationEventIsDone(activationEvent)) { + await this._extensionService.activateByEvent(activationEvent); + await Promise.all(this._mcpRegistry.delegates + .map(r => r.waitForInitialProviderPromises())); + // This can happen if the server was created from a cached MCP server seen + // from an extension, but then it wasn't registered when the extension activated. + if (this._store.isDisposed) { + return { state: McpConnectionState.Kind.Stopped }; + } + } + + let connection = this._connection.get(); + if (connection && McpConnectionState.canBeStarted(connection.state.get().state)) { + connection.dispose(); + connection = undefined; + this._connection.set(connection, undefined); + } + + if (!connection) { + connection = await this._mcpRegistry.resolveConnection({ + logger: this._logger, + collectionRef: this.collection, + definitionRef: this.definition, + forceTrust: isFromInteraction, + }); + if (!connection) { + return { state: McpConnectionState.Kind.Stopped }; + } + + if (this._store.isDisposed) { + connection.dispose(); + return { state: McpConnectionState.Kind.Stopped }; + } + + this._connection.set(connection, undefined); + } + + const start = Date.now(); + const state = await connection.start(); + this._telemetryService.publicLog2('mcp/serverBootState', { + state: McpConnectionState.toKindString(state.state), + time: Date.now() - start, + }); + + return state; + }); + } + + public stop(): Promise { + return this._connection.get()?.stop() || Promise.resolve(); + } + + private resetLiveData() { + transaction(tx => { + this.toolsFromServerPromise.set(undefined, tx); + }); + } + + private async _normalizeTool(originalTool: MCP.Tool): Promise { + const tool: IValidatedMcpTool = { ...originalTool, serverToolName: originalTool.name }; + if (!tool.description) { + // Ensure a description is provided for each tool, #243919 + this._logger.warn(`Tool ${tool.name} does not have a description. Tools must be accurately described to be called`); + tool.description = ''; + } + + if (toolInvalidCharRe.test(tool.name)) { + this._logger.warn(`Tool ${JSON.stringify(tool.name)} is invalid. Tools names may only contain [a-z0-9_-]`); + tool.name = tool.name.replace(toolInvalidCharRe, '_'); + } + + type JsonDiagnostic = { message: string; range: { line: number; character: number }[] }; + + let diagnostics: JsonDiagnostic[] = []; + const toolJson = JSON.stringify(tool.inputSchema); + try { + const schemaUri = URI.parse('https://json-schema.org/draft-07/schema'); + diagnostics = await this._commandService.executeCommand('json.validate', schemaUri, toolJson) || []; + } catch (e) { + // ignored (error in json extension?); + } + + if (!diagnostics.length) { + return tool; + } + + // because it's all one line from JSON.stringify, we can treat characters as offsets. + const tree = json.parseTree(toolJson); + const messages = diagnostics.map(d => { + const node = json.findNodeAtOffset(tree, d.range[0].character); + const path = node && `/${json.getNodePath(node).join('/')}`; + return d.message + (path ? ` (at ${path})` : ''); + }); + + return { error: messages }; + } + + private async _getValidatedTools(handler: McpServerRequestHandler, tools: MCP.Tool[]): Promise { + let error = ''; + + const validations = await Promise.all(tools.map(t => this._normalizeTool(t))); + const validated: IValidatedMcpTool[] = []; + for (const [i, result] of validations.entries()) { + if ('error' in result) { + error += localize('mcpBadSchema.tool', 'Tool `{0}` has invalid JSON parameters:', tools[i].name) + '\n'; + for (const message of result.error) { + error += `\t- ${message}\n`; + } + error += `\t- Schema: ${JSON.stringify(tools[i].inputSchema)}\n\n`; + } else { + validated.push(result); + } + } + + if (error) { + handler.logger.warn(`${tools.length - validated.length} tools have invalid JSON schemas and will be omitted`); + warnInvalidTools(this._instantiationService, this.definition.label, error); + } + + return validated; + } + + private populateLiveData(handler: McpServerRequestHandler, store: DisposableStore) { + const cts = new CancellationTokenSource(); + store.add(toDisposable(() => cts.dispose(true))); + + // todo: add more than just tools here + + const updateTools = (tx: ITransaction | undefined) => { + const toolPromise = handler.capabilities.tools ? handler.listTools({}, cts.token) : Promise.resolve([]); + const toolPromiseSafe = toolPromise.then(async tools => { + handler.logger.info(`Discovered ${tools.length} tools`); + return this._getValidatedTools(handler, tools); + }); + this.toolsFromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx); + + return [toolPromise]; + }; + + store.add(handler.onDidChangeToolList(() => { + handler.logger.info('Tool list changed, refreshing tools...'); + updateTools(undefined); + })); + + let promises: ReturnType; + transaction(tx => { + promises = updateTools(tx); + }); + + Promise.all(promises!).then(([tools]) => { + this._telemetryService.publicLog2('mcp/serverBoot', { + supportsLogging: !!handler.capabilities.logging, + supportsPrompts: !!handler.capabilities.prompts, + supportsResources: !!handler.capabilities.resources, + toolCount: tools.length, + }); + }); + } + + /** + * Helper function to call the function on the handler once it's online. The + * connection started if it is not already. + */ + public async callOn(fn: (handler: McpServerRequestHandler) => Promise, token: CancellationToken = CancellationToken.None): Promise { + + await this.start(); // idempotent + + let ranOnce = false; + let d: IDisposable; + + const callPromise = new Promise((resolve, reject) => { + + d = autorun(reader => { + const connection = this._connection.read(reader); + if (!connection || ranOnce) { + return; + } + + const handler = connection.handler.read(reader); + if (!handler) { + const state = connection.state.read(reader); + if (state.state === McpConnectionState.Kind.Error) { + reject(new McpConnectionFailedError(`MCP server could not be started: ${state.message}`)); + return; + } else if (state.state === McpConnectionState.Kind.Stopped) { + reject(new McpConnectionFailedError('MCP server has stopped')); + return; + } else { + // keep waiting for handler + return; + } + } + + resolve(fn(handler)); + ranOnce = true; // aggressive prevent multiple racey calls, don't dispose because autorun is sync + }); + }); + + return raceCancellationError(callPromise, token).finally(() => d.dispose()); + } +} + +export class McpTool implements IMcpTool { + + readonly id: string; + + public get definition(): MCP.Tool { return this._definition; } + + constructor( + private readonly _server: McpServer, + idPrefix: string, + private readonly _definition: IValidatedMcpTool, + ) { + this.id = (idPrefix + _definition.name).replaceAll('.', '_'); + } + + call(params: Record, token?: CancellationToken): Promise { + // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it. + const name = this._definition.serverToolName ?? this._definition.name; + return this._server.callOn(h => h.callTool({ name, arguments: params }), token); + } +} + +function warnInvalidTools(instaService: IInstantiationService, serverName: string, errorText: string) { + instaService.invokeFunction((accessor) => { + const notificationService = accessor.get(INotificationService); + const editorService = accessor.get(IEditorService); + notificationService.notify({ + severity: Severity.Warning, + message: localize('mcpBadSchema', 'MCP server `{0}` has tools with invalid parameters which will be omitted.', serverName), + actions: { + primary: [{ + class: undefined, + enabled: true, + id: 'mcpBadSchema.show', + tooltip: '', + label: localize('mcpBadSchema.show', 'Show'), + run: () => { + editorService.openEditor({ + resource: undefined, + contents: errorText, + }); + } + }] + } + }); + }); +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts new file mode 100644 index 00000000..f367162f --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { Disposable, DisposableStore, IReference, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js'; +import { localize } from '../../../../nls.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogger, log } from '../../../../platform/log/common/log.js'; +import { IMcpHostDelegate, IMcpMessageTransport } from './mcpRegistryTypes.js'; +import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; +import { IMcpServerConnection, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from './mcpTypes.js'; + +export class McpServerConnection extends Disposable implements IMcpServerConnection { + private readonly _launch = this._register(new MutableDisposable>()); + private readonly _state = observableValue('mcpServerState', { state: McpConnectionState.Kind.Stopped }); + private readonly _requestHandler = observableValue('mcpServerRequestHandler', undefined); + + public readonly state: IObservable = this._state; + public readonly handler: IObservable = this._requestHandler; + + constructor( + private readonly _collection: McpCollectionDefinition, + public readonly definition: McpServerDefinition, + private readonly _delegate: IMcpHostDelegate, + public readonly launchDefinition: McpServerLaunch, + private readonly _logger: ILogger, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + } + + /** @inheritdoc */ + public async start(): Promise { + const currentState = this._state.get(); + if (!McpConnectionState.canBeStarted(currentState.state)) { + return this._waitForState(McpConnectionState.Kind.Running, McpConnectionState.Kind.Error); + } + + this._launch.value = undefined; + this._state.set({ state: McpConnectionState.Kind.Starting }, undefined); + this._logger.info(localize('mcpServer.starting', 'Starting server {0}', this.definition.label)); + + try { + const launch = this._delegate.start(this._collection, this.definition, this.launchDefinition); + this._launch.value = this.adoptLaunch(launch); + return this._waitForState(McpConnectionState.Kind.Running, McpConnectionState.Kind.Error); + } catch (e) { + const errorState: McpConnectionState = { + state: McpConnectionState.Kind.Error, + message: e instanceof Error ? e.message : String(e) + }; + this._state.set(errorState, undefined); + return errorState; + } + } + + private adoptLaunch(launch: IMcpMessageTransport): IReference { + const store = new DisposableStore(); + const cts = new CancellationTokenSource(); + + store.add(toDisposable(() => cts.dispose(true))); + store.add(launch); + store.add(launch.onDidLog(({ level, message }) => { + log(this._logger, level, message); + })); + + let didStart = false; + store.add(autorun(reader => { + const state = launch.state.read(reader); + this._state.set(state, undefined); + this._logger.info(localize('mcpServer.state', 'Connection state: {0}', McpConnectionState.toString(state))); + + if (state.state === McpConnectionState.Kind.Running && !didStart) { + didStart = true; + McpServerRequestHandler.create(this._instantiationService, launch, this._logger, cts.token).then( + handler => { + if (!store.isDisposed) { + this._requestHandler.set(handler, undefined); + } else { + handler.dispose(); + } + }, + err => { + store.dispose(); + if (!store.isDisposed) { + this._logger.error(err); + this._state.set({ state: McpConnectionState.Kind.Error, message: `Could not initialize MCP server: ${err.message}` }, undefined); + } + }, + ); + } + })); + + return { dispose: () => store.dispose(), object: launch }; + } + + public async stop(): Promise { + this._logger.info(localize('mcpServer.stopping', 'Stopping server {0}', this.definition.label)); + this._launch.value?.object.stop(); + await this._waitForState(McpConnectionState.Kind.Stopped, McpConnectionState.Kind.Error); + } + + public override dispose(): void { + this._requestHandler.get()?.dispose(); + super.dispose(); + this._state.set({ state: McpConnectionState.Kind.Stopped }, undefined); + } + + private _waitForState(...kinds: McpConnectionState.Kind[]): Promise { + const current = this._state.get(); + if (kinds.includes(current.state)) { + return Promise.resolve(current); + } + + return new Promise(resolve => { + const disposable = autorun(reader => { + const state = this._state.read(reader); + if (kinds.includes(state.state)) { + disposable.dispose(); + resolve(state); + } + }); + }); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts new file mode 100644 index 00000000..8c0fa08c --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts @@ -0,0 +1,486 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equals } from '../../../../base/common/arrays.js'; +import { DeferredPromise, IntervalTimer } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { CancellationError } from '../../../../base/common/errors.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { canLog, ILogger, LogLevel } from '../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IMcpMessageTransport } from './mcpRegistryTypes.js'; +import { McpConnectionState, MpcResponseError } from './mcpTypes.js'; +import { MCP } from './modelContextProtocol.js'; + +/** + * Maps request IDs to handlers. + */ +interface PendingRequest { + promise: DeferredPromise; +} + +export interface McpRoot { + uri: string; + name?: string; +} + +/** + * Request handler for communicating with an MCP server. + * + * Handles sending requests and receiving responses, with automatic + * handling of ping requests and typed client request methods. + */ +export class McpServerRequestHandler extends Disposable { + private _nextRequestId = 1; + private readonly _pendingRequests = new Map(); + + private _hasAnnouncedRoots = false; + private _roots: MCP.Root[] = []; + + public set roots(roots: MCP.Root[]) { + if (!equals(this._roots, roots)) { + this._roots = roots; + if (this._hasAnnouncedRoots) { + this.sendNotification({ method: 'notifications/roots/list_changed' }); + this._hasAnnouncedRoots = false; + } + } + } + + private _serverInit!: MCP.InitializeResult; + public get capabilities(): MCP.ServerCapabilities { + return this._serverInit.capabilities; + } + + // Event emitters for server notifications + private readonly _onDidReceiveCancelledNotification = this._register(new Emitter()); + readonly onDidReceiveCancelledNotification = this._onDidReceiveCancelledNotification.event; + + private readonly _onDidReceiveProgressNotification = this._register(new Emitter()); + readonly onDidReceiveProgressNotification = this._onDidReceiveProgressNotification.event; + + private readonly _onDidChangeResourceList = this._register(new Emitter()); + readonly onDidChangeResourceList = this._onDidChangeResourceList.event; + + private readonly _onDidUpdateResource = this._register(new Emitter()); + readonly onDidUpdateResource = this._onDidUpdateResource.event; + + private readonly _onDidChangeToolList = this._register(new Emitter()); + readonly onDidChangeToolList = this._onDidChangeToolList.event; + + private readonly _onDidChangePromptList = this._register(new Emitter()); + readonly onDidChangePromptList = this._onDidChangePromptList.event; + + /** + * Connects to the MCP server and does the initialization handshake. + * @throws MpcResponseError if the server fails to initialize. + */ + public static async create(instaService: IInstantiationService, launch: IMcpMessageTransport, logger: ILogger, token?: CancellationToken) { + const mcp = new McpServerRequestHandler(launch, logger); + const store = new DisposableStore(); + try { + const timer = store.add(new IntervalTimer()); + timer.cancelAndSet(() => { + logger.info('Waiting for server to respond to `initialize` request...'); + }, 5000); + + await instaService.invokeFunction(async accessor => { + const productService = accessor.get(IProductService); + const initialized = await mcp.sendRequest({ + method: 'initialize', + params: { + protocolVersion: MCP.LATEST_PROTOCOL_VERSION, + capabilities: { + roots: { listChanged: true }, + }, + clientInfo: { + name: productService.nameLong, + version: productService.version, + } + } + }, token); + + mcp._serverInit = initialized; + + mcp.sendNotification({ + method: 'notifications/initialized' + }); + }); + + return mcp; + } catch (e) { + mcp.dispose(); + throw e; + } finally { + store.dispose(); + } + } + + protected constructor( + private readonly launch: IMcpMessageTransport, + public readonly logger: ILogger, + ) { + super(); + this._register(launch.onDidReceiveMessage(message => this.handleMessage(message))); + this._register(autorun(reader => { + const state = launch.state.read(reader).state; + // the handler will get disposed when the launch stops, but if we're still + // create()'ing we need to make sure to cancel the initialize request. + if (state === McpConnectionState.Kind.Error || state === McpConnectionState.Kind.Stopped) { + this.cancelAllRequests(); + } + })); + } + + /** + * Send a client request to the server and return the response. + * + * @param request The request to send + * @param token Cancellation token + * @param timeoutMs Optional timeout in milliseconds + * @returns A promise that resolves with the response + */ + private async sendRequest( + request: Pick, + token: CancellationToken = CancellationToken.None + ): Promise { + if (this._store.isDisposed) { + return Promise.reject(new CancellationError()); + } + + const id = this._nextRequestId++; + + // Create the full JSON-RPC request + const jsonRpcRequest: MCP.JSONRPCRequest = { + jsonrpc: MCP.JSONRPC_VERSION, + id, + ...request + }; + + const promise = new DeferredPromise(); + // Store the pending request + this._pendingRequests.set(id, { promise }); + // Set up cancellation + const cancelListener = token.onCancellationRequested(() => { + if (!promise.isSettled) { + this._pendingRequests.delete(id); + this.sendNotification({ method: 'notifications/cancelled', params: { requestId: id } }); + promise.cancel(); + } + cancelListener.dispose(); + }); + + // Send the request + this.send(jsonRpcRequest); + const ret = promise.p.finally(() => { + cancelListener.dispose(); + this._pendingRequests.delete(id); + }); + + return ret as Promise; + } + + private send(mcp: MCP.JSONRPCMessage) { + if (canLog(this.logger.getLevel(), LogLevel.Debug)) { // avoid building the string if we don't need to + this.logger.debug(`[editor -> server] ${JSON.stringify(mcp)}`); + } + + this.launch.send(mcp); + } + + /** + * Handles paginated requests by making multiple requests until all items are retrieved. + * + * @param method The method name to call + * @param getItems Function to extract the array of items from a result + * @param initialParams Initial parameters + * @param token Cancellation token + * @returns Promise with all items combined + */ + private async sendRequestPaginated(method: T['method'], getItems: (result: R) => I[], initialParams?: Omit, token: CancellationToken = CancellationToken.None): Promise { + let allItems: I[] = []; + let nextCursor: MCP.Cursor | undefined = undefined; + + do { + const params: T['params'] = { + ...initialParams, + cursor: nextCursor + }; + + const result: R = await this.sendRequest({ method, params }, token); + allItems = allItems.concat(getItems(result)); + nextCursor = result.nextCursor; + } while (nextCursor !== undefined && !token.isCancellationRequested); + + return allItems; + } + + private sendNotification(notification: N): void { + this.send({ ...notification, jsonrpc: MCP.JSONRPC_VERSION }); + } + + /** + * Handle incoming messages from the server + */ + private handleMessage(message: MCP.JSONRPCMessage): void { + if (canLog(this.logger.getLevel(), LogLevel.Debug)) { // avoid building the string if we don't need to + this.logger.debug(`[server <- editor] ${JSON.stringify(message)}`); + } + + // Handle responses to our requests + if ('id' in message) { + if ('result' in message) { + this.handleResult(message); + } else if ('error' in message) { + this.handleError(message); + } + } + + // Handle requests from the server + if ('method' in message) { + if ('id' in message) { + this.handleServerRequest(message as MCP.JSONRPCRequest & MCP.ServerRequest); + } else { + this.handleServerNotification(message as MCP.JSONRPCNotification & MCP.ServerNotification); + + } + } + } + + /** + * Handle successful responses + */ + private handleResult(response: MCP.JSONRPCResponse): void { + const request = this._pendingRequests.get(response.id); + if (request) { + this._pendingRequests.delete(response.id); + request.promise.complete(response.result); + } + } + + /** + * Handle error responses + */ + private handleError(response: MCP.JSONRPCError): void { + const request = this._pendingRequests.get(response.id); + if (request) { + this._pendingRequests.delete(response.id); + request.promise.error(new MpcResponseError(response.error.message, response.error.code, response.error.data)); + } + } + + /** + * Handle incoming server requests + */ + private handleServerRequest(request: MCP.JSONRPCRequest & MCP.ServerRequest): void { + switch (request.method) { + case 'ping': + return this.respondToRequest(request, this.handlePing(request)); + case 'roots/list': + return this.respondToRequest(request, this.handleRootsList(request)); + + default: { + const errorResponse: MCP.JSONRPCError = { + jsonrpc: MCP.JSONRPC_VERSION, + id: request.id, + error: { + code: MCP.METHOD_NOT_FOUND, + message: `Method not found: ${request.method}` + } + }; + this.send(errorResponse); + break; + } + } + } + /** + * Handle incoming server notifications + */ + private handleServerNotification(request: MCP.JSONRPCNotification & MCP.ServerNotification): void { + switch (request.method) { + case 'notifications/message': + return this.handleLoggingNotification(request); + case 'notifications/cancelled': + this._onDidReceiveCancelledNotification.fire(request); + return this.handleCancelledNotification(request); + case 'notifications/progress': + this._onDidReceiveProgressNotification.fire(request); + return; + case 'notifications/resources/list_changed': + this._onDidChangeResourceList.fire(); + return; + case 'notifications/resources/updated': + this._onDidUpdateResource.fire(request); + return; + case 'notifications/tools/list_changed': + this._onDidChangeToolList.fire(); + return; + case 'notifications/prompts/list_changed': + this._onDidChangePromptList.fire(); + return; + } + } + + private handleCancelledNotification(request: MCP.CancelledNotification): void { + const pendingRequest = this._pendingRequests.get(request.params.requestId); + if (pendingRequest) { + this._pendingRequests.delete(request.params.requestId); + pendingRequest.promise.cancel(); + } + } + + private handleLoggingNotification(request: MCP.LoggingMessageNotification): void { + let contents = typeof request.params.data === 'string' ? request.params.data : JSON.stringify(request.params.data); + if (request.params.logger) { + contents = `${request.params.logger}: ${contents}`; + } + + switch (request.params?.level) { + case 'debug': + this.logger.debug(contents); + break; + case 'info': + case 'notice': + this.logger.info(contents); + break; + case 'warning': + this.logger.warn(contents); + break; + case 'error': + case 'critical': + case 'alert': + case 'emergency': + this.logger.error(contents); + break; + default: + this.logger.info(contents); + break; + } + } + + /** + * Send a generic response to a request + */ + private respondToRequest(request: MCP.JSONRPCRequest, result: MCP.Result): void { + const response: MCP.JSONRPCResponse = { + jsonrpc: MCP.JSONRPC_VERSION, + id: request.id, + result + }; + this.send(response); + } + + /** + * Send a response to a ping request + */ + private handlePing(_request: MCP.PingRequest): {} { + return {}; + } + + /** + * Send a response to a roots/list request + */ + private handleRootsList(_request: MCP.ListRootsRequest): MCP.ListRootsResult { + this._hasAnnouncedRoots = true; + return { roots: this._roots }; + } + + private cancelAllRequests() { + this._pendingRequests.forEach(pending => pending.promise.cancel()); + this._pendingRequests.clear(); + } + + public override dispose(): void { + this.cancelAllRequests(); + super.dispose(); + } + + /** + * Send an initialize request + */ + initialize(params: MCP.InitializeRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'initialize', params }, token); + } + + /** + * List available resources + */ + listResources(params?: MCP.ListResourcesRequest['params'], token?: CancellationToken): Promise { + return this.sendRequestPaginated('resources/list', result => result.resources, params, token); + } + + /** + * Read a specific resource + */ + readResource(params: MCP.ReadResourceRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'resources/read', params }, token); + } + + /** + * List available resource templates + */ + listResourceTemplates(params?: MCP.ListResourceTemplatesRequest['params'], token?: CancellationToken): Promise { + return this.sendRequestPaginated('resources/templates/list', result => result.resourceTemplates, params, token); + } + + /** + * Subscribe to resource updates + */ + subscribe(params: MCP.SubscribeRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'resources/subscribe', params }, token); + } + + /** + * Unsubscribe from resource updates + */ + unsubscribe(params: MCP.UnsubscribeRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'resources/unsubscribe', params }, token); + } + + /** + * List available prompts + */ + listPrompts(params?: MCP.ListPromptsRequest['params'], token?: CancellationToken): Promise { + return this.sendRequestPaginated('prompts/list', result => result.prompts, params, token); + } + + /** + * Get a specific prompt + */ + getPrompt(params: MCP.GetPromptRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'prompts/get', params }, token); + } + + /** + * List available tools + */ + listTools(params?: MCP.ListToolsRequest['params'], token?: CancellationToken): Promise { + return this.sendRequestPaginated('tools/list', result => result.tools, params, token); + } + + /** + * Call a specific tool + */ + callTool(params: MCP.CallToolRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'tools/call', params }, token); + } + + /** + * Set the logging level + */ + setLevel(params: MCP.SetLevelRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'logging/setLevel', params }, token); + } + + /** + * Find completions for an argument + */ + complete(params: MCP.CompleteRequest['params'], token?: CancellationToken): Promise { + return this.sendRequest({ method: 'completion/complete', params }, token); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts new file mode 100644 index 00000000..38666d83 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -0,0 +1,275 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; +import { equals } from '../../../../base/common/objects.js'; +import { autorun, IObservable, observableValue, transaction } from '../../../../base/common/observable.js'; +import { localize } from '../../../../nls.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { StorageScope } from '../../../../platform/storage/common/storage.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js'; +import { IMcpRegistry } from './mcpRegistryTypes.js'; +import { McpServer, McpServerMetadataCache } from './mcpServer.js'; +import { IMcpServer, IMcpService, IMcpTool, McpCollectionDefinition, McpServerDefinition, McpServerToolsState } from './mcpTypes.js'; + +interface ISyncedToolData { + toolData: IToolData; + toolDispose: IDisposable; + implDispose: IDisposable; +} + +type IMcpServerRec = IReference; + +export class McpService extends Disposable implements IMcpService { + + declare _serviceBrand: undefined; + + private readonly _servers = observableValue(this, []); + public readonly servers: IObservable = this._servers.map(servers => servers.map(s => s.object)); + + public get lazyCollectionState() { return this._mcpRegistry.lazyCollectionState; } + + protected readonly userCache: McpServerMetadataCache; + protected readonly workspaceCache: McpServerMetadataCache; + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @ILanguageModelToolsService private readonly _toolsService: ILanguageModelToolsService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + + this.userCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.PROFILE)); + this.workspaceCache = this._register(_instantiationService.createInstance(McpServerMetadataCache, StorageScope.WORKSPACE)); + + const updateThrottle = this._store.add(new RunOnceScheduler(() => this._updateCollectedServers(), 500)); + + // Throttle changes so that if a collection is changed, or a server is + // unregistered/registered, we don't stop servers unnecessarily. + this._register(autorun(reader => { + for (const collection of this._mcpRegistry.collections.read(reader)) { + collection.serverDefinitions.read(reader); + } + updateThrottle.schedule(500); + })); + } + + public resetCaches(): void { + this.userCache.reset(); + this.workspaceCache.reset(); + } + + public async activateCollections(): Promise { + const collections = await this._mcpRegistry.discoverCollections(); + const collectionIds = new Set(collections.map(c => c.id)); + + this._updateCollectedServers(); + + // Discover any newly-collected servers with unknown tools + const todo: Promise[] = []; + for (const { object: server } of this._servers.get()) { + if (collectionIds.has(server.collection.id)) { + const state = server.toolsState.get(); + if (state === McpServerToolsState.Unknown) { + todo.push(server.start()); + } + } + } + + await Promise.all(todo); + } + + private _syncTools(server: McpServer, store: DisposableStore) { + const tools = new Map(); + + store.add(autorun(reader => { + const toDelete = new Set(tools.keys()); + for (const tool of server.tools.read(reader)) { + const existing = tools.get(tool.id); + const collection = this._mcpRegistry.collections.get().find(c => c.id === server.collection.id); + const toolData: IToolData = { + id: tool.id, + source: { type: 'mcp', collectionId: server.collection.id, definitionId: server.definition.id }, + icon: Codicon.tools, + displayName: tool.definition.name, + toolReferenceName: tool.definition.name, + modelDescription: tool.definition.description ?? '', + userDescription: tool.definition.description ?? '', + inputSchema: tool.definition.inputSchema, + canBeReferencedInPrompt: true, + supportsToolPicker: true, + runsInWorkspace: collection?.scope === StorageScope.WORKSPACE || !!collection?.remoteAuthority, + tags: ['mcp'], + }; + + if (existing) { + if (!equals(existing.toolData, toolData)) { + existing.toolData = toolData; + existing.toolDispose.dispose(); + existing.toolDispose = this._toolsService.registerToolData(toolData); + } + toDelete.delete(tool.id); + } else { + tools.set(tool.id, { + toolData, + toolDispose: this._toolsService.registerToolData(toolData), + implDispose: this._toolsService.registerToolImplementation(tool.id, this._instantiationService.createInstance(McpToolImplementation, tool, server)), + }); + } + } + + for (const id of toDelete) { + const tool = tools.get(id); + if (tool) { + tool.toolDispose.dispose(); + tool.implDispose.dispose(); + tools.delete(id); + } + } + })); + + store.add(toDisposable(() => { + for (const tool of tools.values()) { + tool.toolDispose.dispose(); + tool.implDispose.dispose(); + } + })); + } + + private _updateCollectedServers() { + const definitions = this._mcpRegistry.collections.get().flatMap(collectionDefinition => + collectionDefinition.serverDefinitions.get().map(serverDefinition => ({ + serverDefinition, + collectionDefinition, + })) + ); + + const nextDefinitions = new Set(definitions); + const currentServers = this._servers.get(); + const nextServers: IMcpServerRec[] = []; + const pushMatch = (match: (typeof definitions)[0], rec: IMcpServerRec) => { + nextDefinitions.delete(match); + nextServers.push(rec); + const connection = rec.object.connection.get(); + // if the definition was modified, stop the server; it'll be restarted again on-demand + if (connection && !McpServerDefinition.equals(connection.definition, match.serverDefinition)) { + rec.object.stop(); + this._logService.debug(`MCP server ${rec.object.definition.id} stopped because the definition changed`); + } + }; + + // Transfer over any servers that are still valid. + for (const server of currentServers) { + const match = definitions.find(d => defsEqual(server.object, d)); + if (match) { + pushMatch(match, server); + } else { + server.dispose(); + } + } + + // Create any new servers that are needed. + for (const def of nextDefinitions) { + const store = new DisposableStore(); + const object = this._instantiationService.createInstance( + McpServer, + def.collectionDefinition, + def.serverDefinition, + def.serverDefinition.roots, + !!def.collectionDefinition.lazy, + def.collectionDefinition.scope === StorageScope.WORKSPACE ? this.workspaceCache : this.userCache, + ); + store.add(object); + this._syncTools(object, store); + + nextServers.push({ object, dispose: () => store.dispose() }); + } + + transaction(tx => { + this._servers.set(nextServers, tx); + }); + } + + public override dispose(): void { + this._servers.get().forEach(s => s.dispose()); + super.dispose(); + } +} + +function defsEqual(server: IMcpServer, def: { serverDefinition: McpServerDefinition; collectionDefinition: McpCollectionDefinition }) { + return server.collection.id === def.collectionDefinition.id && server.definition.id === def.serverDefinition.id; +} + +class McpToolImplementation implements IToolImpl { + constructor( + private readonly _tool: IMcpTool, + private readonly _server: IMcpServer, + @IProductService private readonly _productService: IProductService, + ) { } + + async prepareToolInvocation(parameters: any): Promise { + const tool = this._tool; + const server = this._server; + + const mcpToolWarning = localize( + 'mcp.tool.warning', + "{0} This tool is from \'{1}\' (MCP Server). Note that MCP servers or malicious conversation content may attempt to misuse '{2}' through tools. Please carefully review any requested actions.", + '$(info)', + server.definition.label, + this._productService.nameShort + ); + + return { + confirmationMessages: { + title: localize('msg.title', "Run `{0}`", tool.definition.name, server.definition.label), + message: new MarkdownString(localize('msg.msg', "{0}\n\n {1}", tool.definition.description, mcpToolWarning), { supportThemeIcons: true }), + allowAutoConfirm: true, + }, + invocationMessage: new MarkdownString(localize('msg.run', "Running `{0}`", tool.definition.name, server.definition.label)), + pastTenseMessage: new MarkdownString(localize('msg.ran', "Ran `{0}` ", tool.definition.name, server.definition.label)), + toolSpecificData: { + kind: 'input', + rawInput: parameters + } + }; + } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken) { + + const result: IToolResult = { + content: [] + }; + + const outputParts: string[] = []; + + const callResult = await this._tool.call(invocation.parameters as Record, token); + for (const item of callResult.content) { + if (item.type === 'text') { + result.content.push({ + kind: 'text', + value: item.text + }); + + outputParts.push(item.text); + } else { + // TODO@jrieken handle different item types + } + } + + result.toolResultDetails = { + input: JSON.stringify(invocation.parameters, undefined, 2), + output: outputParts.join('\n') + }; + + return result; + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts new file mode 100644 index 00000000..6c4a2540 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -0,0 +1,413 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { assertNever } from '../../../../base/common/assert.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { equals as objectsEqual } from '../../../../base/common/objects.js'; +import { equals as arraysEqual } from '../../../../base/common/arrays.js'; +import { IObservable } from '../../../../base/common/observable.js'; +import { URI, UriComponents } from '../../../../base/common/uri.js'; +import { Location } from '../../../../editor/common/languages.js'; +import { localize } from '../../../../nls.js'; +import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { StorageScope } from '../../../../platform/storage/common/storage.js'; +import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; +import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; +import { MCP } from './modelContextProtocol.js'; + +export const extensionMcpCollectionPrefix = 'ext.'; + +export function extensionPrefixedIdentifier(identifier: ExtensionIdentifier, id: string): string { + return ExtensionIdentifier.toKey(identifier) + '/' + id; +} + +/** + * An McpCollection contains McpServers. There may be multiple collections for + * different locations servers are discovered. + */ +export interface McpCollectionDefinition { + /** Origin authority from which this collection was discovered. */ + readonly remoteAuthority: string | null; + /** Globally-unique, stable ID for this definition */ + readonly id: string; + /** Human-readable label for the definition */ + readonly label: string; + /** Definitions this collection contains. */ + readonly serverDefinitions: IObservable; + /** If 'false', consent is required before any MCP servers in this collection are automatically launched. */ + readonly isTrustedByDefault: boolean; + /** Scope where associated collection info should be stored. */ + readonly scope: StorageScope; + + /** For lazy-loaded collections only: */ + readonly lazy?: { + /** True if `serverDefinitions` were loaded from the cache */ + isCached: boolean; + /** Triggers a load of the real server definition, which should be pushed to the IMcpRegistry. If not this definition will be removed. */ + load(): Promise; + /** Called after `load()` if the extension is not found. */ + removed?(): void; + }; + + readonly presentation?: { + /** Sort order of the collection. */ + readonly order?: number; + /** Place where this collection is configured, used in workspace trust prompts and "show config" */ + readonly origin?: URI; + }; +} + +export const enum McpCollectionSortOrder { + WorkspaceFolder = 0, + Workspace = 100, + User = 200, + Extension = 300, + Filesystem = 400, + + RemoteBoost = -50, +} + +export namespace McpCollectionDefinition { + export interface FromExtHost { + readonly id: string; + readonly label: string; + readonly isTrustedByDefault: boolean; + readonly scope: StorageScope; + } + + export function equals(a: McpCollectionDefinition, b: McpCollectionDefinition): boolean { + return a.id === b.id + && a.remoteAuthority === b.remoteAuthority + && a.label === b.label + && a.isTrustedByDefault === b.isTrustedByDefault; + } +} + +export interface McpServerDefinition { + /** Globally-unique, stable ID for this definition */ + readonly id: string; + /** Human-readable label for the definition */ + readonly label: string; + /** Descriptor defining how the configuration should be launched. */ + readonly launch: McpServerLaunch; + /** Explicit roots. If undefined, all workspace folders. */ + readonly roots?: URI[] | undefined; + /** If set, allows configuration variables to be resolved in the {@link launch} with the given context */ + readonly variableReplacement?: McpServerDefinitionVariableReplacement; + + readonly presentation?: { + /** Sort order of the definition. */ + readonly order?: number; + /** Place where this server is configured, used in workspace trust prompts and "show config" */ + readonly origin?: Location; + }; +} + +export namespace McpServerDefinition { + export interface Serialized { + readonly id: string; + readonly label: string; + readonly launch: McpServerLaunch.Serialized; + readonly variableReplacement?: McpServerDefinitionVariableReplacement.Serialized; + } + + export function toSerialized(def: McpServerDefinition): McpServerDefinition.Serialized { + return def; + } + + export function fromSerialized(def: McpServerDefinition.Serialized): McpServerDefinition { + return { + id: def.id, + label: def.label, + launch: McpServerLaunch.fromSerialized(def.launch), + variableReplacement: def.variableReplacement ? McpServerDefinitionVariableReplacement.fromSerialized(def.variableReplacement) : undefined, + }; + } + + export function equals(a: McpServerDefinition, b: McpServerDefinition): boolean { + return a.id === b.id + && a.label === b.label + && arraysEqual(a.roots, b.roots, (a, b) => a.toString() === b.toString()) + && objectsEqual(a.launch, b.launch) + && objectsEqual(a.presentation, b.presentation) + && objectsEqual(a.variableReplacement, b.variableReplacement); + } +} + + +export interface McpServerDefinitionVariableReplacement { + section?: string; // e.g. 'mcp' + folder?: IWorkspaceFolderData; + target: ConfigurationTarget; +} + +export namespace McpServerDefinitionVariableReplacement { + export interface Serialized { + target: ConfigurationTarget; + section?: string; + folder?: { name: string; index: number; uri: UriComponents }; + } + + export function toSerialized(def: McpServerDefinitionVariableReplacement): McpServerDefinitionVariableReplacement.Serialized { + return def; + } + + export function fromSerialized(def: McpServerDefinitionVariableReplacement.Serialized): McpServerDefinitionVariableReplacement { + return { + section: def.section, + folder: def.folder ? { ...def.folder, uri: URI.revive(def.folder.uri) } : undefined, + target: def.target, + }; + } +} + +export interface IMcpService { + _serviceBrand: undefined; + readonly servers: IObservable; + + /** Resets the cached tools. */ + resetCaches(): void; + + /** Set if there are extensions that register MCP servers that have never been activated. */ + readonly lazyCollectionState: IObservable; + /** Activatese extensions and runs their MCP servers. */ + activateCollections(): Promise; +} + +export const enum LazyCollectionState { + HasUnknown, + LoadingUnknown, + AllKnown, +} + +export const IMcpService = createDecorator('IMcpService'); + +export interface McpCollectionReference { + id: string; + label: string; + presentation?: McpCollectionDefinition['presentation']; +} + +export interface McpDefinitionReference { + id: string; + label: string; +} + +export interface IMcpServer extends IDisposable { + readonly collection: McpCollectionReference; + readonly definition: McpDefinitionReference; + readonly connection: IObservable; + readonly connectionState: IObservable; + /** + * Reflects the MCP server trust state. True if trusted, false if untrusted, + * undefined if consent is required but not indicated. + */ + readonly trusted: IObservable; + + showOutput(): void; + /** + * Starts the server and returns its resulting state. One of: + * - Running, if all good + * - Error, if the server failed to start + * - Stopped, if the server was disposed or the user cancelled the launch + */ + start(isFromInteraction?: boolean): Promise; + stop(): Promise; + + readonly toolsState: IObservable; + readonly tools: IObservable; +} + +export const enum McpServerToolsState { + /** Tools have not been read before */ + Unknown, + /** Tools were read from the cache */ + Cached, + /** Tools are refreshing for the first time */ + RefreshingFromUnknown, + /** Tools are refreshing and the current tools are cached */ + RefreshingFromCached, + /** Tool state is live, server is connected */ + Live, +} + +export interface IMcpTool { + + readonly id: string; + + readonly definition: MCP.Tool; + + /** + * Calls a tool + * @throws {@link MpcResponseError} if the tool fails to execute + * @throws {@link McpConnectionFailedError} if the connection to the server fails + */ + call(params: Record, token?: CancellationToken): Promise; +} + +export const enum McpServerTransportType { + /** A command-line MCP server communicating over standard in/out */ + Stdio = 1 << 0, + /** An MCP server that uses Server-Sent Events */ + SSE = 1 << 1, +} + +/** + * MCP server launched on the command line which communicated over stdio. + * https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio + */ +export interface McpServerTransportStdio { + readonly type: McpServerTransportType.Stdio; + readonly cwd: URI | undefined; + readonly command: string; + readonly args: readonly string[]; + readonly env: Record; + readonly envFile: string | undefined; +} + +/** + * MCP server launched on the command line which communicated over server-sent-events. + * https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse + */ +export interface McpServerTransportSSE { + readonly type: McpServerTransportType.SSE; + readonly uri: URI; + readonly headers: [string, string][]; +} + +export type McpServerLaunch = + | McpServerTransportStdio + | McpServerTransportSSE; + +export namespace McpServerLaunch { + export type Serialized = + | { type: McpServerTransportType.SSE; uri: UriComponents; headers: [string, string][] } + | { type: McpServerTransportType.Stdio; cwd: UriComponents | undefined; command: string; args: readonly string[]; env: Record; envFile: string | undefined }; + + export function toSerialized(launch: McpServerLaunch): McpServerLaunch.Serialized { + return launch; + } + + export function fromSerialized(launch: McpServerLaunch.Serialized): McpServerLaunch { + switch (launch.type) { + case McpServerTransportType.SSE: + return { type: launch.type, uri: URI.revive(launch.uri), headers: launch.headers }; + case McpServerTransportType.Stdio: + return { + type: launch.type, + cwd: launch.cwd ? URI.revive(launch.cwd) : undefined, + command: launch.command, + args: launch.args, + env: launch.env, + envFile: launch.envFile, + }; + } + } +} + +/** + * An instance that manages a connection to an MCP server. It can be started, + * stopped, and restarted. Once started and in a running state, it will + * eventually build a {@link IMcpServerConnection.handler}. + */ +export interface IMcpServerConnection extends IDisposable { + readonly definition: McpServerDefinition; + readonly state: IObservable; + readonly handler: IObservable; + + /** + * Starts the server if it's stopped. Returns a promise that resolves once + * server exits a 'starting' state. + */ + start(): Promise; + + /** + * Stops the server. + */ + stop(): Promise; +} + +/** + * McpConnectionState is the state of the underlying connection and is + * communicated e.g. from the extension host to the renderer. + */ +export namespace McpConnectionState { + export const enum Kind { + Stopped, + Starting, + Running, + Error, + } + + export const toString = (s: McpConnectionState): string => { + switch (s.state) { + case Kind.Stopped: + return localize('mcpstate.stopped', 'Stopped'); + case Kind.Starting: + return localize('mcpstate.starting', 'Starting'); + case Kind.Running: + return localize('mcpstate.running', 'Running'); + case Kind.Error: + return localize('mcpstate.error', 'Error {0}', s.message); + default: + assertNever(s); + } + }; + + export const toKindString = (s: McpConnectionState.Kind): string => { + switch (s) { + case Kind.Stopped: + return 'stopped'; + case Kind.Starting: + return 'starting'; + case Kind.Running: + return 'running'; + case Kind.Error: + return 'error'; + default: + assertNever(s); + } + }; + + /** Returns if the MCP state is one where starting a new server is valid */ + export const canBeStarted = (s: Kind) => s === Kind.Error || s === Kind.Stopped; + + /** Gets whether the state is a running state. */ + export const isRunning = (s: McpConnectionState) => !canBeStarted(s.state); + + export interface Stopped { + readonly state: Kind.Stopped; + } + + export interface Starting { + readonly state: Kind.Starting; + } + + export interface Running { + readonly state: Kind.Running; + } + + export interface Error { + readonly state: Kind.Error; + readonly message: string; + } +} + +export type McpConnectionState = + | McpConnectionState.Stopped + | McpConnectionState.Starting + | McpConnectionState.Running + | McpConnectionState.Error; + +export class MpcResponseError extends Error { + constructor(message: string, public readonly code: number, public readonly data: unknown) { + super(`MPC ${code}: ${message}`); + } +} + +export class McpConnectionFailedError extends Error { } diff --git a/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts new file mode 100644 index 00000000..582e69ea --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts @@ -0,0 +1,1138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable @stylistic/ts/member-delimiter-style */ +/* eslint-disable local/code-no-unexternalized-strings */ + +/** + * Schema updated from the Model Context Protocol repository at + * https://github.com/modelcontextprotocol/specification/tree/main/schema + * + * ⚠️ Do not edit within `namespace` manually except to update schema versions ⚠️ + */ +export namespace MCP { + /* JSON-RPC types */ + export type JSONRPCMessage = + | JSONRPCRequest + | JSONRPCNotification + | JSONRPCResponse + | JSONRPCError; + + export const LATEST_PROTOCOL_VERSION = "2024-11-05"; + export const JSONRPC_VERSION = "2.0"; + + /** + * A progress token, used to associate progress notifications with the original request. + */ + export type ProgressToken = string | number; + + /** + * An opaque token used to represent a cursor for pagination. + */ + export type Cursor = string; + + export interface Request { + method: string; + params?: { + _meta?: { + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken?: ProgressToken; + }; + [key: string]: unknown; + }; + } + + export interface Notification { + method: string; + params?: { + /** + * This parameter name is reserved by MCP to allow clients and servers to attach additional metadata to their notifications. + */ + _meta?: { [key: string]: unknown }; + [key: string]: unknown; + }; + } + + export interface Result { + /** + * This result property is reserved by the protocol to allow clients and servers to attach additional metadata to their responses. + */ + _meta?: { [key: string]: unknown }; + [key: string]: unknown; + } + + /** + * A uniquely identifying ID for a request in JSON-RPC. + */ + export type RequestId = string | number; + + /** + * A request that expects a response. + */ + export interface JSONRPCRequest extends Request { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; + } + + /** + * A notification which does not expect a response. + */ + export interface JSONRPCNotification extends Notification { + jsonrpc: typeof JSONRPC_VERSION; + } + + /** + * A successful (non-error) response to a request. + */ + export interface JSONRPCResponse { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; + result: Result; + } + + // Standard JSON-RPC error codes + export const PARSE_ERROR = -32700; + export const INVALID_REQUEST = -32600; + export const METHOD_NOT_FOUND = -32601; + export const INVALID_PARAMS = -32602; + export const INTERNAL_ERROR = -32603; + + /** + * A response to a request that indicates an error occurred. + */ + export interface JSONRPCError { + jsonrpc: typeof JSONRPC_VERSION; + id: RequestId; + error: { + /** + * The error type that occurred. + */ + code: number; + /** + * A short description of the error. The message SHOULD be limited to a concise single sentence. + */ + message: string; + /** + * Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + */ + data?: unknown; + }; + } + + /* Empty result */ + /** + * A response that indicates success but carries no data. + */ + export type EmptyResult = Result; + + /* Cancellation */ + /** + * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. + * + * The request SHOULD still be in-flight, but due to communication latency, it is always possible that this notification MAY arrive after the request has already finished. + * + * This notification indicates that the result will be unused, so any associated processing SHOULD cease. + * + * A client MUST NOT attempt to cancel its `initialize` request. + */ + export interface CancelledNotification extends Notification { + method: "notifications/cancelled"; + params: { + /** + * The ID of the request to cancel. + * + * This MUST correspond to the ID of a request previously issued in the same direction. + */ + requestId: RequestId; + + /** + * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. + */ + reason?: string; + }; + } + + /* Initialization */ + /** + * This request is sent from the client to the server when it first connects, asking it to begin initialization. + */ + export interface InitializeRequest extends Request { + method: "initialize"; + params: { + /** + * The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well. + */ + protocolVersion: string; + capabilities: ClientCapabilities; + clientInfo: Implementation; + }; + } + + /** + * After receiving an initialize request from the client, the server sends this response. + */ + export interface InitializeResult extends Result { + /** + * The version of the Model Context Protocol that the server wants to use. This may not match the version that the client requested. If the client cannot support this version, it MUST disconnect. + */ + protocolVersion: string; + capabilities: ServerCapabilities; + serverInfo: Implementation; + /** + * Instructions describing how to use the server and its features. + * + * This can be used by clients to improve the LLM's understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt. + */ + instructions?: string; + } + + /** + * This notification is sent from the client to the server after initialization has finished. + */ + export interface InitializedNotification extends Notification { + method: "notifications/initialized"; + } + + /** + * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + */ + export interface ClientCapabilities { + /** + * Experimental, non-standard capabilities that the client supports. + */ + experimental?: { [key: string]: object }; + /** + * Present if the client supports listing roots. + */ + roots?: { + /** + * Whether the client supports notifications for changes to the roots list. + */ + listChanged?: boolean; + }; + /** + * Present if the client supports sampling from an LLM. + */ + sampling?: object; + } + + /** + * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + */ + export interface ServerCapabilities { + /** + * Experimental, non-standard capabilities that the server supports. + */ + experimental?: { [key: string]: object }; + /** + * Present if the server supports sending log messages to the client. + */ + logging?: object; + /** + * Present if the server offers any prompt templates. + */ + prompts?: { + /** + * Whether this server supports notifications for changes to the prompt list. + */ + listChanged?: boolean; + }; + /** + * Present if the server offers any resources to read. + */ + resources?: { + /** + * Whether this server supports subscribing to resource updates. + */ + subscribe?: boolean; + /** + * Whether this server supports notifications for changes to the resource list. + */ + listChanged?: boolean; + }; + /** + * Present if the server offers any tools to call. + */ + tools?: { + /** + * Whether this server supports notifications for changes to the tool list. + */ + listChanged?: boolean; + }; + } + + /** + * Describes the name and version of an MCP implementation. + */ + export interface Implementation { + name: string; + version: string; + } + + /* Ping */ + /** + * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. + */ + export interface PingRequest extends Request { + method: "ping"; + } + + /* Progress notifications */ + /** + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + */ + export interface ProgressNotification extends Notification { + method: "notifications/progress"; + params: { + /** + * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. + */ + progressToken: ProgressToken; + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + * + * @TJS-type number + */ + progress: number; + /** + * Total number of items to process (or total progress required), if known. + * + * @TJS-type number + */ + total?: number; + }; + } + + /* Pagination */ + export interface PaginatedRequest extends Request { + params?: { + /** + * An opaque token representing the current pagination position. + * If provided, the server should return results starting after this cursor. + */ + cursor?: Cursor; + }; + } + + export interface PaginatedResult extends Result { + /** + * An opaque token representing the pagination position after the last returned result. + * If present, there may be more results available. + */ + nextCursor?: Cursor; + } + + /* Resources */ + /** + * Sent from the client to request a list of resources the server has. + */ + export interface ListResourcesRequest extends PaginatedRequest { + method: "resources/list"; + } + + /** + * The server's response to a resources/list request from the client. + */ + export interface ListResourcesResult extends PaginatedResult { + resources: Resource[]; + } + + /** + * Sent from the client to request a list of resource templates the server has. + */ + export interface ListResourceTemplatesRequest extends PaginatedRequest { + method: "resources/templates/list"; + } + + /** + * The server's response to a resources/templates/list request from the client. + */ + export interface ListResourceTemplatesResult extends PaginatedResult { + resourceTemplates: ResourceTemplate[]; + } + + /** + * Sent from the client to the server, to read a specific resource URI. + */ + export interface ReadResourceRequest extends Request { + method: "resources/read"; + params: { + /** + * The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it. + * + * @format uri + */ + uri: string; + }; + } + + /** + * The server's response to a resources/read request from the client. + */ + export interface ReadResourceResult extends Result { + contents: (TextResourceContents | BlobResourceContents)[]; + } + + /** + * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. + */ + export interface ResourceListChangedNotification extends Notification { + method: "notifications/resources/list_changed"; + } + + /** + * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. + */ + export interface SubscribeRequest extends Request { + method: "resources/subscribe"; + params: { + /** + * The URI of the resource to subscribe to. The URI can use any protocol; it is up to the server how to interpret it. + * + * @format uri + */ + uri: string; + }; + } + + /** + * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. + */ + export interface UnsubscribeRequest extends Request { + method: "resources/unsubscribe"; + params: { + /** + * The URI of the resource to unsubscribe from. + * + * @format uri + */ + uri: string; + }; + } + + /** + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. + */ + export interface ResourceUpdatedNotification extends Notification { + method: "notifications/resources/updated"; + params: { + /** + * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. + * + * @format uri + */ + uri: string; + }; + } + + /** + * A known resource that the server is capable of reading. + */ + export interface Resource extends Annotated { + /** + * The URI of this resource. + * + * @format uri + */ + uri: string; + + /** + * A human-readable name for this resource. + * + * This can be used by clients to populate UI elements. + */ + name: string; + + /** + * A description of what this resource represents. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * The MIME type of this resource, if known. + */ + mimeType?: string; + + /** + * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known. + * + * This can be used by Hosts to display file sizes and estimate context window usage. + */ + size?: number; + } + + /** + * A template description for resources available on the server. + */ + export interface ResourceTemplate extends Annotated { + /** + * A URI template (according to RFC 6570) that can be used to construct resource URIs. + * + * @format uri-template + */ + uriTemplate: string; + + /** + * A human-readable name for the type of resource this template refers to. + * + * This can be used by clients to populate UI elements. + */ + name: string; + + /** + * A description of what this template is for. + * + * This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model. + */ + description?: string; + + /** + * The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. + */ + mimeType?: string; + } + + /** + * The contents of a specific resource or sub-resource. + */ + export interface ResourceContents { + /** + * The URI of this resource. + * + * @format uri + */ + uri: string; + /** + * The MIME type of this resource, if known. + */ + mimeType?: string; + } + + export interface TextResourceContents extends ResourceContents { + /** + * The text of the item. This must only be set if the item can actually be represented as text (not binary data). + */ + text: string; + } + + export interface BlobResourceContents extends ResourceContents { + /** + * A base64-encoded string representing the binary data of the item. + * + * @format byte + */ + blob: string; + } + + /* Prompts */ + /** + * Sent from the client to request a list of prompts and prompt templates the server has. + */ + export interface ListPromptsRequest extends PaginatedRequest { + method: "prompts/list"; + } + + /** + * The server's response to a prompts/list request from the client. + */ + export interface ListPromptsResult extends PaginatedResult { + prompts: Prompt[]; + } + + /** + * Used by the client to get a prompt provided by the server. + */ + export interface GetPromptRequest extends Request { + method: "prompts/get"; + params: { + /** + * The name of the prompt or prompt template. + */ + name: string; + /** + * Arguments to use for templating the prompt. + */ + arguments?: { [key: string]: string }; + }; + } + + /** + * The server's response to a prompts/get request from the client. + */ + export interface GetPromptResult extends Result { + /** + * An optional description for the prompt. + */ + description?: string; + messages: PromptMessage[]; + } + + /** + * A prompt or prompt template that the server offers. + */ + export interface Prompt { + /** + * The name of the prompt or prompt template. + */ + name: string; + /** + * An optional description of what this prompt provides + */ + description?: string; + /** + * A list of arguments to use for templating the prompt. + */ + arguments?: PromptArgument[]; + } + + /** + * Describes an argument that a prompt can accept. + */ + export interface PromptArgument { + /** + * The name of the argument. + */ + name: string; + /** + * A human-readable description of the argument. + */ + description?: string; + /** + * Whether this argument must be provided. + */ + required?: boolean; + } + + /** + * The sender or recipient of messages and data in a conversation. + */ + export type Role = "user" | "assistant"; + + /** + * Describes a message returned as part of a prompt. + * + * This is similar to `SamplingMessage`, but also supports the embedding of + * resources from the MCP server. + */ + export interface PromptMessage { + role: Role; + content: TextContent | ImageContent | EmbeddedResource; + } + + /** + * The contents of a resource, embedded into a prompt or tool call result. + * + * It is up to the client how best to render embedded resources for the benefit + * of the LLM and/or the user. + */ + export interface EmbeddedResource extends Annotated { + type: "resource"; + resource: TextResourceContents | BlobResourceContents; + } + + /** + * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. + */ + export interface PromptListChangedNotification extends Notification { + method: "notifications/prompts/list_changed"; + } + + /* Tools */ + /** + * Sent from the client to request a list of tools the server has. + */ + export interface ListToolsRequest extends PaginatedRequest { + method: "tools/list"; + } + + /** + * The server's response to a tools/list request from the client. + */ + export interface ListToolsResult extends PaginatedResult { + tools: Tool[]; + } + + /** + * The server's response to a tool call. + * + * Any errors that originate from the tool SHOULD be reported inside the result + * object, with `isError` set to true, _not_ as an MCP protocol-level error + * response. Otherwise, the LLM would not be able to see that an error occurred + * and self-correct. + * + * However, any errors in _finding_ the tool, an error indicating that the + * server does not support tool calls, or any other exceptional conditions, + * should be reported as an MCP error response. + */ + export interface CallToolResult extends Result { + content: (TextContent | ImageContent | EmbeddedResource)[]; + + /** + * Whether the tool call ended in an error. + * + * If not set, this is assumed to be false (the call was successful). + */ + isError?: boolean; + } + + /** + * Used by the client to invoke a tool provided by the server. + */ + export interface CallToolRequest extends Request { + method: "tools/call"; + params: { + name: string; + arguments?: { [key: string]: unknown }; + }; + } + + /** + * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. + */ + export interface ToolListChangedNotification extends Notification { + method: "notifications/tools/list_changed"; + } + + /** + * Definition for a tool the client can call. + */ + export interface Tool { + /** + * The name of the tool. + */ + name: string; + /** + * A human-readable description of the tool. + */ + description?: string; + /** + * A JSON Schema object defining the expected parameters for the tool. + */ + inputSchema: { + type: "object"; + properties?: { [key: string]: object }; + required?: string[]; + }; + } + + /* Logging */ + /** + * A request from the client to the server, to enable or adjust logging. + */ + export interface SetLevelRequest extends Request { + method: "logging/setLevel"; + params: { + /** + * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/message. + */ + level: LoggingLevel; + }; + } + + /** + * Notification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. + */ + export interface LoggingMessageNotification extends Notification { + method: "notifications/message"; + params: { + /** + * The severity of this log message. + */ + level: LoggingLevel; + /** + * An optional name of the logger issuing this message. + */ + logger?: string; + /** + * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + */ + data: unknown; + }; + } + + /** + * The severity of a log message. + * + * These map to syslog message severities, as specified in RFC-5424: + * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 + */ + export type LoggingLevel = + | "debug" + | "info" + | "notice" + | "warning" + | "error" + | "critical" + | "alert" + | "emergency"; + + /* Sampling */ + /** + * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. + */ + export interface CreateMessageRequest extends Request { + method: "sampling/createMessage"; + params: { + messages: SamplingMessage[]; + /** + * The server's preferences for which model to select. The client MAY ignore these preferences. + */ + modelPreferences?: ModelPreferences; + /** + * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. + */ + systemPrompt?: string; + /** + * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. The client MAY ignore this request. + */ + includeContext?: "none" | "thisServer" | "allServers"; + /** + * @TJS-type number + */ + temperature?: number; + /** + * The maximum number of tokens to sample, as requested by the server. The client MAY choose to sample fewer tokens than requested. + */ + maxTokens: number; + stopSequences?: string[]; + /** + * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. + */ + metadata?: object; + }; + } + + /** + * The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it. + */ + export interface CreateMessageResult extends Result, SamplingMessage { + /** + * The name of the model that generated the message. + */ + model: string; + /** + * The reason why sampling stopped, if known. + */ + stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string; + } + + /** + * Describes a message issued to or received from an LLM API. + */ + export interface SamplingMessage { + role: Role; + content: TextContent | ImageContent; + } + + /** + * Base for objects that include optional annotations for the client. The client can use annotations to inform how objects are used or displayed + */ + export interface Annotated { + annotations?: { + /** + * Describes who the intended customer of this object or data is. + * + * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). + */ + audience?: Role[]; + + /** + * Describes how important this data is for operating the server. + * + * A value of 1 means "most important," and indicates that the data is + * effectively required, while 0 means "least important," and indicates that + * the data is entirely optional. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + priority?: number; + } + } + + /** + * Text provided to or from an LLM. + */ + export interface TextContent extends Annotated { + type: "text"; + /** + * The text content of the message. + */ + text: string; + } + + /** + * An image provided to or from an LLM. + */ + export interface ImageContent extends Annotated { + type: "image"; + /** + * The base64-encoded image data. + * + * @format byte + */ + data: string; + /** + * The MIME type of the image. Different providers may support different image types. + */ + mimeType: string; + } + + /** + * The server's preferences for model selection, requested of the client during sampling. + * + * Because LLMs can vary along multiple dimensions, choosing the "best" model is + * rarely straightforward. Different models excel in different areas-some are + * faster but less capable, others are more capable but more expensive, and so + * on. This interface allows servers to express their priorities across multiple + * dimensions to help clients make an appropriate selection for their use case. + * + * These preferences are always advisory. The client MAY ignore them. It is also + * up to the client to decide how to interpret these preferences and how to + * balance them against other considerations. + */ + export interface ModelPreferences { + /** + * Optional hints to use for model selection. + * + * If multiple hints are specified, the client MUST evaluate them in order + * (such that the first match is taken). + * + * The client SHOULD prioritize these hints over the numeric priorities, but + * MAY still use the priorities to select from ambiguous matches. + */ + hints?: ModelHint[]; + + /** + * How much to prioritize cost when selecting a model. A value of 0 means cost + * is not important, while a value of 1 means cost is the most important + * factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + costPriority?: number; + + /** + * How much to prioritize sampling speed (latency) when selecting a model. A + * value of 0 means speed is not important, while a value of 1 means speed is + * the most important factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + speedPriority?: number; + + /** + * How much to prioritize intelligence and capabilities when selecting a + * model. A value of 0 means intelligence is not important, while a value of 1 + * means intelligence is the most important factor. + * + * @TJS-type number + * @minimum 0 + * @maximum 1 + */ + intelligencePriority?: number; + } + + /** + * Hints to use for model selection. + * + * Keys not declared here are currently left unspecified by the spec and are up + * to the client to interpret. + */ + export interface ModelHint { + /** + * A hint for a model name. + * + * The client SHOULD treat this as a substring of a model name; for example: + * - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022` + * - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc. + * - `claude` should match any Claude model + * + * The client MAY also map the string to a different provider's model name or a different model family, as long as it fills a similar niche; for example: + * - `gemini-1.5-flash` could match `claude-3-haiku-20240307` + */ + name?: string; + } + + /* Autocomplete */ + /** + * A request from the client to the server, to ask for completion options. + */ + export interface CompleteRequest extends Request { + method: "completion/complete"; + params: { + ref: PromptReference | ResourceReference; + /** + * The argument's information + */ + argument: { + /** + * The name of the argument + */ + name: string; + /** + * The value of the argument to use for completion matching. + */ + value: string; + }; + }; + } + + /** + * The server's response to a completion/complete request + */ + export interface CompleteResult extends Result { + completion: { + /** + * An array of completion values. Must not exceed 100 items. + */ + values: string[]; + /** + * The total number of completion options available. This can exceed the number of values actually sent in the response. + */ + total?: number; + /** + * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + */ + hasMore?: boolean; + }; + } + + /** + * A reference to a resource or resource template definition. + */ + export interface ResourceReference { + type: "ref/resource"; + /** + * The URI or URI template of the resource. + * + * @format uri-template + */ + uri: string; + } + + /** + * Identifies a prompt. + */ + export interface PromptReference { + type: "ref/prompt"; + /** + * The name of the prompt or prompt template + */ + name: string; + } + + /* Roots */ + /** + * Sent from the server to request a list of root URIs from the client. Roots allow + * servers to ask for specific directories or files to operate on. A common example + * for roots is providing a set of repositories or directories a server should operate + * on. + * + * This request is typically used when the server needs to understand the file system + * structure or access specific locations that the client has permission to read from. + */ + export interface ListRootsRequest extends Request { + method: "roots/list"; + } + + /** + * The client's response to a roots/list request from the server. + * This result contains an array of Root objects, each representing a root directory + * or file that the server can operate on. + */ + export interface ListRootsResult extends Result { + roots: Root[]; + } + + /** + * Represents a root directory or file that the server can operate on. + */ + export interface Root { + /** + * The URI identifying the root. This *must* start with file:// for now. + * This restriction may be relaxed in future versions of the protocol to allow + * other URI schemes. + * + * @format uri + */ + uri: string; + /** + * An optional name for the root. This can be used to provide a human-readable + * identifier for the root, which may be useful for display purposes or for + * referencing the root in other parts of the application. + */ + name?: string; + } + + /** + * A notification from the client to the server, informing it that the list of roots has changed. + * This notification should be sent whenever the client adds, removes, or modifies any root. + * The server should then request an updated list of roots using the ListRootsRequest. + */ + export interface RootsListChangedNotification extends Notification { + method: "notifications/roots/list_changed"; + } + + /* Client messages */ + export type ClientRequest = + | PingRequest + | InitializeRequest + | CompleteRequest + | SetLevelRequest + | GetPromptRequest + | ListPromptsRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscribeRequest + | UnsubscribeRequest + | CallToolRequest + | ListToolsRequest; + + export type ClientNotification = + | CancelledNotification + | ProgressNotification + | InitializedNotification + | RootsListChangedNotification; + + export type ClientResult = EmptyResult | CreateMessageResult | ListRootsResult; + + /* Server messages */ + export type ServerRequest = + | PingRequest + | CreateMessageRequest + | ListRootsRequest; + + export type ServerNotification = + | CancelledNotification + | ProgressNotification + | LoggingMessageNotification + | ResourceUpdatedNotification + | ResourceListChangedNotification + | ToolListChangedNotification + | PromptListChangedNotification; + + export type ServerResult = + | EmptyResult + | InitializeResult + | CompleteResult + | GetPromptResult + | ListPromptsResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | CallToolResult + | ListToolsResult; +} diff --git a/src/vs/workbench/contrib/mcp/electron-sandbox/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/electron-sandbox/mcp.contribution.ts new file mode 100644 index 00000000..efdf3ee1 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/electron-sandbox/mcp.contribution.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js'; +import { NativeMcpDiscovery } from './nativeMpcDiscovery.js'; + +mcpDiscoveryRegistry.register(new SyncDescriptor(NativeMcpDiscovery)); diff --git a/src/vs/workbench/contrib/mcp/electron-sandbox/nativeMpcDiscovery.ts b/src/vs/workbench/contrib/mcp/electron-sandbox/nativeMpcDiscovery.ts new file mode 100644 index 00000000..df53ead1 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/electron-sandbox/nativeMpcDiscovery.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { INativeMcpDiscoveryHelperService, NativeMcpDiscoveryHelperChannelName } from '../../../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; +import { NativeFilesystemMcpDiscovery } from '../common/discovery/nativeMcpDiscoveryAbstract.js'; +import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; + +export class NativeMcpDiscovery extends NativeFilesystemMcpDiscovery { + constructor( + @IMainProcessService private readonly mainProcess: IMainProcessService, + @ILogService private readonly logService: ILogService, + @ILabelService labelService: ILabelService, + @IFileService fileService: IFileService, + @IInstantiationService instantiationService: IInstantiationService, + @IMcpRegistry mcpRegistry: IMcpRegistry, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(null, labelService, fileService, instantiationService, mcpRegistry, configurationService); + } + + public override start(): void { + const service = ProxyChannel.toService( + this.mainProcess.getChannel(NativeMcpDiscoveryHelperChannelName)); + + service.load().then( + data => this.setDetails(data), + err => { + this.logService.warn('Error getting main process MCP environment', err); + this.setDetails(undefined); + } + ); + } +} diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts new file mode 100644 index 00000000..8f4cbb70 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts @@ -0,0 +1,566 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { timeout } from '../../../../../base/common/async.js'; +import { ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { upcast } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; +import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILogger, ILoggerService, NullLogger } from '../../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { ISecretStorageService } from '../../../../../platform/secrets/common/secrets.js'; +import { TestSecretStorageService } from '../../../../../platform/secrets/test/common/testSecretStorageService.js'; +import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { IConfigurationResolverService } from '../../../../services/configurationResolver/common/configurationResolver.js'; +import { IOutputService } from '../../../../services/output/common/output.js'; +import { TestLoggerService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { McpRegistry } from '../../common/mcpRegistry.js'; +import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js'; +import { McpServerConnection } from '../../common/mcpServerConnection.js'; +import { LazyCollectionState, McpCollectionDefinition, McpCollectionReference, McpServerDefinition, McpServerTransportType } from '../../common/mcpTypes.js'; +import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; +import { ConfigurationResolverExpression } from '../../../../services/configurationResolver/common/configurationResolverExpression.js'; + +class TestConfigurationResolverService implements Partial { + declare readonly _serviceBrand: undefined; + + private interactiveCounter = 0; + + // Used to simulate stored/resolved variables + private readonly resolvedVariables = new Map(); + + constructor() { + // Add some test variables + this.resolvedVariables.set('workspaceFolder', '/test/workspace'); + this.resolvedVariables.set('fileBasename', 'test.txt'); + } + + resolveAsync(folder: any, value: any): Promise { + const parsed = ConfigurationResolverExpression.parse(value); + for (const variable of parsed.unresolved()) { + const resolved = this.resolvedVariables.get(variable.inner); + if (resolved) { + parsed.resolve(variable, resolved); + } + } + + return Promise.resolve(parsed.toObject()); + } + + resolveWithInteraction(folder: any, config: any, section?: string, variables?: Record, target?: ConfigurationTarget): Promise | undefined> { + const parsed = ConfigurationResolverExpression.parse(config); + // For testing, we simulate interaction by returning a map with some variables + const result = new Map(); + result.set('input:testInteractive', `interactiveValue${this.interactiveCounter++}`); + result.set('command:testCommand', `commandOutput${this.interactiveCounter++}}`); + + // If variables are provided, include those too + for (const [k, v] of result.entries()) { + parsed.resolve({ id: '${' + k + '}' } as any, v); + } + + return Promise.resolve(result); + } +} + +class TestMcpHostDelegate implements IMcpHostDelegate { + priority = 0; + + canStart(): boolean { + return true; + } + + start(): IMcpMessageTransport { + return new TestMcpMessageTransport(); + } + + waitForInitialProviderPromises(): Promise { + return Promise.resolve(); + } +} + +class TestDialogService implements Partial { + declare readonly _serviceBrand: undefined; + + private _promptResult: boolean | undefined; + private _promptSpy: sinon.SinonStub; + + constructor() { + this._promptSpy = sinon.stub(); + this._promptSpy.callsFake(() => { + return Promise.resolve({ result: this._promptResult }); + }); + } + + setPromptResult(result: boolean | undefined): void { + this._promptResult = result; + } + + get promptSpy(): sinon.SinonStub { + return this._promptSpy; + } + + prompt(options: any): Promise { + return this._promptSpy(options); + } +} + +suite('Workbench - MCP - Registry', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let registry: McpRegistry; + let testStorageService: TestStorageService; + let testConfigResolverService: TestConfigurationResolverService; + let testDialogService: TestDialogService; + let testCollection: McpCollectionDefinition & { serverDefinitions: ISettableObservable }; + let baseDefinition: McpServerDefinition; + let logger: ILogger; + + setup(() => { + testConfigResolverService = new TestConfigurationResolverService(); + testStorageService = store.add(new TestStorageService()); + testDialogService = new TestDialogService(); + + const services = new ServiceCollection( + [IConfigurationResolverService, testConfigResolverService], + [IStorageService, testStorageService], + [ISecretStorageService, new TestSecretStorageService()], + [ILoggerService, store.add(new TestLoggerService())], + [IOutputService, upcast({ showChannel: () => { } })], + [IDialogService, testDialogService], + [IProductService, {}], + ); + + logger = new NullLogger(); + + const instaService = store.add(new TestInstantiationService(services)); + registry = store.add(instaService.createInstance(McpRegistry)); + + // Create test collection that can be reused + testCollection = { + id: 'test-collection', + label: 'Test Collection', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + // Create base definition that can be reused + baseDefinition = { + id: 'test-server', + label: 'Test Server', + launch: { + type: McpServerTransportType.Stdio, + command: 'test-command', + args: [], + env: {}, + envFile: undefined, + cwd: URI.parse('file:///test') + } + }; + }); + + test('registerCollection adds collection to registry', () => { + const disposable = registry.registerCollection(testCollection); + store.add(disposable); + + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.collections.get()[0], testCollection); + + disposable.dispose(); + assert.strictEqual(registry.collections.get().length, 0); + }); + + test('registerDelegate adds delegate to registry', () => { + const delegate = new TestMcpHostDelegate(); + const disposable = registry.registerDelegate(delegate); + store.add(disposable); + + assert.strictEqual(registry.delegates.length, 1); + assert.strictEqual(registry.delegates[0], delegate); + + disposable.dispose(); + assert.strictEqual(registry.delegates.length, 0); + }); + + test('resolveConnection creates connection with resolved variables and memorizes them until cleared', async () => { + const definition: McpServerDefinition = { + ...baseDefinition, + launch: { + type: McpServerTransportType.Stdio, + command: '${workspaceFolder}/cmd', + args: ['--file', '${fileBasename}'], + env: { + PATH: '${input:testInteractive}' + }, + envFile: undefined, + cwd: URI.parse('file:///test') + }, + variableReplacement: { + section: 'mcp', + target: ConfigurationTarget.WORKSPACE, + } + }; + + const delegate = new TestMcpHostDelegate(); + store.add(registry.registerDelegate(delegate)); + testCollection.serverDefinitions.set([definition], undefined); + store.add(registry.registerCollection(testCollection)); + + const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger }) as McpServerConnection; + + assert.ok(connection); + assert.strictEqual(connection.definition, definition); + assert.strictEqual((connection.launchDefinition as any).command, '/test/workspace/cmd'); + assert.strictEqual((connection.launchDefinition as any).env.PATH, 'interactiveValue0'); + connection.dispose(); + + const connection2 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger }) as McpServerConnection; + + assert.ok(connection2); + assert.strictEqual((connection2.launchDefinition as any).env.PATH, 'interactiveValue0'); + connection2.dispose(); + + registry.clearSavedInputs(StorageScope.WORKSPACE); + + const connection3 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger }) as McpServerConnection; + + assert.ok(connection3); + assert.strictEqual((connection3.launchDefinition as any).env.PATH, 'interactiveValue4'); + connection3.dispose(); + }); + + suite('Trust Management', () => { + setup(() => { + const delegate = new TestMcpHostDelegate(); + store.add(registry.registerDelegate(delegate)); + }); + + test('resolveConnection connects to server when trusted by default', async () => { + const definition = { ...baseDefinition }; + store.add(registry.registerCollection(testCollection)); + testCollection.serverDefinitions.set([definition], undefined); + + const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger }); + + assert.ok(connection); + assert.strictEqual(testDialogService.promptSpy.called, false); + connection?.dispose(); + }); + + test('resolveConnection prompts for confirmation when not trusted by default', async () => { + const untrustedCollection: McpCollectionDefinition = { + ...testCollection, + isTrustedByDefault: false + }; + + const definition = { ...baseDefinition }; + store.add(registry.registerCollection(untrustedCollection)); + testCollection.serverDefinitions.set([definition], undefined); + + testDialogService.setPromptResult(true); + + const connection = await registry.resolveConnection({ + logger, + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.ok(connection); + assert.strictEqual(testDialogService.promptSpy.called, true); + connection?.dispose(); + + testDialogService.promptSpy.resetHistory(); + const connection2 = await registry.resolveConnection({ + logger, + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.ok(connection2); + assert.strictEqual(testDialogService.promptSpy.called, false); + connection2?.dispose(); + }); + + test('resolveConnection returns undefined when user does not trust the server', async () => { + const untrustedCollection: McpCollectionDefinition = { + ...testCollection, + isTrustedByDefault: false + }; + + const definition = { ...baseDefinition }; + store.add(registry.registerCollection(untrustedCollection)); + testCollection.serverDefinitions.set([definition], undefined); + + testDialogService.setPromptResult(false); + + const connection = await registry.resolveConnection({ + logger, + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.strictEqual(connection, undefined); + assert.strictEqual(testDialogService.promptSpy.called, true); + + testDialogService.promptSpy.resetHistory(); + const connection2 = await registry.resolveConnection({ + logger, + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.strictEqual(connection2, undefined); + assert.strictEqual(testDialogService.promptSpy.called, false); + }); + + test('resolveConnection honors forceTrust parameter', async () => { + const untrustedCollection: McpCollectionDefinition = { + ...testCollection, + isTrustedByDefault: false + }; + + const definition = { ...baseDefinition }; + store.add(registry.registerCollection(untrustedCollection)); + testCollection.serverDefinitions.set([definition], undefined); + + testDialogService.setPromptResult(false); + + const connection1 = await registry.resolveConnection({ + logger, + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.strictEqual(connection1, undefined); + + testDialogService.promptSpy.resetHistory(); + testDialogService.setPromptResult(true); + + const connection2 = await registry.resolveConnection({ + logger, + collectionRef: untrustedCollection, + definitionRef: definition, + forceTrust: true + }); + + assert.ok(connection2); + assert.strictEqual(testDialogService.promptSpy.called, true); + connection2?.dispose(); + + testDialogService.promptSpy.resetHistory(); + const connection3 = await registry.resolveConnection({ + logger, + collectionRef: untrustedCollection, + definitionRef: definition + }); + + assert.ok(connection3); + assert.strictEqual(testDialogService.promptSpy.called, false); + connection3?.dispose(); + }); + }); + + suite('Lazy Collections', () => { + let lazyCollection: McpCollectionDefinition; + let normalCollection: McpCollectionDefinition; + let removedCalled: boolean; + + setup(() => { + removedCalled = false; + lazyCollection = { + ...testCollection, + id: 'lazy-collection', + lazy: { + isCached: false, + load: () => Promise.resolve(), + removed: () => { removedCalled = true; } + } + }; + normalCollection = { + ...testCollection, + id: 'lazy-collection', + serverDefinitions: observableValue('serverDefs', [baseDefinition]) + }; + }); + + test('registers lazy collection', () => { + const disposable = registry.registerCollection(lazyCollection); + store.add(disposable); + + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.collections.get()[0], lazyCollection); + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.HasUnknown); + }); + + test('lazy collection is replaced by normal collection', () => { + store.add(registry.registerCollection(lazyCollection)); + store.add(registry.registerCollection(normalCollection)); + + const collections = registry.collections.get(); + assert.strictEqual(collections.length, 1); + assert.strictEqual(collections[0], normalCollection); + assert.strictEqual(collections[0].lazy, undefined); + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.AllKnown); + }); + + test('lazyCollectionState updates correctly during loading', async () => { + lazyCollection = { + ...lazyCollection, + lazy: { + ...lazyCollection.lazy!, + load: async () => { + await timeout(0); + store.add(registry.registerCollection(normalCollection)); + return Promise.resolve(); + } + } + }; + + store.add(registry.registerCollection(lazyCollection)); + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.HasUnknown); + + const loadingPromise = registry.discoverCollections(); + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.LoadingUnknown); + + await loadingPromise; + + // The collection wasn't replaced, so it should be removed + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.AllKnown); + assert.strictEqual(removedCalled, false); + }); + + test('removed callback is called when lazy collection is not replaced', async () => { + store.add(registry.registerCollection(lazyCollection)); + await registry.discoverCollections(); + + assert.strictEqual(removedCalled, true); + }); + + test('cached lazy collections are tracked correctly', () => { + lazyCollection.lazy!.isCached = true; + store.add(registry.registerCollection(lazyCollection)); + + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.AllKnown); + + // Adding an uncached lazy collection changes the state + const uncachedLazy = { + ...lazyCollection, + id: 'uncached-lazy', + lazy: { + ...lazyCollection.lazy!, + isCached: false + } + }; + store.add(registry.registerCollection(uncachedLazy)); + + assert.strictEqual(registry.lazyCollectionState.get(), LazyCollectionState.HasUnknown); + }); + }); + + suite('Collection Tool Prefixes', () => { + test('assigns unique prefixes to collections', () => { + const collection1: McpCollectionDefinition = { + id: 'collection1', + label: 'Collection 1', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + const collection2: McpCollectionDefinition = { + id: 'collection2', + label: 'Collection 2', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + store.add(registry.registerCollection(collection1)); + store.add(registry.registerCollection(collection2)); + + const prefix1 = registry.collectionToolPrefix(collection1).get(); + const prefix2 = registry.collectionToolPrefix(collection2).get(); + + assert.notStrictEqual(prefix1, prefix2); + assert.ok(/^[a-f0-9]{3}\.$/.test(prefix1)); + assert.ok(/^[a-f0-9]{3}\.$/.test(prefix2)); + }); + + test('handles hash collisions by incrementing view', () => { + // These strings are known to have SHA1 hash collisions in their first 3 characters + const collection1: McpCollectionDefinition = { + id: 'potato', + label: 'Collection 1', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + const collection2: McpCollectionDefinition = { + id: 'candidate_83048', + label: 'Collection 2', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + store.add(registry.registerCollection(collection1)); + store.add(registry.registerCollection(collection2)); + + const prefix1 = registry.collectionToolPrefix(collection1).get(); + const prefix2 = registry.collectionToolPrefix(collection2).get(); + + assert.notStrictEqual(prefix1, prefix2); + assert.ok(/^[a-f0-9]{3}\.$/.test(prefix1)); + assert.ok(/^[a-f0-9]{3}\.$/.test(prefix2)); + }); + + test('prefix changes when collections change', () => { + const collection1: McpCollectionDefinition = { + id: 'collection1', + label: 'Collection 1', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + const disposable = registry.registerCollection(collection1); + store.add(disposable); + + const prefix1 = registry.collectionToolPrefix(collection1).get(); + assert.ok(!!prefix1); + + disposable.dispose(); + + const prefix2 = registry.collectionToolPrefix(collection1).get(); + + assert.strictEqual(prefix2, ''); + }); + + test('prefix is empty for unknown collections', () => { + const unknownCollection: McpCollectionReference = { + id: 'unknown', + label: 'Unknown' + }; + + const prefix = registry.collectionToolPrefix(unknownCollection).get(); + assert.strictEqual(prefix, ''); + }); + }); +}); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistryInputStorage.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryInputStorage.test.ts new file mode 100644 index 00000000..36cc0ca4 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryInputStorage.test.ts @@ -0,0 +1,178 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; +import { TestSecretStorageService } from '../../../../../platform/secrets/test/common/testSecretStorageService.js'; +import { StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { McpRegistryInputStorage } from '../../common/mcpRegistryInputStorage.js'; + +suite('Workbench - MCP - RegistryInputStorage', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let testStorageService: TestStorageService; + let testSecretStorageService: TestSecretStorageService; + let testLogService: ILogService; + let mcpInputStorage: McpRegistryInputStorage; + + setup(() => { + testStorageService = store.add(new TestStorageService()); + testSecretStorageService = new TestSecretStorageService(); + testLogService = store.add(new NullLogService()); + + // Create the input storage with APPLICATION scope + mcpInputStorage = store.add(new McpRegistryInputStorage( + StorageScope.APPLICATION, + StorageTarget.MACHINE, + testStorageService, + testSecretStorageService, + testLogService + )); + }); + + test('setPlainText stores values that can be retrieved with getMap', async () => { + const values = { + 'key1': { value: 'value1' }, + 'key2': { value: 'value2' } + }; + + await mcpInputStorage.setPlainText(values); + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.key1.value, 'value1'); + assert.strictEqual(result.key2.value, 'value2'); + }); + + test('setSecrets stores encrypted values that can be retrieved with getMap', async () => { + const secrets = { + 'secretKey1': { value: 'secretValue1' }, + 'secretKey2': { value: 'secretValue2' } + }; + + await mcpInputStorage.setSecrets(secrets); + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.secretKey1.value, 'secretValue1'); + assert.strictEqual(result.secretKey2.value, 'secretValue2'); + }); + + test('getMap returns combined plain text and secret values', async () => { + await mcpInputStorage.setPlainText({ + 'plainKey': { value: 'plainValue' } + }); + + await mcpInputStorage.setSecrets({ + 'secretKey': { value: 'secretValue' } + }); + + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.plainKey.value, 'plainValue'); + assert.strictEqual(result.secretKey.value, 'secretValue'); + }); + + test('clear removes specific values', async () => { + await mcpInputStorage.setPlainText({ + 'key1': { value: 'value1' }, + 'key2': { value: 'value2' } + }); + + await mcpInputStorage.setSecrets({ + 'secretKey1': { value: 'secretValue1' }, + 'secretKey2': { value: 'secretValue2' } + }); + + // Clear one plain and one secret value + await mcpInputStorage.clear('key1'); + await mcpInputStorage.clear('secretKey1'); + + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.key1, undefined); + assert.strictEqual(result.key2.value, 'value2'); + assert.strictEqual(result.secretKey1, undefined); + assert.strictEqual(result.secretKey2.value, 'secretValue2'); + }); + + test('clearAll removes all values', async () => { + await mcpInputStorage.setPlainText({ + 'key1': { value: 'value1' } + }); + + await mcpInputStorage.setSecrets({ + 'secretKey1': { value: 'secretValue1' } + }); + + mcpInputStorage.clearAll(); + + const result = await mcpInputStorage.getMap(); + + assert.deepStrictEqual(result, {}); + }); + + test('updates to plain text values overwrite existing values', async () => { + await mcpInputStorage.setPlainText({ + 'key1': { value: 'value1' }, + 'key2': { value: 'value2' } + }); + + await mcpInputStorage.setPlainText({ + 'key1': { value: 'updatedValue1' } + }); + + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.key1.value, 'updatedValue1'); + assert.strictEqual(result.key2.value, 'value2'); + }); + + test('updates to secret values overwrite existing values', async () => { + await mcpInputStorage.setSecrets({ + 'secretKey1': { value: 'secretValue1' }, + 'secretKey2': { value: 'secretValue2' } + }); + + await mcpInputStorage.setSecrets({ + 'secretKey1': { value: 'updatedSecretValue1' } + }); + + const result = await mcpInputStorage.getMap(); + + assert.strictEqual(result.secretKey1.value, 'updatedSecretValue1'); + assert.strictEqual(result.secretKey2.value, 'secretValue2'); + }); + + test('storage persists values across instances', async () => { + // Set values on first instance + await mcpInputStorage.setPlainText({ + 'key1': { value: 'value1' } + }); + + await mcpInputStorage.setSecrets({ + 'secretKey1': { value: 'secretValue1' } + }); + + await testStorageService.flush(); + + // Create a second instance that should have access to the same storage + const secondInstance = store.add(new McpRegistryInputStorage( + StorageScope.APPLICATION, + StorageTarget.MACHINE, + testStorageService, + testSecretStorageService, + testLogService + )); + + const result = await secondInstance.getMap(); + + assert.strictEqual(result.key1.value, 'value1'); + assert.strictEqual(result.secretKey1.value, 'secretValue1'); + + assert.ok(!testStorageService.get('mcpInputs', StorageScope.APPLICATION)?.includes('secretValue1')); + }); +}); + diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts new file mode 100644 index 00000000..36ce1367 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from '../../../../../base/common/event.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../base/common/observable.js'; +import { LogLevel } from '../../../../../platform/log/common/log.js'; +import { IMcpMessageTransport } from '../../common/mcpRegistryTypes.js'; +import { McpConnectionState } from '../../common/mcpTypes.js'; +import { MCP } from '../../common/modelContextProtocol.js'; + +/** + * Implementation of IMcpMessageTransport for testing purposes. + * Allows tests to easily send/receive messages and control the connection state. + */ +export class TestMcpMessageTransport extends Disposable implements IMcpMessageTransport { + private readonly _onDidLog = this._register(new Emitter<{ level: LogLevel; message: string }>()); + public readonly onDidLog = this._onDidLog.event; + + private readonly _onDidReceiveMessage = this._register(new Emitter()); + public readonly onDidReceiveMessage = this._onDidReceiveMessage.event; + + private readonly _stateValue = observableValue('testTransportState', { state: McpConnectionState.Kind.Starting }); + public readonly state = this._stateValue; + + private readonly _sentMessages: MCP.JSONRPCMessage[] = []; + + constructor() { + super(); + } + + /** + * Send a message through the transport. + */ + public send(message: MCP.JSONRPCMessage): void { + this._sentMessages.push(message); + } + + /** + * Stop the transport. + */ + public stop(): void { + this._stateValue.set({ state: McpConnectionState.Kind.Stopped }, undefined); + } + + // Test Helper Methods + + /** + * Simulate receiving a message from the server. + */ + public simulateReceiveMessage(message: MCP.JSONRPCMessage): void { + this._onDidReceiveMessage.fire(message); + } + + /** + * Simulates a reply to an 'initialized' request. + */ + public simulateInitialized() { + if (!this._sentMessages.length) { + throw new Error('initialize was not called yet'); + } + + this.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: (this.getSentMessages()[0] as MCP.JSONRPCRequest).id, + result: { + protocolVersion: MCP.LATEST_PROTOCOL_VERSION, + capabilities: { + tools: {}, + }, + serverInfo: { + name: 'Test Server', + version: '1.0.0' + }, + } satisfies MCP.InitializeResult + }); + } + + /** + * Simulate a log event. + */ + public simulateLog(message: string): void { + this._onDidLog.fire({ level: LogLevel.Info, message }); + } + + /** + * Set the connection state. + */ + public setConnectionState(state: McpConnectionState): void { + this._stateValue.set(state, undefined); + } + + /** + * Get all messages that have been sent. + */ + public getSentMessages(): readonly MCP.JSONRPCMessage[] { + return [...this._sentMessages]; + } + + /** + * Clear the sent messages history. + */ + public clearSentMessages(): void { + this._sentMessages.length = 0; + } +} diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts new file mode 100644 index 00000000..bcc62930 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts @@ -0,0 +1,389 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { timeout } from '../../../../../base/common/async.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, observableValue } from '../../../../../base/common/observable.js'; +import { upcast } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILogger, ILoggerService, LogLevel, NullLogger } from '../../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { IOutputService } from '../../../../services/output/common/output.js'; +import { TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js'; +import { McpServerConnection } from '../../common/mcpServerConnection.js'; +import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerTransportType } from '../../common/mcpTypes.js'; +import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; + +class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate { + private readonly _transport: TestMcpMessageTransport; + private _canStartValue = true; + + priority = 0; + + constructor() { + super(); + this._transport = this._register(new TestMcpMessageTransport()); + } + + canStart(): boolean { + return this._canStartValue; + } + + start(): IMcpMessageTransport { + if (!this._canStartValue) { + throw new Error('Cannot start server'); + } + return this._transport; + } + + getTransport(): TestMcpMessageTransport { + return this._transport; + } + + setCanStart(value: boolean): void { + this._canStartValue = value; + } + + waitForInitialProviderPromises(): Promise { + return Promise.resolve(); + } +} + +suite('Workbench - MCP - ServerConnection', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + let delegate: TestMcpHostDelegate; + let transport: TestMcpMessageTransport; + let collection: McpCollectionDefinition; + let serverDefinition: McpServerDefinition; + + setup(() => { + delegate = store.add(new TestMcpHostDelegate()); + transport = delegate.getTransport(); + + // Setup test services + const services = new ServiceCollection( + [ILoggerService, store.add(new TestLoggerService())], + [IOutputService, upcast({ showChannel: () => { } })], + [IStorageService, store.add(new TestStorageService())], + [IProductService, TestProductService], + ); + + instantiationService = store.add(new TestInstantiationService(services)); + + // Create test collection + collection = { + id: 'test-collection', + label: 'Test Collection', + remoteAuthority: null, + serverDefinitions: observableValue('serverDefs', []), + isTrustedByDefault: true, + scope: StorageScope.APPLICATION + }; + + // Create server definition + serverDefinition = { + id: 'test-server', + label: 'Test Server', + launch: { + type: McpServerTransportType.Stdio, + command: 'test-command', + args: [], + env: {}, + envFile: undefined, + cwd: URI.parse('file:///test') + } + }; + }); + + function waitForHandler(cnx: McpServerConnection) { + const handler = cnx.handler.get(); + if (handler) { + return Promise.resolve(handler); + } + + return new Promise(resolve => { + const disposable = autorun(reader => { + const handler = cnx.handler.read(reader); + if (handler) { + disposable.dispose(); + resolve(handler); + } + }); + }); + } + + test('should start and set state to Running when transport succeeds', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch, + new NullLogger(), + ); + store.add(connection); + + // Start the connection + const startPromise = connection.start(); + + // Simulate successful connection + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + + const state = await startPromise; + assert.strictEqual(state.state, McpConnectionState.Kind.Running); + + transport.simulateInitialized(); + assert.ok(await waitForHandler(connection)); + }); + + test('should handle errors during start', async () => { + // Setup delegate to fail on start + delegate.setCanStart(false); + + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch, + new NullLogger(), + ); + store.add(connection); + + // Start the connection + const state = await connection.start(); + + assert.strictEqual(state.state, McpConnectionState.Kind.Error); + assert.ok(state.message); + }); + + test('should handle transport errors', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch, + new NullLogger(), + ); + store.add(connection); + + // Start the connection + const startPromise = connection.start(); + + // Simulate error in transport + transport.setConnectionState({ + state: McpConnectionState.Kind.Error, + message: 'Test error message' + }); + + const state = await startPromise; + assert.strictEqual(state.state, McpConnectionState.Kind.Error); + assert.strictEqual(state.message, 'Test error message'); + }); + + test('should stop and set state to Stopped', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch, + new NullLogger(), + ); + store.add(connection); + + // Start the connection + const startPromise = connection.start(); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + await startPromise; + + // Stop the connection + const stopPromise = connection.stop(); + await stopPromise; + + assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped); + }); + + test('should not restart if already starting', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch, + new NullLogger(), + ); + store.add(connection); + + // Start the connection + const startPromise1 = connection.start(); + + // Try to start again while starting + const startPromise2 = connection.start(); + + // Simulate successful connection + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + + const state1 = await startPromise1; + const state2 = await startPromise2; + + // Both promises should resolve to the same state + assert.strictEqual(state1.state, McpConnectionState.Kind.Running); + assert.strictEqual(state2.state, McpConnectionState.Kind.Running); + + transport.simulateInitialized(); + assert.ok(await waitForHandler(connection)); + + connection.dispose(); + }); + + test('should clean up when disposed', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch, + new NullLogger(), + ); + + // Start the connection + const startPromise = connection.start(); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + await startPromise; + + // Dispose the connection + connection.dispose(); + + assert.strictEqual(connection.state.get().state, McpConnectionState.Kind.Stopped); + }); + + test('should log transport messages', async () => { + // Track logged messages + const loggedMessages: string[] = []; + + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch, + { + getLevel: () => LogLevel.Debug, + info: (message: string) => { + loggedMessages.push(message); + }, + error: () => { }, + dispose: () => { } + } as Partial as ILogger, + ); + store.add(connection); + + // Start the connection + const startPromise = connection.start(); + + // Simulate log message from transport + transport.simulateLog('Test log message'); + + // Set connection to running + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + await startPromise; + + // Check that the message was logged + assert.ok(loggedMessages.some(msg => msg === 'Test log message')); + + connection.dispose(); + await timeout(10); + }); + + test('should correctly handle transitions to and from error state', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch, + new NullLogger(), + ); + store.add(connection); + + // Start the connection + const startPromise = connection.start(); + + // Transition to error state + const errorState: McpConnectionState = { + state: McpConnectionState.Kind.Error, + message: 'Temporary error' + }; + transport.setConnectionState(errorState); + + let state = await startPromise; + assert.equal(state, errorState); + + + transport.setConnectionState({ state: McpConnectionState.Kind.Stopped }); + + // Transition back to running state + const startPromise2 = connection.start(); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + state = await startPromise2; + assert.deepStrictEqual(state, { state: McpConnectionState.Kind.Running }); + + connection.dispose(); + await timeout(10); + }); + + test('should handle multiple start/stop cycles', async () => { + // Create server connection + const connection = instantiationService.createInstance( + McpServerConnection, + collection, + serverDefinition, + delegate, + serverDefinition.launch, + new NullLogger(), + ); + store.add(connection); + + // First cycle + let startPromise = connection.start(); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + await startPromise; + + await connection.stop(); + assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Stopped }); + + // Second cycle + startPromise = connection.start(); + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + await startPromise; + + assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Running }); + + await connection.stop(); + + assert.deepStrictEqual(connection.state.get(), { state: McpConnectionState.Kind.Stopped }); + + connection.dispose(); + await timeout(10); + }); +}); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts new file mode 100644 index 00000000..205465a0 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts @@ -0,0 +1,398 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { upcast } from '../../../../../base/common/types.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILoggerService } from '../../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { IStorageService } from '../../../../../platform/storage/common/storage.js'; +import { TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { IMcpHostDelegate } from '../../common/mcpRegistryTypes.js'; +import { McpServerRequestHandler } from '../../common/mcpServerRequestHandler.js'; +import { McpConnectionState } from '../../common/mcpTypes.js'; +import { MCP } from '../../common/modelContextProtocol.js'; +import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; +import { IOutputService } from '../../../../services/output/common/output.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; + +class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate { + private readonly _transport: TestMcpMessageTransport; + + priority = 0; + + constructor() { + super(); + this._transport = this._register(new TestMcpMessageTransport()); + } + + canStart(): boolean { + return true; + } + + start(): TestMcpMessageTransport { + return this._transport; + } + + getTransport(): TestMcpMessageTransport { + return this._transport; + } + + waitForInitialProviderPromises(): Promise { + return Promise.resolve(); + } +} + +suite('Workbench - MCP - ServerRequestHandler', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + let delegate: TestMcpHostDelegate; + let transport: TestMcpMessageTransport; + let handler: McpServerRequestHandler; + let cts: CancellationTokenSource; + + setup(async () => { + delegate = store.add(new TestMcpHostDelegate()); + transport = delegate.getTransport(); + cts = store.add(new CancellationTokenSource()); + + // Setup test services + const services = new ServiceCollection( + [ILoggerService, store.add(new TestLoggerService())], + [IOutputService, upcast({ showChannel: () => { } })], + [IStorageService, store.add(new TestStorageService())], + [IProductService, TestProductService], + ); + + instantiationService = store.add(new TestInstantiationService(services)); + + transport.setConnectionState({ state: McpConnectionState.Kind.Running }); + + // Manually create the handler since we need the transport already set up + const logger = store.add((instantiationService.get(ILoggerService) as TestLoggerService) + .createLogger('mcpServerTest', { hidden: true, name: 'MCP Test' })); + + // Start the handler creation + const handlerPromise = McpServerRequestHandler.create(instantiationService, transport, logger, cts.token); + + // Simulate successful initialization + // We need to respond to the initialize request that the handler will make + transport.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: 1, // The handler uses 1 for the first request + result: { + protocolVersion: MCP.LATEST_PROTOCOL_VERSION, + serverInfo: { + name: 'Test MCP Server', + version: '1.0.0', + }, + capabilities: { + resources: { + supportedTypes: ['text/plain'], + }, + tools: { + supportsCancellation: true, + } + } + } + }); + + handler = await handlerPromise; + store.add(handler); + }); + + test('should send and receive JSON-RPC requests', async () => { + // Setup request + const requestPromise = handler.listResources(); + + // Get the sent message and verify it + const sentMessages = transport.getSentMessages(); + assert.strictEqual(sentMessages.length, 3); // initialize + listResources + + // Verify listResources request format + const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest; + assert.strictEqual(listResourcesRequest.method, 'resources/list'); + assert.strictEqual(listResourcesRequest.jsonrpc, MCP.JSONRPC_VERSION); + assert.ok(typeof listResourcesRequest.id === 'number'); + + // Simulate server response with mock resources that match the expected Resource interface + transport.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: listResourcesRequest.id, + result: { + resources: [ + { uri: 'resource1', type: 'text/plain', name: 'Test Resource 1' }, + { uri: 'resource2', type: 'text/plain', name: 'Test Resource 2' } + ] + } + }); + + // Verify the result + const resources = await requestPromise; + assert.strictEqual(resources.length, 2); + assert.strictEqual(resources[0].uri, 'resource1'); + assert.strictEqual(resources[1].name, 'Test Resource 2'); + }); + + test('should handle paginated requests', async () => { + // Setup request + const requestPromise = handler.listResources(); + + // Get the first request and respond with pagination + const sentMessages = transport.getSentMessages(); + const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest; + + // Send first page with nextCursor + transport.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: listResourcesRequest.id, + result: { + resources: [ + { uri: 'resource1', type: 'text/plain', name: 'Test Resource 1' } + ], + nextCursor: 'page2' + } + }); + + // Clear the sent messages to only capture the next page request + transport.clearSentMessages(); + + // Wait a bit to allow the handler to process and send the next request + await new Promise(resolve => setTimeout(resolve, 0)); + + // Get the second request and verify cursor is included + const sentMessages2 = transport.getSentMessages(); + assert.strictEqual(sentMessages2.length, 1); + + const listResourcesRequest2 = sentMessages2[0] as MCP.JSONRPCRequest; + assert.strictEqual(listResourcesRequest2.method, 'resources/list'); + assert.deepStrictEqual(listResourcesRequest2.params, { cursor: 'page2' }); + + // Send final page with no nextCursor + transport.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: listResourcesRequest2.id, + result: { + resources: [ + { uri: 'resource2', type: 'text/plain', name: 'Test Resource 2' } + ] + } + }); + + // Verify the combined result + const resources = await requestPromise; + assert.strictEqual(resources.length, 2); + assert.strictEqual(resources[0].uri, 'resource1'); + assert.strictEqual(resources[1].uri, 'resource2'); + }); + + test('should handle error responses', async () => { + // Setup request + const requestPromise = handler.readResource({ uri: 'non-existent' }); + + // Get the sent message + const sentMessages = transport.getSentMessages(); + const readResourceRequest = sentMessages[2] as MCP.JSONRPCRequest; // [0] is initialize + + // Simulate error response + transport.simulateReceiveMessage({ + jsonrpc: MCP.JSONRPC_VERSION, + id: readResourceRequest.id, + error: { + code: MCP.METHOD_NOT_FOUND, + message: 'Resource not found' + } + }); + + // Verify the error is thrown correctly + try { + await requestPromise; + assert.fail('Expected error was not thrown'); + } catch (e: any) { + assert.strictEqual(e.message, 'MPC -32601: Resource not found'); + assert.strictEqual(e.code, MCP.METHOD_NOT_FOUND); + } + }); + + test('should handle server requests', async () => { + // Simulate ping request from server + const pingRequest: MCP.JSONRPCRequest & MCP.PingRequest = { + jsonrpc: MCP.JSONRPC_VERSION, + id: 100, + method: 'ping' + }; + + transport.simulateReceiveMessage(pingRequest); + + // The handler should have sent a response + const sentMessages = transport.getSentMessages(); + const pingResponse = sentMessages.find(m => + 'id' in m && m.id === pingRequest.id && 'result' in m + ) as MCP.JSONRPCResponse; + + assert.ok(pingResponse, 'No ping response was sent'); + assert.deepStrictEqual(pingResponse.result, {}); + }); + + test('should handle roots list requests', async () => { + // Set roots + handler.roots = [ + { uri: 'file:///test/root1', name: 'Root 1' }, + { uri: 'file:///test/root2', name: 'Root 2' } + ]; + + // Simulate roots/list request from server + const rootsRequest: MCP.JSONRPCRequest & MCP.ListRootsRequest = { + jsonrpc: MCP.JSONRPC_VERSION, + id: 101, + method: 'roots/list' + }; + + transport.simulateReceiveMessage(rootsRequest); + + // The handler should have sent a response + const sentMessages = transport.getSentMessages(); + const rootsResponse = sentMessages.find(m => + 'id' in m && m.id === rootsRequest.id && 'result' in m + ) as MCP.JSONRPCResponse; + + assert.ok(rootsResponse, 'No roots/list response was sent'); + assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots.length, 2); + assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots[0].uri, 'file:///test/root1'); + }); + + test('should handle server notifications', async () => { + let progressNotificationReceived = false; + store.add(handler.onDidReceiveProgressNotification(notification => { + progressNotificationReceived = true; + assert.strictEqual(notification.method, 'notifications/progress'); + assert.strictEqual(notification.params.progressToken, 'token1'); + assert.strictEqual(notification.params.progress, 50); + })); + + // Simulate progress notification with correct format + const progressNotification: MCP.JSONRPCNotification & MCP.ProgressNotification = { + jsonrpc: MCP.JSONRPC_VERSION, + method: 'notifications/progress', + params: { + progressToken: 'token1', + progress: 50, + total: 100 + } + }; + + transport.simulateReceiveMessage(progressNotification); + assert.strictEqual(progressNotificationReceived, true); + }); + + test('should handle cancellation', async () => { + // Setup a new cancellation token source for this specific test + const testCts = store.add(new CancellationTokenSource()); + const requestPromise = handler.listResources(undefined, testCts.token); + + // Get the request ID + const sentMessages = transport.getSentMessages(); + const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest; + const requestId = listResourcesRequest.id; + + // Cancel the request + testCts.cancel(); + + // Check that a cancellation notification was sent + const cancelNotification = transport.getSentMessages().find(m => + !('id' in m) && + 'method' in m && + m.method === 'notifications/cancelled' && + 'params' in m && + m.params && m.params.requestId === requestId + ); + + assert.ok(cancelNotification, 'No cancellation notification was sent'); + + // Verify the promise was cancelled + try { + await requestPromise; + assert.fail('Promise should have been cancelled'); + } catch (e) { + assert.strictEqual(e.name, 'Canceled'); + } + }); + + test('should handle cancelled notification from server', async () => { + // Setup request + const requestPromise = handler.listResources(); + + // Get the request ID + const sentMessages = transport.getSentMessages(); + const listResourcesRequest = sentMessages[2] as MCP.JSONRPCRequest; + const requestId = listResourcesRequest.id; + + // Simulate cancelled notification from server + const cancelledNotification: MCP.JSONRPCNotification & MCP.CancelledNotification = { + jsonrpc: MCP.JSONRPC_VERSION, + method: 'notifications/cancelled', + params: { + requestId + } + }; + + transport.simulateReceiveMessage(cancelledNotification); + + // Verify the promise was cancelled + try { + await requestPromise; + assert.fail('Promise should have been cancelled'); + } catch (e) { + assert.strictEqual(e.name, 'Canceled'); + } + }); + + test('should dispose properly and cancel pending requests', async () => { + // Setup multiple requests + const request1 = handler.listResources(); + const request2 = handler.listTools(); + + // Dispose the handler + handler.dispose(); + + // Verify all promises were cancelled + try { + await request1; + assert.fail('Promise 1 should have been cancelled'); + } catch (e) { + assert.strictEqual(e.name, 'Canceled'); + } + + try { + await request2; + assert.fail('Promise 2 should have been cancelled'); + } catch (e) { + assert.strictEqual(e.name, 'Canceled'); + } + }); + + test('should handle connection error by cancelling requests', async () => { + // Setup request + const requestPromise = handler.listResources(); + + // Simulate connection error + transport.setConnectionState({ + state: McpConnectionState.Kind.Error, + message: 'Connection lost' + }); + + // Verify the promise was cancelled + try { + await requestPromise; + assert.fail('Promise should have been cancelled'); + } catch (e) { + assert.strictEqual(e.name, 'Canceled'); + } + }); +}); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts index c0f8bdf3..bbf7bf34 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution.ts @@ -22,6 +22,8 @@ import { MergeEditorInput } from './mergeEditorInput.js'; import { MergeEditor, MergeEditorOpenHandlerContribution, MergeEditorResolverContribution } from './view/mergeEditor.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { MergeEditorSerializer } from './mergeEditorSerializer.js'; +import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { MergeEditorAccessibilityHelpProvider } from './mergeEditorAccessibilityHelp.js'; Registry.as(EditorExtensions.EditorPane).registerEditorPane( EditorPaneDescriptor.create( @@ -95,3 +97,5 @@ Registry .registerWorkbenchContribution(MergeEditorOpenHandlerContribution, LifecyclePhase.Restored); registerWorkbenchContribution2(MergeEditorResolverContribution.ID, MergeEditorResolverContribution, WorkbenchPhase.BlockStartup /* only registers an editor resolver */); + +AccessibleViewRegistry.register(new MergeEditorAccessibilityHelpProvider()); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts new file mode 100644 index 00000000..2ead17be --- /dev/null +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorAccessibilityHelp.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { localize } from '../../../../nls.js'; +import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType } from '../../../../platform/accessibility/browser/accessibleView.js'; +import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { ContextKeyEqualsExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; + + +export class MergeEditorAccessibilityHelpProvider implements IAccessibleViewImplementation { + readonly name = 'mergeEditor'; + readonly type = AccessibleViewType.Help; + readonly priority = 125; + readonly when = ContextKeyEqualsExpr.create('isMergeEditor', true); + getProvider(accessor: ServicesAccessor) { + const codeEditorService = accessor.get(ICodeEditorService); + + const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!codeEditor) { + return; + } + + const content = [ + localize('msg1', "You are in a merge editor."), + localize('msg2', "Navigate between merge conflicts using the commands Go to Next Unhandled Conflict{0} and Go to Previous Unhandled Conflict{1}.", '', ''), + localize('msg3', "Run the command Merge Editor: Accept All Changes from the Left{0} and Merge Editor: Accept All Changes from the Right{1}", '', ''), + ]; + + return new AccessibleContentProvider( + AccessibleViewProviderId.MergeEditor, + { type: AccessibleViewType.Help }, + () => content.join('\n'), + () => codeEditor.focus(), + AccessibilityVerbositySettingId.MergeEditor, + ); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts index c0c4acc6..a110faf6 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { assertFn } from '../../../../base/common/assert.js'; -import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { BugIndicatingError, onUnexpectedError } from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; -import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, IDisposable, IReference } from '../../../../base/common/lifecycle.js'; import { derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../base/common/resources.js'; import Severity from '../../../../base/common/severity.js'; @@ -27,6 +27,8 @@ import { MergeEditorTelemetry } from './telemetry.js'; import { StorageCloseWithConflicts } from '../common/mergeEditor.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ITextFileEditorModel, ITextFileSaveOptions, ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; export interface MergeEditorArgs { base: URI; @@ -273,6 +275,8 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITextModelService private readonly _textModelService: ITextModelService, @ITextFileService private readonly textFileService: ITextFileService, + @IModelService private readonly _modelService: IModelService, + @ILanguageService private readonly _languageService: ILanguageService, ) { } @@ -292,18 +296,33 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa modelListener.add(this.textFileService.files.onDidCreate(handleDidCreate)); this.textFileService.files.models.forEach(handleDidCreate); - const [ + let [ base, result, input1Data, input2Data, ] = await Promise.all([ - this._textModelService.createModelReference(args.base), + this._textModelService.createModelReference(args.base).then>(v => ({ + object: v.object.textEditorModel, + dispose: () => v.dispose(), + })).catch(e => { + onUnexpectedError(e); + console.error(e); // Only file not found error should be handled ideally + return undefined; + }), this._textModelService.createModelReference(args.result), toInputData(args.input1, this._textModelService, store), toInputData(args.input2, this._textModelService, store), ]); + if (base === undefined) { + const tm = this._modelService.createModel('', this._languageService.createById(result.object.getLanguageId())); + base = { + dispose: () => { tm.dispose(); }, + object: tm + }; + } + store.add(base); store.add(result); @@ -321,7 +340,7 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa const model = this._instantiationService.createInstance( MergeEditorModel, - base.object.textEditorModel, + base.object, input1Data, input2Data, result.object.textEditorModel, diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index d60d64bc..308db886 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -5,7 +5,7 @@ import { ArrayQueue, CompareResult } from '../../../../base/common/arrays.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; -import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable, autorunOpts } from '../../../../base/common/observable.js'; import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; import { IModelDeltaDecoration } from '../../../../editor/common/model.js'; @@ -85,19 +85,6 @@ export function elementAtOrUndefined(arr: T[], index: number): T | undefined return arr[index]; } -export function thenIfNotDisposed(promise: Promise, then: () => void): IDisposable { - let disposed = false; - promise.then(() => { - if (disposed) { - return; - } - then(); - }); - return toDisposable(() => { - disposed = true; - }); -} - export function setFields(obj: T, fields: Partial): T { return Object.assign(obj, fields); } @@ -154,4 +141,3 @@ export class PersistentStore { ); } } - diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 32b12772..666ad3f3 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -10,7 +10,7 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Color } from '../../../../../base/common/color.js'; import { BugIndicatingError, onUnexpectedError } from '../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, thenIfNotDisposed, toDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun, autorunWithStore, IObservable, IReader, observableValue, transaction } from '../../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../../base/common/resources.js'; import { isDefined } from '../../../../../base/common/types.js'; @@ -23,7 +23,6 @@ import { ICodeEditorViewState, ScrollType } from '../../../../../editor/common/e import { ITextModel } from '../../../../../editor/common/model.js'; import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; import { localize } from '../../../../../nls.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IEditorOptions, ITextEditorOptions, ITextResourceEditorInput } from '../../../../../platform/editor/common/editor.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -39,8 +38,7 @@ import { readTransientState, writeTransientState } from '../../../codeEditor/bro import { MergeEditorInput } from '../mergeEditorInput.js'; import { IMergeEditorInputModel } from '../mergeEditorInputModel.js'; import { MergeEditorModel } from '../model/mergeEditorModel.js'; -import { deepMerge, PersistentStore, thenIfNotDisposed } from '../utils.js'; -import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { deepMerge, PersistentStore } from '../utils.js'; import { BaseCodeEditorView } from './editors/baseCodeEditorView.js'; import { ScrollSynchronizer } from './scrollSynchronizer.js'; import { MergeEditorViewModel } from './viewModel.js'; @@ -90,22 +88,12 @@ export class MergeEditor extends AbstractTextEditor { return this.inputModel.get()?.model; } - private get inputsWritable(): boolean { - return !!this._configurationService.getValue('mergeEditor.writableInputs'); - } - private readonly viewZoneComputer = new ViewZoneComputer( this.input1View.editor, this.input2View.editor, this.inputResultView.editor, ); - protected readonly codeLensesVisible = observableConfigValue( - 'mergeEditor.showCodeLenses', - true, - this.configurationService, - ); - private readonly scrollSynchronizer = this._register(new ScrollSynchronizer(this._viewModel, this.input1View, this.input2View, this.baseView, this.inputResultView, this._layoutModeObs)); constructor( @@ -116,12 +104,10 @@ export class MergeEditor extends AbstractTextEditor { @IStorageService storageService: IStorageService, @IThemeService themeService: IThemeService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @IEditorService editorService: IEditorService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IFileService fileService: IFileService, - @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService ) { super(MergeEditor.ID, group, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } @@ -170,14 +156,18 @@ export class MergeEditor extends AbstractTextEditor { const inputOptions: ICodeEditorOptions = deepMerge(options, { minimap: { enabled: false }, glyphMargin: false, - lineNumbersMinChars: 2, - readOnly: !this.inputsWritable + lineNumbersMinChars: 2 }); - this.input1View.updateOptions(inputOptions); - this.input2View.updateOptions(inputOptions); + const readOnlyInputOptions: ICodeEditorOptions = deepMerge(inputOptions, { + readOnly: true, + readOnlyMessage: undefined + }); + + this.input1View.updateOptions(readOnlyInputOptions); + this.input2View.updateOptions(readOnlyInputOptions); this.baseViewOptions.set({ ...this.input2View.editor.getRawOptions() }, undefined); - this.inputResultView.updateOptions(options); + this.inputResultView.updateOptions(inputOptions); } protected getMainControl(): ICodeEditor | undefined { @@ -213,7 +203,6 @@ export class MergeEditor extends AbstractTextEditor { this.showNonConflictingChanges, ); - model.telemetry.reportMergeEditorOpened({ combinableConflictCount: model.combinableConflictCount, conflictCount: model.conflictCount, @@ -382,7 +371,7 @@ export class MergeEditor extends AbstractTextEditor { const resultViewZoneIds: string[] = []; const viewZones = this.viewZoneComputer.computeViewZones(reader, viewModel, { - codeLensesVisible: this.codeLensesVisible.read(reader), + codeLensesVisible: true, showNonConflictingChanges: this.showNonConflictingChanges.read(reader), shouldAlignBase, shouldAlignResult, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts index 00b45c45..584fc890 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts @@ -11,10 +11,10 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { CellKind, NotebookSetting } from '../../../common/notebookCommon.js'; import { INotebookEditor, INotebookEditorContribution } from '../../notebookBrowser.js'; import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; -import { Iterable } from '../../../../../../base/common/iterator.js'; import { CodeCellViewModel } from '../../viewModel/codeCellViewModel.js'; import { Event } from '../../../../../../base/common/event.js'; import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; +import { ChatAgentLocation } from '../../../../chat/common/constants.js'; export class CellDiagnostics extends Disposable implements INotebookEditorContribution { @@ -43,12 +43,17 @@ export class CellDiagnostics extends Disposable implements INotebookEditorContri })); } + private hasNotebookAgent(): boolean { + const agents = this.chatAgentService.getAgents(); + return !!agents.find(agent => agent.locations.includes(ChatAgentLocation.Notebook)); + } + private updateEnabled() { const settingEnabled = this.configurationService.getValue(NotebookSetting.cellFailureDiagnostics); - if (this.enabled && (!settingEnabled || Iterable.isEmpty(this.chatAgentService.getAgents()))) { + if (this.enabled && (!settingEnabled || !this.hasNotebookAgent())) { this.enabled = false; this.clearAll(); - } else if (!this.enabled && settingEnabled && !Iterable.isEmpty(this.chatAgentService.getAgents())) { + } else if (!this.enabled && settingEnabled && this.hasNotebookAgent()) { this.enabled = true; if (!this.listening) { this.listening = true; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts index 0d7c0c3d..f10ea922 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts @@ -16,7 +16,7 @@ import { CodeCellViewModel } from '../../viewModel/codeCellViewModel.js'; import { INotebookCellStatusBarItem, CellStatusbarAlignment } from '../../../common/notebookCommon.js'; import { ICellExecutionError } from '../../../common/notebookExecutionStateService.js'; import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; -import { Iterable } from '../../../../../../base/common/iterator.js'; +import { ChatAgentLocation } from '../../../../chat/common/constants.js'; export class DiagnosticCellStatusBarContrib extends Disposable implements INotebookEditorContribution { static id: string = 'workbench.notebook.statusBar.diagtnostic'; @@ -49,10 +49,15 @@ class DiagnosticCellStatusBarItem extends Disposable { this._register(autorun((reader) => this.updateSparkleItem(reader.readObservable(cell.executionErrorDiagnostic)))); } + private hasNotebookAgent(): boolean { + const agents = this.chatAgentService.getAgents(); + return !!agents.find(agent => agent.locations.includes(ChatAgentLocation.Notebook)); + } + private async updateSparkleItem(error: ICellExecutionError | undefined) { let item: INotebookCellStatusBarItem | undefined; - if (error?.location && !Iterable.isEmpty(this.chatAgentService.getAgents())) { + if (error?.location && this.hasNotebookAgent()) { const keybinding = this.keybindingService.lookupKeybinding(OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID)?.getLabel(); const tooltip = localize('notebook.cell.status.diagnostic', "Quick Actions {0}", `(${keybinding})`); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts index 992fcddb..184c94db 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts @@ -288,7 +288,7 @@ class TimerCellStatusBarItem extends Disposable { this._deferredUpdate = disposableTimeout(() => { this._deferredUpdate = undefined; this._currentItemIds = this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items }]); - }, UPDATE_TIMER_GRACE_PERIOD); + }, UPDATE_TIMER_GRACE_PERIOD, this._store); } } else { this._deferredUpdate?.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts deleted file mode 100644 index 6b8387bd..00000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/contribution.ts +++ /dev/null @@ -1,17 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { INotebookModelSynchronizerFactory, NotebookModelSynchronizerFactory } from './notebookSynchronizer.js'; -import { INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory } from './notebookOriginalModelRefFactory.js'; -import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; -import { InstantiationType, registerSingleton } from '../../../../../../platform/instantiation/common/extensions.js'; -import { INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory } from './notebookOriginalCellModelFactory.js'; -import { NotebookChatEditorControllerContrib } from './notebookChatEditController.js'; - - -registerNotebookContribution(NotebookChatEditorControllerContrib.ID, NotebookChatEditorControllerContrib); -registerSingleton(INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory, InstantiationType.Delayed); -registerSingleton(INotebookModelSynchronizerFactory, NotebookModelSynchronizerFactory, InstantiationType.Delayed); -registerSingleton(INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts deleted file mode 100644 index ec0d2f43..00000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookCellDecorators.ts +++ /dev/null @@ -1,361 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, derived, observableFromEvent } from '../../../../../../base/common/observable.js'; -import { IChatEditingService, ChatEditingSessionState } from '../../../../chat/common/chatEditingService.js'; -import { INotebookEditor } from '../../notebookBrowser.js'; -import { ThrottledDelayer } from '../../../../../../base/common/async.js'; -import { ICodeEditor, IViewZone } from '../../../../../../editor/browser/editorBrowser.js'; -import { IEditorWorkerService } from '../../../../../../editor/common/services/editorWorker.js'; -import { EditorOption } from '../../../../../../editor/common/config/editorOptions.js'; -import { themeColorFromId } from '../../../../../../base/common/themables.js'; -import { RenderOptions, LineSource, renderLines } from '../../../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; -import { diffAddDecoration, diffWholeLineAddDecoration, diffDeleteDecoration } from '../../../../../../editor/browser/widget/diffEditor/registrations.contribution.js'; -import { IDocumentDiff } from '../../../../../../editor/common/diff/documentDiffProvider.js'; -import { ITextModel, TrackedRangeStickiness, MinimapPosition, IModelDeltaDecoration, OverviewRulerLane } from '../../../../../../editor/common/model.js'; -import { ModelDecorationOptions } from '../../../../../../editor/common/model/textModel.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../../../editor/common/viewModel.js'; -import { Range } from '../../../../../../editor/common/core/range.js'; -import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; -import { INotebookOriginalCellModelFactory } from './notebookOriginalCellModelFactory.js'; -import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; -import { isEqual } from '../../../../../../base/common/resources.js'; -import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../../../scm/common/quickDiff.js'; - - -export class NotebookCellDiffDecorator extends DisposableStore { - private _viewZones: string[] = []; - private readonly throttledDecorator = new ThrottledDelayer(50); - private diffForPreviouslyAppliedDecorators?: IDocumentDiff; - - private readonly perEditorDisposables = this.add(new DisposableStore()); - constructor( - notebookEditor: INotebookEditor, - public readonly modifiedCell: NotebookCellTextModel, - public readonly originalCell: NotebookCellTextModel, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @INotebookOriginalCellModelFactory private readonly originalCellModelFactory: INotebookOriginalCellModelFactory, - - ) { - super(); - - const onDidChangeVisibleRanges = observableFromEvent(notebookEditor.onDidChangeVisibleRanges, () => notebookEditor.visibleRanges); - const editorObs = derived((r) => { - const visibleRanges = onDidChangeVisibleRanges.read(r); - const visibleCellHandles = visibleRanges.map(range => notebookEditor.getCellsInRange(range)).flat().map(c => c.handle); - if (!visibleCellHandles.includes(modifiedCell.handle)) { - return; - } - const editor = notebookEditor.codeEditors.find(item => item[0].handle === modifiedCell.handle)?.[1]; - if (editor?.getModel() !== this.modifiedCell.textModel) { - return; - } - return editor; - }); - - this.add(autorunWithStore((r, store) => { - const editor = editorObs.read(r); - this.perEditorDisposables.clear(); - - if (editor) { - store.add(editor.onDidChangeModel(() => { - this.perEditorDisposables.clear(); - })); - store.add(editor.onDidChangeModelContent(() => { - this.update(editor); - })); - store.add(editor.onDidChangeConfiguration((e) => { - if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.lineHeight)) { - this.update(editor); - } - })); - this.update(editor); - } - })); - - const shouldBeReadOnly = derived(this, r => { - const editorUri = editorObs.read(r)?.getModel()?.uri; - if (!editorUri) { - return false; - } - const sessions = this._chatEditingService.editingSessionsObs.read(r); - const session = sessions.find(s => s.entries.read(r).some(e => isEqual(e.modifiedURI, editorUri))); - return session?.state.read(r) === ChatEditingSessionState.StreamingEdits; - }); - - - let actualReadonly: boolean | undefined; - let actualDeco: 'off' | 'editable' | 'on' | undefined; - - this.add(autorun((r) => { - const editor = editorObs.read(r); - if (!editor) { - return; - } - const value = shouldBeReadOnly.read(r); - if (value) { - actualReadonly ??= editor.getOption(EditorOption.readOnly); - actualDeco ??= editor.getOption(EditorOption.renderValidationDecorations); - - editor.updateOptions({ - readOnly: true, - renderValidationDecorations: 'off' - }); - } else { - if (actualReadonly !== undefined && actualDeco !== undefined) { - editor.updateOptions({ - readOnly: actualReadonly, - renderValidationDecorations: actualDeco - }); - actualReadonly = undefined; - actualDeco = undefined; - } - } - })); - } - - public update(editor: ICodeEditor): void { - this.throttledDecorator.trigger(() => this._updateImpl(editor)); - } - - private async _updateImpl(editor: ICodeEditor) { - if (this.isDisposed) { - return; - } - if (editor.getOption(EditorOption.inDiffEditor)) { - this.perEditorDisposables.clear(); - return; - } - const model = editor.getModel(); - if (!model || model !== this.modifiedCell.textModel) { - this.perEditorDisposables.clear(); - return; - } - - const originalModel = this.getOrCreateOriginalModel(editor); - if (!originalModel) { - this.perEditorDisposables.clear(); - return; - } - const version = model.getVersionId(); - const diff = await this._editorWorkerService.computeDiff( - originalModel.uri, - model.uri, - { computeMoves: true, ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER }, - 'advanced' - ); - - - if (this.isDisposed) { - return; - } - - - if (diff && !diff.identical && originalModel && model === editor.getModel() && editor.getModel()?.getVersionId() === version) { - this._updateWithDiff(editor, originalModel, diff); - } else { - this.perEditorDisposables.clear(); - } - } - - private _originalModel?: ITextModel; - private getOrCreateOriginalModel(editor: ICodeEditor) { - if (!this._originalModel) { - const model = editor.getModel(); - if (!model) { - return; - } - this._originalModel = this.add(this.originalCellModelFactory.getOrCreate(model.uri, this.originalCell.getValue(), model.getLanguageId(), this.modifiedCell.cellKind)).object; - } - return this._originalModel; - } - - private _updateWithDiff(editor: ICodeEditor, originalModel: ITextModel | undefined, diff: IDocumentDiff): void { - if (areDiffsEqual(diff, this.diffForPreviouslyAppliedDecorators)) { - return; - } - this.perEditorDisposables.clear(); - const decorations = editor.createDecorationsCollection(); - this.perEditorDisposables.add(toDisposable(() => { - editor.changeViewZones((viewZoneChangeAccessor) => { - for (const id of this._viewZones) { - viewZoneChangeAccessor.removeZone(id); - } - }); - this._viewZones = []; - decorations.clear(); - this.diffForPreviouslyAppliedDecorators = undefined; - })); - - this.diffForPreviouslyAppliedDecorators = diff; - - const chatDiffAddDecoration = ModelDecorationOptions.createDynamic({ - ...diffAddDecoration, - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - }); - const chatDiffWholeLineAddDecoration = ModelDecorationOptions.createDynamic({ - ...diffWholeLineAddDecoration, - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - }); - const createOverviewDecoration = (overviewRulerColor: string, minimapColor: string) => { - return ModelDecorationOptions.createDynamic({ - description: 'chat-editing-decoration', - overviewRuler: { color: themeColorFromId(overviewRulerColor), position: OverviewRulerLane.Left }, - minimap: { color: themeColorFromId(minimapColor), position: MinimapPosition.Gutter }, - }); - }; - const modifiedDecoration = createOverviewDecoration(overviewRulerModifiedForeground, minimapGutterModifiedBackground); - const addedDecoration = createOverviewDecoration(overviewRulerAddedForeground, minimapGutterAddedBackground); - const deletedDecoration = createOverviewDecoration(overviewRulerDeletedForeground, minimapGutterDeletedBackground); - - editor.changeViewZones((viewZoneChangeAccessor) => { - for (const id of this._viewZones) { - viewZoneChangeAccessor.removeZone(id); - } - this._viewZones = []; - const modifiedDecorations: IModelDeltaDecoration[] = []; - const mightContainNonBasicASCII = originalModel?.mightContainNonBasicASCII(); - const mightContainRTL = originalModel?.mightContainRTL(); - const renderOptions = RenderOptions.fromEditor(editor); - - for (const diffEntry of diff.changes) { - const originalRange = diffEntry.original; - if (originalModel) { - originalModel.tokenization.forceTokenization(Math.max(1, originalRange.endLineNumberExclusive - 1)); - } - const source = new LineSource( - (originalRange.length && originalModel) ? originalRange.mapToLineArray(l => originalModel.tokenization.getLineTokens(l)) : [], - [], - mightContainNonBasicASCII, - mightContainRTL, - ); - const decorations: InlineDecoration[] = []; - for (const i of diffEntry.innerChanges || []) { - decorations.push(new InlineDecoration( - i.originalRange.delta(-(diffEntry.original.startLineNumber - 1)), - diffDeleteDecoration.className!, - InlineDecorationType.Regular - )); - modifiedDecorations.push({ - range: i.modifiedRange, options: chatDiffAddDecoration - }); - } - if (!diffEntry.modified.isEmpty) { - modifiedDecorations.push({ - range: diffEntry.modified.toInclusiveRange()!, options: chatDiffWholeLineAddDecoration - }); - } - - if (diffEntry.original.isEmpty) { - // insertion - modifiedDecorations.push({ - range: diffEntry.modified.toInclusiveRange()!, - options: addedDecoration - }); - } else if (diffEntry.modified.isEmpty) { - // deletion - modifiedDecorations.push({ - range: new Range(diffEntry.modified.startLineNumber - 1, 1, diffEntry.modified.startLineNumber, 1), - options: deletedDecoration - }); - } else { - // modification - modifiedDecorations.push({ - range: diffEntry.modified.toInclusiveRange()!, - options: modifiedDecoration - }); - } - const domNode = document.createElement('div'); - domNode.className = 'chat-editing-original-zone view-lines line-delete monaco-mouse-cursor-text'; - const result = renderLines(source, renderOptions, decorations, domNode); - - const isCreatedContent = decorations.length === 1 && decorations[0].range.isEmpty() && decorations[0].range.startLineNumber === 1; - if (!isCreatedContent) { - const viewZoneData: IViewZone = { - afterLineNumber: diffEntry.modified.startLineNumber - 1, - heightInLines: result.heightInLines, - domNode, - ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 - }; - - this._viewZones.push(viewZoneChangeAccessor.addZone(viewZoneData)); - } - } - - decorations.set(modifiedDecorations); - }); - } -} - -function areDiffsEqual(a: IDocumentDiff | undefined, b: IDocumentDiff | undefined): boolean { - if (a && b) { - if (a.changes.length !== b.changes.length) { - return false; - } - if (a.moves.length !== b.moves.length) { - return false; - } - if (!areLineRangeMappinsEqual(a.changes, b.changes)) { - return false; - } - if (!a.moves.some((move, i) => { - const bMove = b.moves[i]; - if (!areLineRangeMappinsEqual(move.changes, bMove.changes)) { - return true; - } - if (move.lineRangeMapping.changedLineCount !== bMove.lineRangeMapping.changedLineCount) { - return true; - } - if (!move.lineRangeMapping.modified.equals(bMove.lineRangeMapping.modified)) { - return true; - } - if (!move.lineRangeMapping.original.equals(bMove.lineRangeMapping.original)) { - return true; - } - return false; - })) { - return false; - } - return true; - } else if (!a && !b) { - return true; - } else { - return false; - } -} - -function areLineRangeMappinsEqual(a: readonly DetailedLineRangeMapping[], b: readonly DetailedLineRangeMapping[]): boolean { - if (a.length !== b.length) { - return false; - } - if (a.some((c, i) => { - const bChange = b[i]; - if (c.changedLineCount !== bChange.changedLineCount) { - return true; - } - if ((c.innerChanges || []).length !== (bChange.innerChanges || []).length) { - return true; - } - if ((c.innerChanges || []).some((innerC, innerIdx) => { - const bInnerC = bChange.innerChanges![innerIdx]; - if (!innerC.modifiedRange.equalsRange(bInnerC.modifiedRange)) { - return true; - } - if (!innerC.originalRange.equalsRange(bInnerC.originalRange)) { - return true; - } - return false; - })) { - return true; - } - - return false; - })) { - return false; - } - return true; -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts deleted file mode 100644 index 5669e5b1..00000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatActionsOverlay.ts +++ /dev/null @@ -1,323 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { CellEditState, getNotebookEditorFromEditorPane, INotebookEditor, INotebookViewModel } from '../../notebookBrowser.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; -import { MenuId } from '../../../../../../platform/actions/common/actions.js'; -import { ActionViewItem } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { ActionRunner, IAction, IActionRunner } from '../../../../../../base/common/actions.js'; -import { $ } from '../../../../../../base/browser/dom.js'; -import { IChatEditingService, IModifiedFileEntry } from '../../../../chat/common/chatEditingService.js'; -import { ACTIVE_GROUP, IEditorService } from '../../../../../services/editor/common/editorService.js'; -import { autorun, autorunWithStore, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; -import { isEqual } from '../../../../../../base/common/resources.js'; -import { CellDiffInfo } from '../../diff/notebookDiffViewModel.js'; -import { AcceptAction, navigationBearingFakeActionId, RejectAction } from '../../../../chat/browser/chatEditing/chatEditingEditorActions.js'; -import { INotebookDeletedCellDecorator } from '../../diff/inlineDiff/notebookDeletedCellDecorator.js'; -import { ChatEditingModifiedDocumentEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedDocumentEntry.js'; - -export class NotebookChatActionsOverlayController extends Disposable { - constructor( - private readonly notebookEditor: INotebookEditor, - cellDiffInfo: IObservable, - deletedCellDecorator: INotebookDeletedCellDecorator, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - - const notebookModel = observableFromEvent(this.notebookEditor.onDidChangeModel, e => e); - - this._register(autorunWithStore((r, store) => { - const model = notebookModel.read(r); - if (!model) { - return; - } - const sessions = this._chatEditingService.editingSessionsObs.read(r); - const session = sessions.find(s => s.readEntry(model.uri, r)); - const entry = session?.readEntry(model.uri, r); - if (!session || !entry || !(entry instanceof ChatEditingModifiedDocumentEntry)) { - return; - } - - const entries = session.entries.read(r); - const idx = entries.findIndex(e => isEqual(e.modifiedURI, model.uri)); - if (idx >= 0) { - const entry = entries[idx]; - const nextEntry = entries[(idx + 1) % entries.length]; - const previousEntry = entries[(idx - 1 + entries.length) % entries.length]; - store.add(instantiationService.createInstance(NotebookChatActionsOverlay, notebookEditor, entry, cellDiffInfo, nextEntry, previousEntry, deletedCellDecorator)); - } - })); - } -} - -// Copied from src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts (until we unify these) -export class NotebookChatActionsOverlay extends Disposable { - private readonly focusedDiff = observableValue('focusedDiff', undefined); - private readonly toolbarNode: HTMLElement; - private added: boolean = false; - constructor( - private readonly notebookEditor: INotebookEditor, - entry: IModifiedFileEntry, - cellDiffInfo: IObservable, - nextEntry: IModifiedFileEntry, - previousEntry: IModifiedFileEntry, - deletedCellDecorator: INotebookDeletedCellDecorator, - @IEditorService _editorService: IEditorService, - @IInstantiationService instaService: IInstantiationService, - ) { - super(); - - this._register(notebookEditor.onDidBlurWidget(() => { - if (getNotebookEditorFromEditorPane(_editorService.activeEditorPane) !== notebookEditor) { - this.focusedDiff.set(undefined, undefined); - } - })); - this._register(notebookEditor.onDidChangeActiveEditor(e => { - if (e !== notebookEditor) { - this.focusedDiff.set(undefined, undefined); - } - })); - - this.toolbarNode = $('div'); - this.toolbarNode.classList.add('notebook-chat-editor-overlay-widget'); - this._register(toDisposable(() => this.hide())); - - this._register(autorun(r => { - const diffs = cellDiffInfo.read(r); - if (diffs?.some(d => d.type !== 'unchanged')) { - this.show(); - } else { - this.hide(); - } - })); - - const focusedDiff = this.focusedDiff; - const _toolbar = instaService.createInstance(MenuWorkbenchToolBar, this.toolbarNode, MenuId.ChatEditingEditorContent, { - telemetrySource: 'chatEditor.overlayToolbar', - hiddenItemStrategy: HiddenItemStrategy.Ignore, - toolbarOptions: { - primaryGroup: () => true, - useSeparatorsInPrimaryActions: true - }, - menuOptions: { renderShortTitle: true }, - actionViewItemProvider: (action, options) => { - - if (action.id === navigationBearingFakeActionId) { - return this._register(new class extends ActionViewItem { - constructor() { - super(undefined, action, { ...options, icon: false, label: false, keybindingNotRenderedWithLabel: true }); - } - }); - } - - if (action.id === AcceptAction.ID || action.id === RejectAction.ID) { - return this._register(new class extends ActionViewItem { - private readonly _reveal = this._store.add(new MutableDisposable()); - constructor() { - super(undefined, action, { ...options, icon: false, label: true, keybindingNotRenderedWithLabel: true }); - } - override set actionRunner(actionRunner: IActionRunner) { - super.actionRunner = actionRunner; - - const store = new DisposableStore(); - - store.add(actionRunner.onWillRun(_e => { - notebookEditor.focus(); - })); - store.add(actionRunner.onDidRun(e => { - if (e.action !== this.action) { - return; - } - if (entry === nextEntry) { - return; - } - - })); - - this._reveal.value = store; - } - override get actionRunner(): IActionRunner { - return super.actionRunner; - } - }); - } - // Override next/previous with our implementation. - if (action.id === 'chatEditor.action.navigateNext' || action.id === 'chatEditor.action.navigatePrevious') { - return this._register(new class extends ActionViewItem { - constructor() { - super(undefined, action, { ...options, icon: true, label: false, keybindingNotRenderedWithLabel: true }); - } - override set actionRunner(_: IActionRunner) { - const next = action.id === 'chatEditor.action.navigateNext' ? nextEntry : previousEntry; - const direction = action.id === 'chatEditor.action.navigateNext' ? 'next' : 'previous'; - super.actionRunner = this._register(new NextPreviousChangeActionRunner(notebookEditor, cellDiffInfo, entry, next, direction, _editorService, deletedCellDecorator, focusedDiff)); - } - override get actionRunner(): IActionRunner { - return super.actionRunner; - } - }); - } - return undefined; - } - - }); - - this._register(_toolbar); - } - - private show() { - if (!this.added) { - this.notebookEditor.getDomNode().appendChild(this.toolbarNode); - this.added = true; - } - } - - private hide() { - if (this.added) { - this.notebookEditor.getDomNode().removeChild(this.toolbarNode); - this.added = false; - } - } - - -} - -class NextPreviousChangeActionRunner extends ActionRunner { - constructor( - private readonly notebookEditor: INotebookEditor, - private readonly cellDiffInfo: IObservable, - private readonly entry: IModifiedFileEntry, - private readonly next: IModifiedFileEntry, - private readonly direction: 'next' | 'previous', - private readonly editorService: IEditorService, - private readonly deletedCellDecorator: INotebookDeletedCellDecorator, - private readonly focusedDiff: ISettableObservable - ) { - super(); - } - protected override async runAction(_action: IAction, _context?: unknown): Promise { - const viewModel = this.notebookEditor.getViewModel(); - const activeCell = this.notebookEditor.activeCellAndCodeEditor; - const cellDiff = this.cellDiffInfo.read(undefined); - if (!viewModel || !cellDiff?.length || (!activeCell && this.focusedDiff.read(undefined))) { - return this.goToNextEntry(); - } - - const nextDiff = this.getNextCellDiff(cellDiff, viewModel); - if (nextDiff && (await this.focusDiff(nextDiff, viewModel))) { - return; - } - - return this.goToNextEntry(); - } - - /** - * @returns `true` if focused to the next diff - */ - private async focusDiff(diff: CellDiffInfo, viewModel: INotebookViewModel) { - if (diff.type === 'delete') { - const top = this.deletedCellDecorator.getTop(diff.originalCellIndex); - if (typeof top === 'number') { - this.focusedDiff.set(diff, undefined); - this.notebookEditor.setScrollTop(top); - return true; - } - } else { - const index = diff.modifiedCellIndex; - this.focusedDiff.set(diff, undefined); - await this.notebookEditor.focusNotebookCell(viewModel.viewCells[index], 'container'); - this.notebookEditor.revealInViewAtTop(viewModel.viewCells[index]); - viewModel.viewCells[index].updateEditState(CellEditState.Editing, 'chatEdit'); - return true; - } - return false; - } - - private getNextCellDiff(cellDiffInfo: CellDiffInfo[], viewModel: INotebookViewModel) { - const activeCell = this.notebookEditor.activeCellAndCodeEditor; - const currentCellIndex = activeCell ? viewModel.viewCells.findIndex(c => c.handle === activeCell[0].handle) : (this.direction === 'next' ? 0 : viewModel.viewCells.length - 1); - if (this.focusedDiff.read(undefined)) { - const changes = cellDiffInfo.filter(d => d.type !== 'unchanged'); - const idx = changes.findIndex(d => d === this.focusedDiff.read(undefined)); - if (idx >= 0) { - const next = this.direction === 'next' ? idx + 1 : idx - 1; - if (next >= 0 && next < changes.length) { - return changes[next]; - } - } - } else if (this.direction === 'next') { - let currentIndex = 0; - let next: CellDiffInfo | undefined; - cellDiffInfo - .forEach((d, i) => { - if (next) { - return; - } - if (d.type === 'insert' || d.type === 'modified') { - if (d.modifiedCellIndex > currentCellIndex) { - next = d; - } - currentIndex = d.modifiedCellIndex; - } else if (d.type === 'unchanged') { - currentIndex = d.modifiedCellIndex; - } else if (currentIndex >= currentCellIndex) { - next = d; - } - }); - if (next) { - return next; - } - } else { - let currentIndex = 0; - let previous: CellDiffInfo | undefined; - cellDiffInfo - .forEach((d, i) => { - if (d.type === 'insert' || d.type === 'modified') { - if (d.modifiedCellIndex < currentCellIndex) { - previous = d; - } - currentIndex = d.modifiedCellIndex; - } else if (d.type === 'unchanged') { - currentIndex = d.modifiedCellIndex; - } else if (currentIndex <= currentCellIndex) { - previous = d; - } - }); - if (previous) { - return previous; - } - } - - if (this.canGoToNextEntry()) { - return; - } - - return this.direction === 'next' ? cellDiffInfo[0] : cellDiffInfo[cellDiffInfo.length - 1]; - } - - - private canGoToNextEntry() { - return this.entry !== this.next; - } - - private async goToNextEntry() { - if (!this.canGoToNextEntry()) { - return; - } - // For now just go to next/previous file. - this.focusedDiff.set(undefined, undefined); - await this.editorService.openEditor({ - resource: this.next.modifiedURI, - options: { - revealIfOpened: false, - revealIfVisible: false, - } - }, ACTIVE_GROUP); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditContext.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditContext.ts deleted file mode 100644 index ff706d88..00000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../../../nls.js'; -import { RawContextKey } from '../../../../../../platform/contextkey/common/contextkey.js'; - -export const ctxNotebookHasEditorModification = new RawContextKey('chat.hasNotebookEditorModifications', undefined, localize('chat.hasNotebookEditorModifications', "The current Notebook editor contains chat modifications")); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts deleted file mode 100644 index 32cce7d9..00000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts +++ /dev/null @@ -1,211 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, dispose, IReference, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { autorun, derived, derivedWithStore, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; -import { IChatEditingService, WorkingSetEntryState } from '../../../../chat/common/chatEditingService.js'; -import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; -import { INotebookEditor, INotebookEditorContribution } from '../../notebookBrowser.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { NotebookCellDiffDecorator } from './notebookCellDecorators.js'; -import { INotebookModelSynchronizerFactory, NotebookModelSynchronizer } from './notebookSynchronizer.js'; -import { INotebookOriginalModelReferenceFactory } from './notebookOriginalModelRefFactory.js'; -import { debouncedObservable } from '../../../../../../base/common/observableInternal/utils.js'; -import { CellDiffInfo } from '../../diff/notebookDiffViewModel.js'; -import { NotebookChatActionsOverlayController } from './notebookChatActionsOverlay.js'; -import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; -import { Event } from '../../../../../../base/common/event.js'; -import { ctxNotebookHasEditorModification } from './notebookChatEditContext.js'; -import { NotebookDeletedCellDecorator } from '../../diff/inlineDiff/notebookDeletedCellDecorator.js'; -import { NotebookInsertedCellDecorator } from '../../diff/inlineDiff/notebookInsertedCellDecorator.js'; -import { ChatEditingModifiedDocumentEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedDocumentEntry.js'; - -export class NotebookChatEditorControllerContrib extends Disposable implements INotebookEditorContribution { - - public static readonly ID: string = 'workbench.notebook.chatEditorController'; - readonly _serviceBrand: undefined; - constructor( - notebookEditor: INotebookEditor, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - - ) { - super(); - this._register(instantiationService.createInstance(NotebookChatEditorController, notebookEditor)); - } -} - - -class NotebookChatEditorController extends Disposable { - private readonly deletedCellDecorator: NotebookDeletedCellDecorator; - private readonly insertedCellDecorator: NotebookInsertedCellDecorator; - private readonly _ctxHasEditorModification: IContextKey; - constructor( - private readonly notebookEditor: INotebookEditor, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @INotebookOriginalModelReferenceFactory private readonly originalModelRefFactory: INotebookOriginalModelReferenceFactory, - @INotebookModelSynchronizerFactory private readonly synchronizerFactory: INotebookModelSynchronizerFactory, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - super(); - this._ctxHasEditorModification = ctxNotebookHasEditorModification.bindTo(contextKeyService); - this.deletedCellDecorator = this._register(instantiationService.createInstance(NotebookDeletedCellDecorator, notebookEditor, undefined)); - this.insertedCellDecorator = this._register(instantiationService.createInstance(NotebookInsertedCellDecorator, notebookEditor)); - const notebookModel = observableFromEvent(this.notebookEditor.onDidChangeModel, e => e); - const originalModel = observableValue('originalModel', undefined); - // We need to render viewzones only when the viewmodel is attached (i.e. list view is ready). - // https://github.com/microsoft/vscode/issues/234718 - const readyToRenderViewzones = observableValue('viewModelAttached', false); - this._register(Event.once(this.notebookEditor.onDidAttachViewModel)(() => readyToRenderViewzones.set(true, undefined))); - const onDidChangeVisibleRanges = debouncedObservable(observableFromEvent(this.notebookEditor.onDidChangeVisibleRanges, () => this.notebookEditor.visibleRanges), 50); - const decorators = new Map(); - - let updatedCellDecoratorsOnceBefore = false; - let updatedDeletedInsertedDecoratorsOnceBefore = false; - - - const clearDecorators = () => { - dispose(Array.from(decorators.values())); - decorators.clear(); - this.deletedCellDecorator.clear(); - this.insertedCellDecorator.clear(); - }; - - this._register(toDisposable(() => clearDecorators())); - - let notebookSynchronizer: IReference; - const entryObs = derived((r) => { - const model = notebookModel.read(r); - if (!model) { - return; - } - const sessions = this._chatEditingService.editingSessionsObs.read(r); - const entry = sessions.map(s => s.readEntry(model.uri, r)).find(r => !!r); - return entry instanceof ChatEditingModifiedDocumentEntry ? entry : undefined; - }).recomputeInitiallyAndOnChange(this._store); - - - this._register(autorun(r => { - const entry = entryObs.read(r); - const model = notebookModel.read(r); - if (!entry || !model || entry.state.read(r) !== WorkingSetEntryState.Modified) { - clearDecorators(); - } - })); - - const notebookDiffInfo = derivedWithStore(this, (r, store) => { - const entry = entryObs.read(r); - const model = notebookModel.read(r); - if (!entry || !model) { - // If entry is undefined, then revert the changes to the notebook. - if (notebookSynchronizer && model) { - notebookSynchronizer.object.revert(); - } - return observableValue<{ - cellDiff: CellDiffInfo[]; - modelVersion: number; - } | undefined>('DefaultDiffIno', undefined); - } - - notebookSynchronizer = notebookSynchronizer || this._register(this.synchronizerFactory.getOrCreate(model)); - this.originalModelRefFactory.getOrCreate(entry, model.viewType).then(ref => originalModel.set(this._register(ref).object, undefined)); - - return notebookSynchronizer.object.diffInfo; - }).recomputeInitiallyAndOnChange(this._store).flatten(); - - const notebookCellDiffInfo = notebookDiffInfo.map(d => d?.cellDiff); - this._register(instantiationService.createInstance(NotebookChatActionsOverlayController, notebookEditor, notebookCellDiffInfo, this.deletedCellDecorator)); - - this._register(autorun(r => { - // If we have a new entry for the file, then clear old decorators. - // User could be cycling through different edit sessions (Undo Last Edit / Redo Last Edit). - entryObs.read(r); - clearDecorators(); - })); - - this._register(autorun(r => { - // If there's no diff info, then we either accepted or rejected everything. - const diffs = notebookDiffInfo.read(r); - if (!diffs || !diffs.cellDiff.length) { - clearDecorators(); - this._ctxHasEditorModification.reset(); - } else { - this._ctxHasEditorModification.set(true); - } - })); - - this._register(autorun(r => { - const entry = entryObs.read(r); - const diffInfo = notebookDiffInfo.read(r); - const modified = notebookModel.read(r); - const original = originalModel.read(r); - onDidChangeVisibleRanges.read(r); - - if (!entry || !modified || !original || !diffInfo) { - return; - } - if (diffInfo && updatedCellDecoratorsOnceBefore && (diffInfo.modelVersion !== modified.versionId)) { - return; - } - - updatedCellDecoratorsOnceBefore = true; - const validDiffDecorators = new Set(); - diffInfo.cellDiff.forEach((diff) => { - if (diff.type === 'modified') { - const modifiedCell = modified.cells[diff.modifiedCellIndex]; - const originalCell = original.cells[diff.originalCellIndex]; - const editor = this.notebookEditor.codeEditors.find(([vm,]) => vm.handle === modifiedCell.handle)?.[1]; - - if (editor) { - const currentDecorator = decorators.get(modifiedCell); - if ((currentDecorator?.modifiedCell !== modifiedCell || currentDecorator?.originalCell !== originalCell)) { - currentDecorator?.dispose(); - const decorator = this.instantiationService.createInstance(NotebookCellDiffDecorator, notebookEditor, modifiedCell, originalCell); - decorators.set(modifiedCell, decorator); - validDiffDecorators.add(decorator); - this._register(editor.onDidDispose(() => { - decorator.dispose(); - if (decorators.get(modifiedCell) === decorator) { - decorators.delete(modifiedCell); - } - })); - } else if (currentDecorator) { - validDiffDecorators.add(currentDecorator); - } - } - } - }); - - // Dispose old decorators - decorators.forEach((v, cell) => { - if (!validDiffDecorators.has(v)) { - v.dispose(); - decorators.delete(cell); - } - }); - })); - - this._register(autorun(r => { - const entry = entryObs.read(r); - const diffInfo = notebookDiffInfo.read(r); - const modified = notebookModel.read(r); - const original = originalModel.read(r); - const ready = readyToRenderViewzones.read(r); - if (!ready || !entry || !modified || !original || !diffInfo) { - return; - } - if (diffInfo && updatedDeletedInsertedDecoratorsOnceBefore && (diffInfo.modelVersion !== modified.versionId)) { - return; - } - updatedDeletedInsertedDecoratorsOnceBefore = true; - this.insertedCellDecorator.apply(diffInfo.cellDiff); - this.deletedCellDecorator.apply(diffInfo.cellDiff, original); - })); - } - -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts deleted file mode 100644 index dbebccbe..00000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts +++ /dev/null @@ -1,500 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { isEqual } from '../../../../../../base/common/resources.js'; -import { Disposable, IReference, ReferenceCollection } from '../../../../../../base/common/lifecycle.js'; -import { IChatEditingService, IModifiedFileEntry } from '../../../../chat/common/chatEditingService.js'; -import { INotebookService } from '../../../common/notebookService.js'; -import { bufferToStream, streamToBuffer, VSBuffer } from '../../../../../../base/common/buffer.js'; -import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; -import { raceCancellation, ThrottledDelayer } from '../../../../../../base/common/async.js'; -import { CellDiffInfo, computeDiff, prettyChanges } from '../../diff/notebookDiffViewModel.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; -import { INotebookEditorWorkerService } from '../../../common/services/notebookWorkerService.js'; -import { CellEditType, ICellDto2, ICellEditOperation, ICellReplaceEdit, NotebookData, NotebookSetting } from '../../../common/notebookCommon.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { EditOperation } from '../../../../../../editor/common/core/editOperation.js'; -import { INotebookLoggingService } from '../../../common/notebookLoggingService.js'; -import { filter } from '../../../../../../base/common/objects.js'; -import { INotebookEditorModelResolverService } from '../../../common/notebookEditorModelResolverService.js'; -import { IChatService } from '../../../../chat/common/chatService.js'; -import { createDecorator, IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { INotebookOriginalModelReferenceFactory } from './notebookOriginalModelRefFactory.js'; -import { autorunWithStore, derived, IObservable, observableValue } from '../../../../../../base/common/observable.js'; -import { SaveReason } from '../../../../../common/editor.js'; -import { ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; -import { SnapshotContext } from '../../../../../services/workingCopy/common/fileWorkingCopy.js'; -import { INotebookEditorService } from '../../services/notebookEditorService.js'; -import { CellEditState } from '../../notebookBrowser.js'; -import { IModelService } from '../../../../../../editor/common/services/model.js'; -import { ChatEditingModifiedDocumentEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedDocumentEntry.js'; - - -export const INotebookModelSynchronizerFactory = createDecorator('INotebookModelSynchronizerFactory'); - -export interface INotebookModelSynchronizerFactory { - readonly _serviceBrand: undefined; - getOrCreate(model: NotebookTextModel): IReference; -} - -class NotebookModelSynchronizerReferenceCollection extends ReferenceCollection { - constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { - super(); - } - protected override createReferencedObject(_key: string, model: NotebookTextModel): NotebookModelSynchronizer { - return this.instantiationService.createInstance(NotebookModelSynchronizer, model); - } - protected override destroyReferencedObject(_key: string, object: NotebookModelSynchronizer): void { - object.dispose(); - } -} - -export class NotebookModelSynchronizerFactory implements INotebookModelSynchronizerFactory { - readonly _serviceBrand: undefined; - private readonly _data: NotebookModelSynchronizerReferenceCollection; - constructor(@IInstantiationService instantiationService: IInstantiationService) { - this._data = instantiationService.createInstance(NotebookModelSynchronizerReferenceCollection); - } - - getOrCreate(model: NotebookTextModel): IReference { - return this._data.acquire(model.uri.toString(), model); - } -} - - -export class NotebookModelSynchronizer extends Disposable { - private readonly throttledUpdateNotebookModel = new ThrottledDelayer(200); - private _diffInfo = observableValue<{ cellDiff: CellDiffInfo[]; modelVersion: number } | undefined>('diffInfo', undefined); - public get diffInfo(): IObservable<{ cellDiff: CellDiffInfo[]; modelVersion: number } | undefined> { - return this._diffInfo; - } - private snapshot?: { bytes: VSBuffer; dirty: boolean }; - private isEditFromUs: boolean = false; - private isTextEditFromUs: boolean = false; - private isReverting = false; - private throttledTextModelUpdate = new ThrottledDelayer(100); - constructor( - private readonly model: NotebookTextModel, - @IChatEditingService _chatEditingService: IChatEditingService, - @INotebookService private readonly notebookService: INotebookService, - @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, - @IChatService chatService: IChatService, - @IModelService private readonly modelService: IModelService, - @ITextModelService private readonly textModelService: ITextModelService, - @INotebookLoggingService private readonly logService: INotebookLoggingService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @INotebookEditorWorkerService private readonly notebookEditorWorkerService: INotebookEditorWorkerService, - @INotebookEditorModelResolverService private readonly notebookModelResolverService: INotebookEditorModelResolverService, - @INotebookOriginalModelReferenceFactory private readonly originalModelRefFactory: INotebookOriginalModelReferenceFactory, - ) { - super(); - - const entryObs = derived((r) => { - const sessions = _chatEditingService.editingSessionsObs.read(r); - const entry = sessions.map(s => s.readEntry(model.uri, r)).find(r => !!r); - return entry instanceof ChatEditingModifiedDocumentEntry ? entry : undefined; - }).recomputeInitiallyAndOnChange(this._store); - - - this._register(chatService.onDidPerformUserAction(async e => { - const entry = entryObs.read(undefined); - if (!entry) { - return; - } - if (e.action.kind === 'chatEditingSessionAction' && !e.action.hasRemainingEdits && isEqual(e.action.uri, entry.modifiedURI)) { - if (e.action.outcome === 'accepted') { - await this.accept(entry); - await this.createSnapshot(); - this._diffInfo.set(undefined, undefined); - } - else if (e.action.outcome === 'rejected') { - await this.revertImpl(); - } - } - })); - - const updateNotebookModel = (entry: IModifiedFileEntry, token: CancellationToken) => { - this.throttledUpdateNotebookModel.trigger(() => this.updateNotebookModel(entry, token)); - }; - - let snapshotCreated = false; - this._register(autorunWithStore((r, store) => { - const entry = entryObs.read(r); - if (!entry) { - return; - } - if (!snapshotCreated) { - this.createSnapshot(); - snapshotCreated = true; - } - - const modifiedModel = this.modelService.getModel(entry.modifiedURI); - if (!modifiedModel) { - return; - } - let cancellationToken = store.add(new CancellationTokenSource()); - store.add(modifiedModel.onDidChangeContent(async () => { - const originalModel = this.modelService.getModel(entry.originalURI); - if (originalModel && !this.isTextEditFromUs && !modifiedModel.isDisposed() && !originalModel.isDisposed() && modifiedModel.getValue() !== originalModel.getValue()) { - cancellationToken = store.add(new CancellationTokenSource()); - updateNotebookModel(entry, cancellationToken.token); - } - })); - - updateNotebookModel(entry, cancellationToken.token); - })); - - this._register(model.onDidChangeContent(() => { - // Track changes from the user. - if (!this.isEditFromUs && this.snapshot) { - this.snapshot.dirty = true; - const entry = entryObs.get(); - if (entry) { - this.throttledTextModelUpdate.trigger(() => this.updateTextDocumentModel(entry)); - } - } - })); - } - - private async createSnapshot() { - const [serializer, ref] = await Promise.all([ - this.getNotebookSerializer(), - this.notebookModelResolverService.resolve(this.model.uri) - ]); - - try { - const data: NotebookData = { - metadata: filter(this.model.metadata, key => !serializer.options.transientDocumentMetadata[key]), - cells: [], - }; - - const indentAmount = this.model.metadata.indentAmount || ref.object.notebook.metadata.indentAmount || undefined; - if (typeof indentAmount === 'string' && indentAmount) { - // This is required for ipynb serializer to preserve the whitespace in the notebook. - data.metadata.indentAmount = indentAmount; - } - - let outputSize = 0; - for (const cell of this.model.cells) { - const cellData: ICellDto2 = { - cellKind: cell.cellKind, - language: cell.language, - mime: cell.mime, - source: cell.getValue(), - outputs: [], - internalMetadata: cell.internalMetadata - }; - - const outputSizeLimit = this.configurationService.getValue(NotebookSetting.outputBackupSizeLimit) * 1024; - if (outputSizeLimit > 0) { - cell.outputs.forEach(output => { - output.outputs.forEach(item => { - outputSize += item.data.byteLength; - }); - }); - if (outputSize > outputSizeLimit) { - return; - } - } - - cellData.outputs = !serializer.options.transientOutputs ? cell.outputs : []; - cellData.metadata = filter(cell.metadata, key => !serializer.options.transientCellMetadata[key]); - - data.cells.push(cellData); - } - - const bytes = await serializer.notebookToData(data); - this.snapshot = { bytes, dirty: ref.object.isDirty() }; - } finally { - ref.dispose(); - } - } - - public async revert() { - await this.revertImpl(); - } - - private async revertImpl(): Promise { - if (!this.snapshot || this.isReverting) { - return; - } - this.isReverting = true; - try { - // NOTE: We must save if the notebook model was not already dirty. - // Today ModifiedFileEntry class will save the text model to get rid of dirty indicator. - // If we do not save the notebook model, then ipynb json text document will get saved in ModifiedFileEntry class, - // and that results in ipynb being saved without going to serializer. - // Serializer is responsible for adding new line to ipynb files, and that new line will not be added when saving ipynb text document. - // As a result of this, reverting (creating new edit sessions), result in ipynb files without new line at the end meaning we still end up with a saved ipynb file with changes. - // Hence we must ensure ipynb notebooks are always saved through serializer. - // But do this only if the notebook model was not already dirty. - await this.updateNotebook(this.snapshot.bytes, !this.snapshot.dirty); - } - finally { - this.isReverting = false; - this._diffInfo.set(undefined, undefined); - } - } - - private async updateNotebook(bytes: VSBuffer, saveForRevert: boolean) { - const oldEditIsFromus = this.isEditFromUs; - this.isEditFromUs = true; - const ref = await this.notebookModelResolverService.resolve(this.model.uri); - try { - const serializer = await this.getNotebookSerializer(); - const data = await serializer.dataToNotebook(bytes); - this.model.reset(data.cells, data.metadata, serializer.options); - if (saveForRevert) { - // When reverting/creating a new session ModifiedFileEntry will revert and save changes to ipynb text document first, and save the file. - // This happens in the NotebookSyncrhonizerService which is called from SimpleNotebookEditorModel (NotebookEditorModel.ts). - // However when creating new sessions, the modified File entry will not exist as its a whole new session, - // As a result we aren't able to save the ipynb text document and match the last modified date time. - // Hence the work around implemented in SimpleNotebookEditorModel does not work. - // Thus we must save the file here igorning the modified since time, but only when reverting. - await ref.object.save({ reason: SaveReason.EXPLICIT, force: true, ignoreModifiedSince: true } as any); - } - } finally { - ref.dispose(); - this.isEditFromUs = oldEditIsFromus; - } - } - - private async accept(entry: IModifiedFileEntry) { - const modifiedModel = this.modelService.getModel(entry.modifiedURI); - if (!modifiedModel) { - return; - } - const content = modifiedModel.getValue(); - await this.updateNotebook(VSBuffer.fromString(content), false); - this._diffInfo.set(undefined, undefined); - - // The original notebook model needs to be updated with the latest content. - const stream = await this.notebookService.createNotebookTextDocumentSnapshot(this.model.uri, SnapshotContext.Save, CancellationToken.None); - const originalModel = await this.getOriginalModel(entry); - await this.notebookService.restoreNotebookTextModelFromSnapshot(originalModel.uri, originalModel.viewType, stream); - } - - private async updateTextDocumentModel(entry: IModifiedFileEntry) { - const modifiedModel = this.modelService.getModel(entry.modifiedURI); - if (!modifiedModel) { - return; - } - const stream = await this.notebookService.createNotebookTextDocumentSnapshot(this.model.uri, SnapshotContext.Save, CancellationToken.None); - const buffer = await streamToBuffer(stream); - const text = new TextDecoder().decode(buffer.buffer); - this.isTextEditFromUs = true; - modifiedModel.pushEditOperations(null, [{ range: modifiedModel.getFullModelRange(), text }], () => null); - this.isTextEditFromUs = false; - } - - async getNotebookSerializer() { - const info = await this.notebookService.withNotebookDataProvider(this.model.viewType); - return info.serializer; - } - - private _originalModel?: Promise; - private async getOriginalModel(entry: IModifiedFileEntry): Promise { - if (!this._originalModel) { - this._originalModel = this.originalModelRefFactory.getOrCreate(entry, this.model.viewType).then(ref => { - if (this._store.isDisposed) { - ref.dispose(); - return ref.object; - } else { - return this._register(ref).object; - } - }); - } - return this._originalModel; - } - - private async updateNotebookModel(entry: IModifiedFileEntry, token: CancellationToken) { - const modifiedModel = this.modelService.getModel(entry.modifiedURI); - if (!modifiedModel) { - return; - } - - const modifiedModelVersion = modifiedModel.getVersionId(); - const currentModel = this.model; - const modelVersion = currentModel?.versionId ?? 0; - const modelWithChatEdits = await this.getModifiedModelForDiff(entry, token); - if (!modelWithChatEdits || token.isCancellationRequested || !currentModel) { - return; - } - const originalModel = await this.getOriginalModel(entry); - // This is the total diff from the original model to the model with chat edits. - const cellDiffInfo = (await this.computeDiff(originalModel, modelWithChatEdits, token))?.cellDiffInfo; - // This is the diff from the current model to the model with chat edits. - const cellDiffInfoToApplyEdits = (await this.computeDiff(currentModel, modelWithChatEdits, token))?.cellDiffInfo; - const currentVersion = modifiedModel.getVersionId(); - if (!cellDiffInfo || !cellDiffInfoToApplyEdits || token.isCancellationRequested || currentVersion !== modifiedModelVersion || modelVersion !== currentModel.versionId) { - return; - } - if (cellDiffInfoToApplyEdits.every(d => d.type === 'unchanged')) { - return; - } - - // All edits from here on are from us. - this.isEditFromUs = true; - try { - const edits: ICellReplaceEdit[] = []; - const mappings = new Map(); - - // First Delete. - const deletedIndexes: number[] = []; - await Promise.all(cellDiffInfoToApplyEdits.reverse().map(async diff => { - if (diff.type === 'delete') { - deletedIndexes.push(diff.originalCellIndex); - edits.push({ - editType: CellEditType.Replace, - index: diff.originalCellIndex, - cells: [], - count: 1 - }); - } - })); - if (edits.length) { - currentModel.applyEdits(edits, true, undefined, () => undefined, undefined, false); - edits.length = 0; - } - - const notebookEditor = this.notebookEditorService.retrieveExistingWidgetFromURI(this.model.uri)?.value; - - // Next insert. - cellDiffInfoToApplyEdits.reverse().forEach(diff => { - if (diff.type === 'modified' || diff.type === 'unchanged') { - mappings.set(diff.modifiedCellIndex, diff.originalCellIndex); - } - if (diff.type === 'insert') { - const originalIndex = mappings.get(diff.modifiedCellIndex - 1) ?? 0; - mappings.set(diff.modifiedCellIndex, originalIndex); - const index = currentModel.cells.length ? originalIndex + 1 : originalIndex; - const cell = modelWithChatEdits.cells[diff.modifiedCellIndex]; - const newCell: ICellDto2 = - { - source: cell.getValue(), - cellKind: cell.cellKind, - language: cell.language, - outputs: cell.outputs.map(output => output.asDto()), - mime: cell.mime, - metadata: cell.metadata, - collapseState: cell.collapseState, - internalMetadata: cell.internalMetadata - }; - edits.push({ - editType: CellEditType.Replace, - index, - cells: [newCell], - count: 0 - }); - } - }); - if (edits.length) { - currentModel.applyEdits(edits, true, undefined, () => undefined, undefined, false); - for (const edit of edits.filter(e => e.editType === CellEditType.Replace)) { - const cell = currentModel.cells[edit.index]; - if (cell) { - const cellViewModel = notebookEditor?.getCellByHandle(cell.handle); - cellViewModel?.updateEditState(CellEditState.Editing, 'chatEdit'); - } - } - edits.length = 0; - } - - // Finally update - await Promise.all(cellDiffInfoToApplyEdits.map(async diff => { - if (diff.type === 'modified') { - const cell = currentModel.cells[diff.originalCellIndex]; - // Ensure the models of these cells have been loaded before we update them. - const cellModelRef = await this.textModelService.createModelReference(cell.uri); - try { - const modifiedCell = modelWithChatEdits.cells[diff.modifiedCellIndex]; - if (modifiedCell.cellKind === cell.cellKind) { - const cellViewModel = notebookEditor?.getCellByHandle(cell.handle); - cellViewModel?.updateEditState(CellEditState.Editing, 'chatEdit'); - const textModel = cellModelRef.object.textEditorModel; - textModel.pushEditOperations(null, [ - EditOperation.replace(textModel.getFullModelRange(), modifiedCell.getValue()) - ], () => null); - } else { - const newCellDto: ICellDto2 = - { - source: modifiedCell.getValue(), - cellKind: modifiedCell.cellKind, - language: modifiedCell.language, - outputs: modifiedCell.outputs.map(output => output.asDto()), - mime: modifiedCell.mime, - metadata: modifiedCell.metadata, - collapseState: modifiedCell.collapseState, - internalMetadata: modifiedCell.internalMetadata - }; - const edit: ICellEditOperation = { - editType: CellEditType.Replace, - index: diff.originalCellIndex, - cells: [newCellDto], - count: 1 - }; - currentModel.applyEdits([edit], true, undefined, () => undefined, undefined, false); - const newCell = currentModel.cells[diff.originalCellIndex]; - const cellViewModel = notebookEditor?.getCellByHandle(newCell.handle); - cellViewModel?.updateEditState(CellEditState.Editing, 'chatEdit'); - } - } finally { - cellModelRef.dispose(); - } - } - })); - - if (edits.length) { - currentModel.applyEdits(edits, true, undefined, () => undefined, undefined, false); - } - this._diffInfo.set({ cellDiff: cellDiffInfo, modelVersion: currentModel.versionId }, undefined); - } - finally { - this.isEditFromUs = false; - } - } - private previousUriOfModelForDiff?: URI; - - private async getModifiedModelForDiff(entry: IModifiedFileEntry, token: CancellationToken): Promise { - const modifiedModel = this.modelService.getModel(entry.modifiedURI); - if (!modifiedModel) { - return; - } - const text = modifiedModel.getValue(); - const bytes = VSBuffer.fromString(text); - const uri = entry.modifiedURI.with({ scheme: `NotebookChatEditorController.modifiedScheme${Date.now().toString()}` }); - const stream = bufferToStream(bytes); - if (this.previousUriOfModelForDiff) { - this.notebookService.getNotebookTextModel(this.previousUriOfModelForDiff)?.dispose(); - } - this.previousUriOfModelForDiff = uri; - try { - const model = await this.notebookService.createNotebookTextModel(this.model.viewType, uri, stream); - if (token.isCancellationRequested) { - model.dispose(); - return; - } - this._register(model); - return model; - } catch (ex) { - this.logService.warn('NotebookChatEdit', `Failed to deserialize Notebook for ${uri.toString}, ${ex.message}`); - this.logService.debug('NotebookChatEdit', ex.toString()); - return; - } - } - - async computeDiff(original: NotebookTextModel, modified: NotebookTextModel, token: CancellationToken) { - const diffResult = await raceCancellation(this.notebookEditorWorkerService.computeDiff(original.uri, modified.uri), token); - if (!diffResult || token.isCancellationRequested) { - // after await the editor might be disposed. - return; - } - - prettyChanges(original, modified, diffResult.cellsDiff); - - return computeDiff(original, modified, diffResult); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts deleted file mode 100644 index 5d788c4c..00000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizerService.ts +++ /dev/null @@ -1,94 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, StoredFileWorkingCopy } from '../../../../../services/workingCopy/common/storedFileWorkingCopy.js'; -import { IUntitledFileWorkingCopy } from '../../../../../services/workingCopy/common/untitledFileWorkingCopy.js'; -import { IStoredFileWorkingCopySaveParticipantContext, IWorkingCopyFileService } from '../../../../../services/workingCopy/common/workingCopyFileService.js'; -import { IChatEditingService } from '../../../../chat/common/chatEditingService.js'; -import { NotebookFileWorkingCopyModel } from '../../../common/notebookEditorModel.js'; -import { INotebookSynchronizerService } from '../../../common/notebookSynchronizerService.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { NotebookSaveParticipant } from '../saveParticipants/saveParticipants.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { IProgress, IProgressStep } from '../../../../../../platform/progress/common/progress.js'; -import { IEditorService } from '../../../../../services/editor/common/editorService.js'; -import { INotebookService } from '../../../common/notebookService.js'; -import { ChatEditingModifiedDocumentEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedDocumentEntry.js'; -import { SaveReason } from '../../../../../common/editor.js'; - -class NotebookSynchronizerSaveParticipant extends NotebookSaveParticipant { - constructor( - @IEditorService editorService: IEditorService, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IFileService protected readonly _fileService: IFileService, - @INotebookService private readonly _notebookService: INotebookService, - ) { - super(editorService); - } - - override async participate(workingCopy: IStoredFileWorkingCopy, context: IStoredFileWorkingCopySaveParticipantContext, progress: IProgress, token: CancellationToken): Promise { - const sessions = this._chatEditingService.editingSessionsObs.get(); - const session = sessions.find(s => s.getEntry(workingCopy.resource)); - if (!session) { - return; - } - - if (!this._notebookService.hasSupportedNotebooks(workingCopy.resource)) { - return; - } - - const entry = session.getEntry(workingCopy.resource); - - if (entry && entry instanceof ChatEditingModifiedDocumentEntry) { - await entry.docFileEditorModel.save({ reason: SaveReason.EXPLICIT, ignoreModifiedSince: true }); - } - - const inWorkingSet = session.workingSet.has(workingCopy.resource); - - if (!(entry && entry instanceof ChatEditingModifiedDocumentEntry) && !inWorkingSet) { - // file not in working set, no need to continue - return; - } - - if (workingCopy instanceof StoredFileWorkingCopy) { - const metadata = await this._fileService.stat(workingCopy.resource); - if (workingCopy.lastResolvedFileStat) { - workingCopy.lastResolvedFileStat = { - ...workingCopy.lastResolvedFileStat, - ...metadata - }; - } - } - } -} - -export class NotebookSynchronizerService extends Disposable implements INotebookSynchronizerService { - _serviceBrand: undefined; - - constructor( - @IChatEditingService private readonly _chatEditingService: IChatEditingService, - @IFileService protected readonly _fileService: IFileService, - @INotebookService private readonly _notebookService: INotebookService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService) { - super(); - this._register(this._workingCopyFileService.addSaveParticipant(this._instantiationService.createInstance(NotebookSynchronizerSaveParticipant))); - } - - async revert(workingCopy: IStoredFileWorkingCopy | IUntitledFileWorkingCopy) { - // check if we have mirror document - const resource = workingCopy.resource; - if (!this._notebookService.hasSupportedNotebooks(workingCopy.resource)) { - return; - } - const sessions = this._chatEditingService.editingSessionsObs.get(); - const entry = sessions.map(s => s.getEntry(resource)).find(r => !!r); - if (entry instanceof ChatEditingModifiedDocumentEntry) { - await entry.docFileEditorModel.revert({ soft: true }); - } - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/debug/notebookDebugDecorations.ts b/src/vs/workbench/contrib/notebook/browser/contrib/debug/notebookDebugDecorations.ts index 3185f926..43215bff 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/debug/notebookDebugDecorations.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/debug/notebookDebugDecorations.ts @@ -10,7 +10,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { debugIconBreakpointForeground } from '../../../../debug/browser/breakpointEditorContribution.js'; import { focusedStackFrameColor, topStackFrameColor } from '../../../../debug/browser/callStackEditorContribution.js'; import { IDebugService, IStackFrame } from '../../../../debug/common/debug.js'; -import { INotebookCellDecorationOptions, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, NotebookOverviewRulerLane } from '../../notebookBrowser.js'; +import { INotebookCellDecorationOptions, INotebookDeltaCellDecoration, INotebookEditor, INotebookEditorContribution, NotebookOverviewRulerLane } from '../../notebookBrowser.js'; import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; import { runningCellRulerDecorationColor } from '../../notebookEditorWidget.js'; import { CellUri, NotebookCellExecutionState } from '../../../common/notebookCommon.js'; @@ -96,7 +96,7 @@ export class PausedCellDecorationContribution extends Disposable implements INot } private setTopFrameDecoration(handlesAndRanges: ICellAndRange[]): void { - const newDecorations = handlesAndRanges.map(({ handle, range }) => { + const newDecorations: INotebookDeltaCellDecoration[] = handlesAndRanges.map(({ handle, range }) => { const options: INotebookCellDecorationOptions = { overviewRuler: { color: topStackFrameColor, @@ -105,14 +105,17 @@ export class PausedCellDecorationContribution extends Disposable implements INot position: NotebookOverviewRulerLane.Full } }; - return { handle, options }; + return { + handle, + options + }; }); this._currentTopDecorations = this._notebookEditor.deltaCellDecorations(this._currentTopDecorations, newDecorations); } private setFocusedFrameDecoration(focusedFrameCellAndRange: ICellAndRange | undefined): void { - let newDecorations: INotebookDeltaDecoration[] = []; + let newDecorations: INotebookDeltaCellDecoration[] = []; if (focusedFrameCellAndRange) { const options: INotebookCellDecorationOptions = { overviewRuler: { @@ -122,14 +125,17 @@ export class PausedCellDecorationContribution extends Disposable implements INot position: NotebookOverviewRulerLane.Full } }; - newDecorations = [{ handle: focusedFrameCellAndRange.handle, options }]; + newDecorations = [{ + handle: focusedFrameCellAndRange.handle, + options + }]; } this._currentOtherDecorations = this._notebookEditor.deltaCellDecorations(this._currentOtherDecorations, newDecorations); } private setExecutingCellDecorations(handles: number[]): void { - const newDecorations = handles.map(handle => { + const newDecorations: INotebookDeltaCellDecoration[] = handles.map(handle => { const options: INotebookCellDecorationOptions = { overviewRuler: { color: runningCellRulerDecorationColor, @@ -138,7 +144,10 @@ export class PausedCellDecorationContribution extends Disposable implements INot position: NotebookOverviewRulerLane.Left } }; - return { handle, options }; + return { + handle, + options + }; }); this._executingCellDecorations = this._notebookEditor.deltaCellDecorations(this._executingCellDecorations, newDecorations); @@ -180,7 +189,7 @@ export class NotebookBreakpointDecorations extends Disposable implements INotebo } }; return { handle: parsed.handle, options }; - }).filter(x => !!x) as INotebookDeltaDecoration[] + }).filter(x => !!x) as INotebookDeltaCellDecoration[] : []; this._currentDecorations = this._notebookEditor.deltaCellDecorations(this._currentDecorations, newDecorations); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts index 8ce231cb..116d5d3f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts @@ -1035,7 +1035,7 @@ class NotebookAddMatchToMultiSelectionAction extends NotebookAction { constructor() { super({ id: NOTEBOOK_ADD_FIND_MATCH_TO_SELECTION_ID, - title: localize('addFindMatchToSelection', "Add Selection To Next Find Match"), + title: localize('addFindMatchToSelection', "Add Selection to Next Find Match"), precondition: ContextKeyExpr.and( ContextKeyExpr.equals('config.notebook.multiCursor.enabled', true), NOTEBOOK_IS_ACTIVE_EDITOR, diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts index 43b0f248..db11bd0f 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts @@ -176,7 +176,7 @@ registerAction2(class OpenCellOutputInEditorAction extends Action2 { if (outputViewModel?.model.outputId && notebookEditor.textModel?.uri) { // reserve notebook document reference since the active notebook editor might not be pinned so it can be replaced by the output editor const ref = await notebookModelService.resolve(notebookEditor.textModel.uri); - await openerService.open(CellUri.generateCellOutputUri(notebookEditor.textModel.uri, outputViewModel.model.outputId)); + await openerService.open(CellUri.generateCellOutputUriWithId(notebookEditor.textModel.uri, outputViewModel.model.outputId)); ref.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts index fd5ac883..1875c730 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts @@ -23,19 +23,33 @@ import { IChatWidget, IChatWidgetService } from '../../../../chat/browser/chat.j import { ChatInputPart } from '../../../../chat/browser/chatInputPart.js'; import { ChatDynamicVariableModel } from '../../../../chat/browser/contrib/chatDynamicVariables.js'; import { computeCompletionRanges } from '../../../../chat/browser/contrib/chatInputCompletions.js'; -import { ChatAgentLocation, IChatAgentService } from '../../../../chat/common/chatAgents.js'; -import { IChatRequestPasteVariableEntry } from '../../../../chat/common/chatModel.js'; +import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; +import { ChatAgentLocation } from '../../../../chat/common/constants.js'; +import { ChatContextKeys } from '../../../../chat/common/chatContextKeys.js'; +import { IBaseChatRequestVariableEntry } from '../../../../chat/common/chatModel.js'; import { chatVariableLeader } from '../../../../chat/common/chatParserTypes.js'; import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT, NOTEBOOK_CELL_OUTPUT_MIMETYPE } from '../../../common/notebookContextKeys.js'; import { INotebookKernelService } from '../../../common/notebookKernelService.js'; -import { getNotebookEditorFromEditorPane, ICellOutputViewModel, INotebookEditor } from '../../notebookBrowser.js'; +import { getNotebookEditorFromEditorPane, ICellOutputViewModel, INotebookEditor, ICellViewModel } from '../../notebookBrowser.js'; import * as icons from '../../notebookIcons.js'; import { getOutputViewModelFromId } from '../cellOutputActions.js'; import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } from '../coreActions.js'; +import { CellUri } from '../../../common/notebookCommon.js'; import './cellChatActions.js'; import { CTX_NOTEBOOK_CHAT_HAS_AGENT } from './notebookChatContext.js'; const NotebookKernelVariableKey = 'kernelVariable'; +const NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST = ['text/plain', 'text/html', + 'application/vnd.code.notebook.error', + 'application/vnd.code.notebook.stdout', + 'application/x.notebook.stdout', + 'application/x.notebook.stream', + 'application/vnd.code.notebook.stderr', + 'application/x.notebook.stderr', + 'image/png', + 'image/jpeg', + 'image/svg', +]; class NotebookChatContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.notebookChatContribution'; @@ -101,7 +115,7 @@ class NotebookChatContribution extends Disposable implements IWorkbenchContribut })); // output context - NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT.bindTo(contextKeyService).set(['image/png']); + NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT.bindTo(contextKeyService).set(NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST); } private async addKernelVariableCompletion(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { @@ -249,6 +263,7 @@ registerAction2(class CopyCellOutputAction extends Action2 { }, category: NOTEBOOK_ACTIONS_CATEGORY, icon: icons.copyIcon, + precondition: ChatContextKeys.enabled }); } @@ -295,42 +310,60 @@ registerAction2(class CopyCellOutputAction extends Action2 { const mimeType = outputViewModel.pickedMimeType?.mimeType; - if (mimeType === 'image/png') { - const chatWidgetService = accessor.get(IChatWidgetService); - - const widget = chatWidgetService.lastFocusedWidget; - if (!widget) { + const chatWidgetService = accessor.get(IChatWidgetService); + let widget = chatWidgetService.lastFocusedWidget; + if (!widget) { + const widgets = chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Panel); + if (widgets.length === 0) { return; } + widget = widgets[0]; + } + if (mimeType && NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST.includes(mimeType)) { - const imageOutput = outputViewModel.model.outputs.find(output => output.mime === mimeType); - if (!imageOutput) { + // get the cell index + const cellFromViewModelHandle = outputViewModel.cellViewModel.handle; + const cell: ICellViewModel | undefined = notebookEditor.getCellByHandle(cellFromViewModelHandle); + if (!cell) { return; } + // uri of the cell + const cellUri = cell.uri; + + // get the output index + const outputId = outputViewModel?.model.outputId; + let outputIndex: number = 0; + if (outputId !== undefined) { + // find the output index + + outputIndex = cell.outputsViewModels.findIndex(output => { + return output.model.outputId === outputId; + }); - const attachedVariables = widget.attachmentModel.attachments; - const displayName = localize('cellOutputDisplayname', 'Notebook Cell Output Image'); - let tempDisplayName = displayName; - for (let appendValue = 2; attachedVariables.some(attachment => attachment.name === tempDisplayName); appendValue++) { - tempDisplayName = `${displayName} ${appendValue}`; } + // get URI of notebook + let notebookUri = notebookEditor.textModel?.uri; + if (!notebookUri) { + // if the notebook is not found, try to parse the cell uri + const parsedCellUri = CellUri.parse(cellUri); + notebookUri = parsedCellUri?.notebook; + if (!notebookUri) { + return; + } + } + // construct the URI using the cell uri and output index + const outputCellUri = CellUri.generateCellOutputUriWithIndex(notebookUri, cellUri, outputIndex); - const imageData = imageOutput.data; - const variableEntry: IChatRequestPasteVariableEntry = { - kind: 'paste', - code: '', - language: '', - pastedLines: '', - fileName: 'notebook-cell-output-image-' + outputViewModel.model.outputId, - copiedFrom: undefined, - id: 'notebook-cell-output-image-' + outputViewModel.model.outputId, - name: tempDisplayName, - isImage: true, - value: imageData.buffer, + + + const l: IBaseChatRequestVariableEntry = { + value: outputCellUri, + id: outputCellUri.toString(), + name: outputCellUri.toString(), + isFile: true, }; - - widget.attachmentModel.addContext(variableEntry); + widget.attachmentModel.addContext(l); } } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index fd3cd47d..2296896d 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -29,7 +29,7 @@ import { localize } from '../../../../../../nls.js'; import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; -import { ChatAgentLocation } from '../../../../chat/common/chatAgents.js'; +import { ChatAgentLocation } from '../../../../chat/common/constants.js'; import { ChatModel, IChatModel } from '../../../../chat/common/chatModel.js'; import { IChatService } from '../../../../chat/common/chatService.js'; import { countWords } from '../../../../chat/common/chatWordCounter.js'; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index 57fc0cfc..b3e1fcda 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts @@ -20,7 +20,7 @@ import { IDebugService } from '../../../debug/common/debug.js'; import { CTX_INLINE_CHAT_FOCUSED } from '../../../inlineChat/common/inlineChat.js'; import { insertCell } from './cellOperations.js'; import { NotebookChatController } from './chat/notebookChatController.js'; -import { CELL_TITLE_CELL_GROUP_ID, CellToolbarOrder, INotebookActionContext, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, NotebookAction, NotebookCellAction, NotebookMultiCellAction, cellExecutionArgs, executeNotebookCondition, getContextFromActiveEditor, getContextFromUri, parseMultiCellExecutionArgs } from './coreActions.js'; +import { CELL_TITLE_CELL_GROUP_ID, CellToolbarOrder, INotebookActionContext, INotebookCellActionContext, INotebookCellToolbarActionContext, INotebookCommandContext, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, NotebookAction, NotebookCellAction, NotebookMultiCellAction, cellExecutionArgs, getContextFromActiveEditor, getContextFromUri, parseMultiCellExecutionArgs } from './coreActions.js'; import { CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, IFocusNotebookCellOptions, ScrollToRevealBehavior } from '../notebookBrowser.js'; import * as icons from '../notebookIcons.js'; import { CellKind, CellUri, NotebookSetting } from '../../common/notebookCommon.js'; @@ -144,7 +144,6 @@ registerAction2(class ExecuteNotebookAction extends NotebookAction { group: 'navigation', when: ContextKeyExpr.and( NOTEBOOK_IS_ACTIVE_EDITOR, - executeNotebookCondition, ContextKeyExpr.or(NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_SOMETHING_RUNNING.toNegated()), ContextKeyExpr.notEquals('config.notebook.globalToolbar', true) ) @@ -154,7 +153,6 @@ registerAction2(class ExecuteNotebookAction extends NotebookAction { order: -1, group: 'navigation/execute', when: ContextKeyExpr.and( - executeNotebookCondition, ContextKeyExpr.or( NOTEBOOK_INTERRUPTIBLE_KERNEL.toNegated(), NOTEBOOK_HAS_SOMETHING_RUNNING.toNegated(), diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts index 1a31b09e..8da4d2c2 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookCellDiffDecorator.ts @@ -21,7 +21,7 @@ import { Range } from '../../../../../../editor/common/core/range.js'; import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../../../scm/common/quickDiff.js'; -import { INotebookOriginalCellModelFactory } from '../../contrib/chatEdit/notebookOriginalCellModelFactory.js'; +import { INotebookOriginalCellModelFactory } from './notebookOriginalCellModelFactory.js'; //TODO: allow client to set read-only - chateditsession should set read-only while making changes export class NotebookCellDiffDecorator extends DisposableStore { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts index 2d9733f6..540f3080 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts @@ -13,13 +13,14 @@ import { NotebookCellTextModel } from '../../../common/model/notebookCellTextMod import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; import { DefaultLineHeight } from '../diffElementViewModel.js'; import { CellDiffInfo } from '../notebookDiffViewModel.js'; -import { INotebookEditor } from '../../notebookBrowser.js'; +import { INotebookEditor, NotebookOverviewRulerLane } from '../../notebookBrowser.js'; import * as DOM from '../../../../../../base/browser/dom.js'; import { MenuWorkbenchToolBar, HiddenItemStrategy } from '../../../../../../platform/actions/browser/toolbar.js'; import { MenuId } from '../../../../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { overviewRulerDeletedForeground } from '../../../../scm/common/quickDiff.js'; const ttPolicy = createTrustedTypesPolicy('notebookRenderer', { createHTML: value => value }); @@ -46,6 +47,10 @@ export class NotebookDeletedCellDecorator extends Disposable implements INoteboo if (!info) { return; } + if (info.previousIndex === -1) { + // deleted cell is before the first real cell + return 0; + } const cells = this._notebookEditor.getCellsInRange({ start: info.previousIndex, end: info.previousIndex + 1 }); if (!cells.length) { return this._notebookEditor.getLayoutInfo().height + info.offset; @@ -65,7 +70,7 @@ export class NotebookDeletedCellDecorator extends Disposable implements INoteboo const info = this.deletedCellInfos.get(deletedIndex); if (info) { - const prevIndex = info.previousIndex; + const prevIndex = info.previousIndex === -1 ? 0 : info.previousIndex; this._notebookEditor.setFocus({ start: prevIndex, end: prevIndex }); this._notebookEditor.setSelections([{ start: prevIndex, end: prevIndex }]); } @@ -139,6 +144,16 @@ export class NotebookDeletedCellDecorator extends Disposable implements INoteboo const id = accessor.addZone(notebookViewZone); accessor.layoutZone(id); this.createdViewZones.set(index, id); + + const deletedCellOverviewRulereDecorationIds = this._notebookEditor.deltaCellDecorations([], [{ + viewZoneId: id, + options: { + overviewRuler: { + color: overviewRulerDeletedForeground, + position: NotebookOverviewRulerLane.Center, + } + } + }]); this.zoneRemover.add(toDisposable(() => { if (this.createdViewZones.get(index) === id) { this.createdViewZones.delete(index); @@ -148,6 +163,8 @@ export class NotebookDeletedCellDecorator extends Disposable implements INoteboo accessor.removeZone(id); dispose(widgets); }); + + this._notebookEditor.deltaCellDecorations(deletedCellOverviewRulereDecorationIds, []); } })); }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInlineDiff.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInlineDiff.ts index 0b1668d0..c0973dd6 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInlineDiff.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInlineDiff.ts @@ -10,13 +10,17 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; import { INotebookEditorWorkerService } from '../../../common/services/notebookWorkerService.js'; -import { CellDiffInfo, computeDiff } from '../notebookDiffViewModel.js'; +import { CellDiffInfo } from '../notebookDiffViewModel.js'; import { INotebookEditorContribution, INotebookEditor } from '../../notebookBrowser.js'; import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; import { NotebookCellDiffDecorator } from './notebookCellDiffDecorator.js'; import { NotebookDeletedCellDecorator } from './notebookDeletedCellDecorator.js'; import { NotebookInsertedCellDecorator } from './notebookInsertedCellDecorator.js'; import { INotebookLoggingService } from '../../../common/notebookLoggingService.js'; +import { computeDiff } from '../../../common/notebookDiff.js'; +import { InstantiationType, registerSingleton } from '../../../../../../platform/instantiation/common/extensions.js'; +import { INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory } from './notebookOriginalModelRefFactory.js'; +import { INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory } from './notebookOriginalCellModelFactory.js'; export class NotebookInlineDiffDecorationContribution extends Disposable implements INotebookEditorContribution { static ID: string = 'workbench.notebook.inlineDiffDecoration'; @@ -162,3 +166,5 @@ export class NotebookInlineDiffDecorationContribution extends Disposable impleme } registerNotebookContribution(NotebookInlineDiffDecorationContribution.ID, NotebookInlineDiffDecorationContribution); +registerSingleton(INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory, InstantiationType.Delayed); +registerSingleton(INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.ts index 99649731..7d3121d3 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookInsertedCellDecorator.ts @@ -5,7 +5,8 @@ import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { CellDiffInfo } from '../notebookDiffViewModel.js'; -import { INotebookEditor } from '../../notebookBrowser.js'; +import { INotebookEditor, NotebookOverviewRulerLane } from '../../notebookBrowser.js'; +import { overviewRulerAddedForeground } from '../../../../scm/common/quickDiff.js'; export class NotebookInsertedCellDecorator extends Disposable { private readonly decorators = this._register(new DisposableStore()); @@ -23,7 +24,14 @@ export class NotebookInsertedCellDecorator extends Disposable { const cells = diffInfo.filter(diff => diff.type === 'insert').map((diff) => model.cells[diff.modifiedCellIndex]); const ids = this.notebookEditor.deltaCellDecorations([], cells.map(cell => ({ handle: cell.handle, - options: { className: 'nb-insertHighlight', outputClassName: 'nb-insertHighlight' } + options: { + className: 'nb-insertHighlight', outputClassName: 'nb-insertHighlight', overviewRuler: { + color: overviewRulerAddedForeground, + modelRanges: [], + includeOutput: true, + position: NotebookOverviewRulerLane.Full + } + } }))); this.clear(); this.decorators.add(toDisposable(() => { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts index f2993cbb..7475ec97 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookModifiedCellDecorator.ts @@ -5,9 +5,9 @@ import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { CellDiffInfo } from '../notebookDiffViewModel.js'; -import { INotebookEditor } from '../../notebookBrowser.js'; -import { CellKind } from '../../../common/notebookCommon.js'; +import { INotebookEditor, NotebookOverviewRulerLane } from '../../notebookBrowser.js'; import { NotebookCellTextModel } from '../../../common/model/notebookCellTextModel.js'; +import { overviewRulerModifiedForeground } from '../../../../scm/common/quickDiff.js'; export class NotebookModifiedCellDecorator extends Disposable { private readonly decorators = this._register(new DisposableStore()); @@ -23,19 +23,24 @@ export class NotebookModifiedCellDecorator extends Disposable { return; } - const modifiedMarkdownCells: NotebookCellTextModel[] = []; + const modifiedCells: NotebookCellTextModel[] = []; for (const diff of diffInfo) { if (diff.type === 'modified') { const cell = model.cells[diff.modifiedCellIndex]; - if (cell.cellKind === CellKind.Markup) { - modifiedMarkdownCells.push(cell); - } + modifiedCells.push(cell); } } - const ids = this.notebookEditor.deltaCellDecorations([], modifiedMarkdownCells.map(cell => ({ + const ids = this.notebookEditor.deltaCellDecorations([], modifiedCells.map(cell => ({ handle: cell.handle, - options: { outputClassName: 'nb-insertHighlight' } + options: { + overviewRuler: { + color: overviewRulerModifiedForeground, + modelRanges: [], + includeOutput: true, + position: NotebookOverviewRulerLane.Full + } + } }))); this.clear(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalCellModelFactory.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalCellModelFactory.ts similarity index 100% rename from src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalCellModelFactory.ts rename to src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalCellModelFactory.ts diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalModelRefFactory.ts similarity index 100% rename from src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts rename to src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalModelRefFactory.ts diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index 639e2fcd..45f4aa68 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -929,9 +929,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD deltaCellOutputContainerClassNames(diffSide: DiffSide, cellId: string, added: string[], removed: string[]) { if (diffSide === DiffSide.Original) { - this._originalWebview?.deltaCellContainerClassNames(cellId, added, removed); + this._originalWebview?.deltaCellOutputContainerClassNames(cellId, added, removed); } else { - this._modifiedWebview?.deltaCellContainerClassNames(cellId, added, removed); + this._modifiedWebview?.deltaCellOutputContainerClassNames(cellId, added, removed); } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts index 37a6a4d2..5cd62062 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffList.ts @@ -511,14 +511,14 @@ export class NotebookTextDiffList extends WorkbenchList div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; } `); } if (styles.listFocusAndSelectionForeground) { content.push(` - .monaco-drag-image, + .monaco-drag-image${suffix}, .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; } `); } @@ -551,7 +551,7 @@ export class NotebookTextDiffList extends WorkbenchList div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; } `); } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffOverviewRuler.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffOverviewRuler.ts index 912e7709..f24aabe7 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffOverviewRuler.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffOverviewRuler.ts @@ -57,7 +57,7 @@ export class NotebookDiffOverviewRuler extends Themable { })); this._register(this.themeService.onDidColorThemeChange(e => { - const colorChanged = this.applyColors(e.theme); + const colorChanged = this.applyColors(e); if (colorChanged) { this._scheduleRender(); } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts index 8ec5ec76..6ef165a4 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { IDiffResult, IDiffChange } from '../../../../../base/common/diff/diff.js'; +import { IDiffResult } from '../../../../../base/common/diff/diff.js'; import { Emitter, type IValueWithChangeEvent } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; @@ -17,11 +17,12 @@ import { DiffElementCellViewModelBase, DiffElementPlaceholderViewModel, IDiffEle import { NotebookDiffEditorEventDispatcher } from './eventDispatcher.js'; import { INotebookDiffViewModel, INotebookDiffViewModelUpdateEvent, NOTEBOOK_DIFF_ITEM_DIFF_STATE, NOTEBOOK_DIFF_ITEM_KIND } from './notebookDiffEditorBrowser.js'; import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; -import { CellUri, INotebookDiffEditorModel, INotebookDiffResult } from '../../common/notebookCommon.js'; +import { CellUri, INotebookDiffEditorModel } from '../../common/notebookCommon.js'; import { INotebookService } from '../../common/notebookService.js'; import { INotebookEditorWorkerService } from '../../common/services/notebookWorkerService.js'; import { IDiffEditorHeightCalculatorService } from './editorHeightCalculator.js'; import { raceCancellation } from '../../../../../base/common/async.js'; +import { computeDiff } from '../../common/notebookDiff.js'; export class NotebookDiffViewModel extends Disposable implements INotebookDiffViewModel, IValueWithChangeEvent { private readonly placeholderAndRelatedCells = new Map(); @@ -415,62 +416,7 @@ export type CellDiffInfo = { modifiedCellIndex: number; type: 'insert'; }; -export function computeDiff(originalModel: NotebookTextModel, modifiedModel: NotebookTextModel, diffResult: INotebookDiffResult) { - const cellChanges = diffResult.cellsDiff.changes; - const cellDiffInfo: CellDiffInfo[] = []; - let originalCellIndex = 0; - let modifiedCellIndex = 0; - let firstChangeIndex = -1; - - for (let i = 0; i < cellChanges.length; i++) { - const change = cellChanges[i]; - // common cells - - for (let j = 0; j < change.originalStart - originalCellIndex; j++) { - const originalCell = originalModel.cells[originalCellIndex + j]; - const modifiedCell = modifiedModel.cells[modifiedCellIndex + j]; - if (originalCell.getHashValue() === modifiedCell.getHashValue()) { - cellDiffInfo.push({ - originalCellIndex: originalCellIndex + j, - modifiedCellIndex: modifiedCellIndex + j, - type: 'unchanged' - }); - } else { - if (firstChangeIndex === -1) { - firstChangeIndex = cellDiffInfo.length; - } - cellDiffInfo.push({ - originalCellIndex: originalCellIndex + j, - modifiedCellIndex: modifiedCellIndex + j, - type: 'modified' - }); - } - } - - const modifiedLCS = computeModifiedLCS(change, originalModel, modifiedModel); - if (modifiedLCS.length && firstChangeIndex === -1) { - firstChangeIndex = cellDiffInfo.length; - } - - cellDiffInfo.push(...modifiedLCS); - originalCellIndex = change.originalStart + change.originalLength; - modifiedCellIndex = change.modifiedStart + change.modifiedLength; - } - - for (let i = originalCellIndex; i < originalModel.cells.length; i++) { - cellDiffInfo.push({ - originalCellIndex: i, - modifiedCellIndex: i - originalCellIndex + modifiedCellIndex, - type: 'unchanged' - }); - } - - return { - cellDiffInfo, - firstChangeIndex - }; -} function isEqual(cellDiffInfo: CellDiffInfo[], viewModels: IDiffElementViewModelBase[], model: INotebookDiffEditorModel) { if (cellDiffInfo.length !== viewModels.length) { return false; @@ -510,40 +456,6 @@ function isEqual(cellDiffInfo: CellDiffInfo[], viewModels: IDiffElementViewModel return true; } - -function computeModifiedLCS(change: IDiffChange, originalModel: NotebookTextModel, modifiedModel: NotebookTextModel) { - const result: CellDiffInfo[] = []; - // modified cells - const modifiedLen = Math.min(change.originalLength, change.modifiedLength); - - for (let j = 0; j < modifiedLen; j++) { - const isTheSame = originalModel.cells[change.originalStart + j].equal(modifiedModel.cells[change.modifiedStart + j]); - result.push({ - originalCellIndex: change.originalStart + j, - modifiedCellIndex: change.modifiedStart + j, - type: isTheSame ? 'unchanged' : 'modified' - }); - } - - for (let j = modifiedLen; j < change.originalLength; j++) { - // deletion - result.push({ - originalCellIndex: change.originalStart + j, - type: 'delete' - }); - } - - for (let j = modifiedLen; j < change.modifiedLength; j++) { - result.push({ - modifiedCellIndex: change.modifiedStart + j, - type: 'insert' - }); - } - - return result; -} - - export abstract class NotebookMultiDiffEditorItem extends MultiDiffEditorItem { constructor( originalUri: URI | undefined, diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 6c2f0f8c..38042ed1 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -334,10 +334,6 @@ flex-direction: row-reverse; } -.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row .codicon:not(.suggest-icon) { - color: var(--vscode-icon-foreground); -} - .monaco-workbench .notebookOverlay > .cell-list-container .notebook-overview-ruler-container { position: absolute; top: 0; diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index c1cb8947..10a50d99 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -105,9 +105,6 @@ import './contrib/notebookVariables/notebookInlineVariables.js'; // Diff Editor Contribution import './diff/notebookDiffActions.js'; -// Chat Edit Contributions -import './contrib/chatEdit/contribution.js'; - // Services import { editorOptionsRegistry } from '../../../../editor/common/config/editorOptions.js'; import { NotebookExecutionStateService } from './services/notebookExecutionStateServiceImpl.js'; @@ -135,8 +132,6 @@ import { NotebookMultiDiffEditorInput } from './diff/notebookMultiDiffEditorInpu import { getFormattedMetadataJSON } from '../common/model/notebookCellTextModel.js'; import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from './viewModel/notebookOutlineEntryFactory.js'; import { getFormattedNotebookMetadataJSON } from '../common/model/notebookMetadataTextModel.js'; -import { INotebookSynchronizerService } from '../common/notebookSynchronizerService.js'; -import { NotebookSynchronizerService } from './contrib/chatEdit/notebookSynchronizerService.js'; /*--------------------------------------------------------------------------------------------- */ @@ -882,7 +877,6 @@ registerSingleton(INotebookKeymapService, NotebookKeymapService, InstantiationTy registerSingleton(INotebookLoggingService, NotebookLoggingService, InstantiationType.Delayed); registerSingleton(INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory, InstantiationType.Delayed); registerSingleton(INotebookOutlineEntryFactory, NotebookOutlineEntryFactory, InstantiationType.Delayed); -registerSingleton(INotebookSynchronizerService, NotebookSynchronizerService, InstantiationType.Delayed); const schemas: IJSONSchemaMap = {}; function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema }): x is IConfigurationPropertySchema { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index b37aaca0..cf385680 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -332,11 +332,33 @@ export interface INotebookCellDecorationOptions { }; } -export interface INotebookDeltaDecoration { +export interface INotebookViewZoneDecorationOptions { + overviewRuler?: { + color: string; + position: NotebookOverviewRulerLane; + }; +} + +export interface INotebookDeltaCellDecoration { readonly handle: number; readonly options: INotebookCellDecorationOptions; } +export interface INotebookDeltaViewZoneDecoration { + readonly viewZoneId: string; + readonly options: INotebookViewZoneDecorationOptions; +} + +export function isNotebookCellDecoration(obj: unknown): obj is INotebookDeltaCellDecoration { + return !!obj && typeof (obj as INotebookDeltaCellDecoration).handle === 'number'; +} + +export function isNotebookViewZoneDecoration(obj: unknown): obj is INotebookDeltaViewZoneDecoration { + return !!obj && typeof (obj as INotebookDeltaViewZoneDecoration).viewZoneId === 'string'; +} + +export type INotebookDeltaDecoration = INotebookDeltaCellDecoration | INotebookDeltaViewZoneDecoration; + export interface INotebookDeltaCellStatusBarItems { readonly handle: number; readonly items: readonly INotebookCellStatusBarItem[]; @@ -442,6 +464,17 @@ export interface INotebookViewZoneChangeAccessor { layoutZone(id: string): void; } +export interface INotebookCellOverlay { + cell: ICellViewModel; + domNode: HTMLElement; +} + +export interface INotebookCellOverlayChangeAccessor { + addOverlay(overlay: INotebookCellOverlay): string; + removeOverlay(id: string): void; + layoutOverlay(id: string): void; +} + export type NotebookViewCellsSplice = [ number /* start */, number /* delete count */, @@ -464,6 +497,7 @@ export interface INotebookViewModel { getNearestVisibleCellIndexUpwards(index: number): number; getTrackedRange(id: string): ICellRange | null; setTrackedRange(id: string | null, newRange: ICellRange | null, newStickiness: TrackedRangeStickiness): string | null; + getOverviewRulerDecorations(): INotebookDeltaViewZoneDecoration[]; getSelections(): ICellRange[]; getCellIndex(cell: ICellViewModel): number; getMostRecentlyExecutedCell(): ICellViewModel | undefined; @@ -737,6 +771,10 @@ export interface INotebookEditor { changeViewZones(callback: (accessor: INotebookViewZoneChangeAccessor) => void): void; + changeCellOverlays(callback: (accessor: INotebookCellOverlayChangeAccessor) => void): void; + + getViewZoneLayoutInfo(id: string): { top: number; height: number } | null; + /** * Get a contribution of this editor. * @id Unique identifier of the contribution. @@ -807,7 +845,7 @@ export interface INotebookEditorDelegate extends INotebookEditor { * Hide the inset in the webview layer without removing it */ hideInset(output: IDisplayOutputViewModel): void; - deltaCellContainerClassNames(cellId: string, added: string[], removed: string[]): void; + deltaCellContainerClassNames(cellId: string, added: string[], removed: string[], cellKind: CellKind): void; } export interface IActiveNotebookEditorDelegate extends INotebookEditorDelegate { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 7234b9b4..b289cdb6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -332,6 +332,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane, I } this._handlePerfMark(perf, input, model.notebook); + this._onDidChangeControl.fire(); } catch (e) { this.logService.warn('NotebookEditorWidget#setInput failed', e); if (isEditorOpenError(e)) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 1c6d1eed..c6d652b6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -55,7 +55,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { contrastBorder, errorForeground, focusBorder, foreground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, transparent } from '../../../../platform/theme/common/colorRegistry.js'; import { EDITOR_PANE_BACKGROUND, PANEL_BORDER, SIDE_BAR_BACKGROUND } from '../../../common/theme.js'; import { debugIconStartForeground } from '../../debug/browser/debugColors.js'; -import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealRangeType, CellRevealType, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewZoneChangeAccessor, INotebookWebviewMessage, RenderOutputType, ScrollToRevealBehavior } from './notebookBrowser.js'; +import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealRangeType, CellRevealType, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookCellOverlayChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewZoneChangeAccessor, INotebookWebviewMessage, RenderOutputType, ScrollToRevealBehavior } from './notebookBrowser.js'; import { NotebookEditorExtensionsRegistry } from './notebookEditorExtensions.js'; import { INotebookEditorService } from './services/notebookEditorService.js'; import { notebookDebug } from './notebookLogger.js'; @@ -856,7 +856,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${cellBottomMargin}px; }`); styleSheets.push(` - .notebookOverlay .monaco-list .monaco-list-row:has(+ .monaco-list-row.selected) .cell-focus-indicator-bottom { + .notebookOverlay .monaco-list.selection-multiple .monaco-list-row:has(+ .monaco-list-row.selected) .cell-focus-indicator-bottom { height: ${bottomToolbarGap + cellBottomMargin}px; } `); @@ -1043,6 +1043,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._onDidScroll.fire(); this.clearActiveCellWidgets(); } + + if (e.scrollTop === e.oldScrollTop && e.scrollHeightChanged) { + this._onDidChangeLayout.fire(); + } })); this._focusTracker = this._register(DOM.trackFocus(this.getDomNode())); @@ -1617,21 +1621,21 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD store.add(cell.onCellDecorationsChanged(e => { e.added.forEach(options => { if (options.className) { - this.deltaCellContainerClassNames(cell.id, [options.className], []); + this.deltaCellContainerClassNames(cell.id, [options.className], [], cell.cellKind); } if (options.outputClassName) { - this.deltaCellContainerClassNames(cell.id, [options.outputClassName], []); + this.deltaCellContainerClassNames(cell.id, [options.outputClassName], [], cell.cellKind); } }); e.removed.forEach(options => { if (options.className) { - this.deltaCellContainerClassNames(cell.id, [], [options.className]); + this.deltaCellContainerClassNames(cell.id, [], [options.className], cell.cellKind); } if (options.outputClassName) { - this.deltaCellContainerClassNames(cell.id, [], [options.outputClassName]); + this.deltaCellContainerClassNames(cell.id, [], [options.outputClassName], cell.cellKind); } }); })); @@ -2285,8 +2289,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return ret; } - deltaCellContainerClassNames(cellId: string, added: string[], removed: string[]) { - this._webview?.deltaCellContainerClassNames(cellId, added, removed); + deltaCellContainerClassNames(cellId: string, added: string[], removed: string[], cellkind: CellKind): void { + if (cellkind === CellKind.Markup) { + this._webview?.deltaMarkupPreviewClassNames(cellId, added, removed); + } else { + this._webview?.deltaCellOutputContainerClassNames(cellId, added, removed); + } } changeModelDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null { @@ -2298,6 +2306,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD //#region View Zones changeViewZones(callback: (accessor: INotebookViewZoneChangeAccessor) => void): void { this._list.changeViewZones(callback); + this._onDidChangeLayout.fire(); + } + + getViewZoneLayoutInfo(id: string): { top: number; height: number } | null { + return this._list.getViewZoneLayoutInfo(id); + } + //#endregion + + //#region Overlay + changeCellOverlays(callback: (accessor: INotebookCellOverlayChangeAccessor) => void): void { + this._list.changeCellOverlays(callback); } //#endregion @@ -2343,6 +2362,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD //#region Cell operations/layout API private _pendingLayouts: WeakMap | null = new WeakMap(); + private _layoutDisposables: Set = new Set(); async layoutNotebookCell(cell: ICellViewModel, height: number, context?: CellLayoutContext): Promise { this._debug('layout cell', cell.handle, height); const viewIndex = this._list.getViewIndex(cell); @@ -2375,6 +2395,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return; } + const pendingLayout = this._pendingLayouts?.get(cell); this._pendingLayouts?.delete(cell); if (!this.hasEditorFocus()) { @@ -2393,15 +2414,21 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._list.updateElementHeight2(cell, height); deferred.complete(undefined); + if (pendingLayout) { + pendingLayout.dispose(); + this._layoutDisposables.delete(pendingLayout); + } }; if (this._list.inRenderingTransaction) { const layoutDisposable = DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this.getDomNode()), doLayout); - this._pendingLayouts?.set(cell, toDisposable(() => { + const disposable = toDisposable(() => { layoutDisposable.dispose(); deferred.complete(undefined); - })); + }); + this._pendingLayouts?.set(cell, disposable); + this._layoutDisposables.add(disposable); } else { doLayout(); } @@ -3267,6 +3294,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._webview?.dispose(); this._webview = null; + this._layoutDisposables.forEach(d => d.dispose()); + this.notebookEditorService.removeNotebookEditor(this); dispose(this._contributions.values()); this._contributions.clear(); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 1bc15e00..20199190 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -48,6 +48,7 @@ import { NotebookMultiDiffEditorInput } from '../diff/notebookMultiDiffEditorInp import { SnapshotContext } from '../../../../services/workingCopy/common/fileWorkingCopy.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { CancellationError } from '../../../../../base/common/errors.js'; +import { ICellRange } from '../../common/notebookRange.js'; export class NotebookProviderInfoStore extends Disposable { @@ -180,19 +181,38 @@ export class NotebookProviderInfoStore extends Disposable { }; const notebookEditorOptions = { canHandleDiff: () => !!this._configurationService.getValue(NotebookSetting.textDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized(), - canSupportResource: (resource: URI) => resource.scheme === Schemas.untitled || resource.scheme === Schemas.vscodeNotebookCell || this._fileService.hasProvider(resource) + canSupportResource: (resource: URI) => { + if (resource.scheme === Schemas.vscodeNotebookCellOutput) { + const params = new URLSearchParams(resource.query); + return params.get('openIn') === 'notebook'; + } + return resource.scheme === Schemas.untitled || resource.scheme === Schemas.vscodeNotebookCell || this._fileService.hasProvider(resource); + } }; - const notebookEditorInputFactory: EditorInputFactoryFunction = ({ resource, options }) => { - const data = CellUri.parse(resource); + const notebookEditorInputFactory: EditorInputFactoryFunction = async ({ resource, options }) => { + let data; + if (resource.scheme === Schemas.vscodeNotebookCellOutput) { + const outputUriData = CellUri.parseCellOutputUri(resource); + if (!outputUriData || !outputUriData.notebook || outputUriData.cellHandle === undefined) { + throw new Error('Invalid cell output uri'); + } + + data = { + notebook: outputUriData.notebook, + handle: outputUriData.cellHandle + }; + + } else { + data = CellUri.parse(resource); + } + let notebookUri: URI; let cellOptions: IResourceEditorInput | undefined; - let preferredResource = resource; if (data) { // resource is a notebook cell notebookUri = this.uriIdentService.asCanonicalUri(data.notebook); - preferredResource = data.notebook; cellOptions = { resource, options }; } else { notebookUri = this.uriIdentService.asCanonicalUri(resource); @@ -202,8 +222,38 @@ export class NotebookProviderInfoStore extends Disposable { cellOptions = (options as INotebookEditorOptions | undefined)?.cellOptions; } - const notebookOptions: INotebookEditorOptions = { ...options, cellOptions, viewState: undefined }; - const editor = NotebookEditorInput.getOrCreate(this._instantiationService, notebookUri, preferredResource, notebookProviderInfo.id); + let notebookOptions: INotebookEditorOptions; + + if (resource.scheme === Schemas.vscodeNotebookCellOutput) { + if (data?.handle === undefined || !data?.notebook) { + throw new Error('Invalid cell handle'); + } + + const cellUri = CellUri.generate(data.notebook, data.handle); + + cellOptions = { resource: cellUri, options }; + + const cellIndex = await this._notebookEditorModelResolverService.resolve(notebookUri) + .then(model => model.object.notebook.cells.findIndex(cell => cell.handle === data?.handle)) + .then(index => index >= 0 ? index : 0); + + const cellIndexesToRanges: ICellRange[] = [{ start: cellIndex, end: cellIndex + 1 }]; + + notebookOptions = { + ...options, + cellOptions, + viewState: undefined, + cellSelections: cellIndexesToRanges + }; + } else { + notebookOptions = { + ...options, + cellOptions, + viewState: undefined, + }; + } + const preferredResourceParam = cellOptions?.resource; + const editor = NotebookEditorInput.getOrCreate(this._instantiationService, notebookUri, preferredResourceParam, notebookProviderInfo.id); return { editor, options: notebookOptions }; }; @@ -212,7 +262,7 @@ export class NotebookProviderInfoStore extends Disposable { // untitled notebooks are disposed when they get saved. we should not hold a reference // to such a disposed notebook and therefore dispose the reference as well - ref.object.notebook.onWillDispose(() => { + Event.once(ref.object.notebook.onWillDispose)(() => { ref.dispose(); }); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts index 4a052791..80864318 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts @@ -5,17 +5,17 @@ import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; -import { IWorkerClient, Proxied } from '../../../../../base/common/worker/simpleWorker.js'; -import { createWebWorker } from '../../../../../base/browser/defaultWorkerFactory.js'; +import { IWebWorkerClient, Proxied } from '../../../../../base/common/worker/webWorker.js'; +import { createWebWorker } from '../../../../../base/browser/webWorkerFactory.js'; import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js'; import { CellUri, IMainCellDto, INotebookDiffResult, NotebookCellsChangeType, NotebookRawContentEventDto } from '../../common/notebookCommon.js'; import { INotebookService } from '../../common/notebookService.js'; -import { NotebookEditorSimpleWorker } from '../../common/services/notebookSimpleWorker.js'; +import { NotebookWorker } from '../../common/services/notebookWebWorker.js'; import { INotebookEditorWorkerService } from '../../common/services/notebookWorkerService.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { TextModel } from '../../../../../editor/common/model/textModel.js'; -import { Schemas } from '../../../../../base/common/network.js'; +import { FileAccess, Schemas } from '../../../../../base/common/network.js'; import { isEqual } from '../../../../../base/common/resources.js'; export class NotebookEditorWorkerServiceImpl extends Disposable implements INotebookEditorWorkerService { @@ -65,6 +65,7 @@ class WorkerManager extends Disposable { // this._lastWorkerUsedTime = (new Date()).getTime(); if (!this._editorWorkerClient) { this._editorWorkerClient = new NotebookWorkerClient(this._notebookService, this._modelService); + this._register(this._editorWorkerClient); } return Promise.resolve(this._editorWorkerClient); } @@ -75,7 +76,7 @@ class NotebookEditorModelManager extends Disposable { private _syncedModelsLastUsedTime: { [modelUrl: string]: number } = Object.create(null); constructor( - private readonly _proxy: Proxied, + private readonly _proxy: Proxied, private readonly _notebookService: INotebookService, private readonly _modelService: IModelService, ) { @@ -235,7 +236,7 @@ class NotebookEditorModelManager extends Disposable { } class NotebookWorkerClient extends Disposable { - private _worker: IWorkerClient | null; + private _worker: IWebWorkerClient | null; private _modelManager: NotebookEditorModelManager | null; @@ -256,29 +257,27 @@ class NotebookWorkerClient extends Disposable { return proxy.$canPromptRecommendation(modelUri.toString()); } - private _getOrCreateModelManager(proxy: Proxied): NotebookEditorModelManager { + private _getOrCreateModelManager(proxy: Proxied): NotebookEditorModelManager { if (!this._modelManager) { this._modelManager = this._register(new NotebookEditorModelManager(proxy, this._notebookService, this._modelService)); } return this._modelManager; } - protected _ensureSyncedResources(resources: URI[]): Proxied { + protected _ensureSyncedResources(resources: URI[]): Proxied { const proxy = this._getOrCreateWorker().proxy; this._getOrCreateModelManager(proxy).ensureSyncedResources(resources); return proxy; } - private _getOrCreateWorker(): IWorkerClient { + private _getOrCreateWorker(): IWebWorkerClient { if (!this._worker) { try { - this._worker = this._register(createWebWorker( - 'vs/workbench/contrib/notebook/common/services/notebookSimpleWorker', + this._worker = this._register(createWebWorker( + FileAccess.asBrowserUri('vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain.js'), 'NotebookEditorWorker' )); } catch (err) { - // logOnceWebWorkerWarning(err); - // this._worker = new SynchronousWorkerClient(new EditorSimpleWorker(new EditorWorkerHost(this), null)); throw (err); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts index 9068b002..4ae75347 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts @@ -72,11 +72,11 @@ export class CellDecorations extends CellContentPart { this.currentCell.getCellDecorations().forEach(options => { if (options.className && this.currentCell) { this.rootContainer.classList.add(options.className); - this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [options.className], []); + this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [options.className], [], this.currentCell.cellKind); } if (options.outputClassName && this.currentCell) { - this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [options.outputClassName], []); + this.notebookEditor.deltaCellContainerClassNames(this.currentCell.id, [options.outputClassName], [], this.currentCell.cellKind); } }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index dfeb1406..84b71097 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -774,7 +774,7 @@ export class CellOutputContainer extends CellContentPart { actionHandler: { callback: (content) => { if (content === 'command:workbench.action.openLargeOutput') { - this.openerService.open(CellUri.generateCellOutputUri(this.notebookEditor.textModel!.uri)); + this.openerService.open(CellUri.generateCellOutputUriWithId(this.notebookEditor.textModel!.uri)); } return; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index f58ff0aa..a23d2f8b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts @@ -79,7 +79,7 @@ export class CellEditorStatusBar extends CellContentPart { readonly showHover = (options: IHoverDelegateOptions) => { options.position = options.position ?? {}; options.position.hoverPosition = HoverPosition.ABOVE; - return hoverService.showHover(options); + return hoverService.showInstantHover(options); }; readonly placement = 'element'; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbarStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbarStickyScroll.ts index 548fae02..aee39b19 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbarStickyScroll.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbarStickyScroll.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { combinedDisposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { clamp } from '../../../../../../base/common/numbers.js'; import { ICellViewModel, INotebookEditor } from '../../notebookBrowser.js'; @@ -27,5 +27,11 @@ export function registerCellToolbarStickyScroll(notebookEditor: INotebookEditor, }; updateForScroll(); - return notebookEditor.onDidScroll(() => updateForScroll()); + const disposables: IDisposable[] = []; + disposables.push( + notebookEditor.onDidScroll(() => updateForScroll()), + notebookEditor.onDidChangeLayout(() => updateForScroll()) + ); + + return combinedDisposable(...disposables); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index e86ed7e3..b595e863 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -201,7 +201,7 @@ export class CodeCell extends Disposable { const cts = new CancellationTokenSource(); this._register({ dispose() { cts.dispose(true); } }); raceCancellation(this.viewCell.resolveTextModel(), cts.token).then(model => { - if (this._isDisposed) { + if (this._isDisposed || model?.isDisposed()) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index fdeaf59a..508e9f18 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -19,7 +19,7 @@ import { PrefixSumComputer } from '../../../../../editor/common/model/prefixSumC import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IListService, IWorkbenchListOptions, WorkbenchList } from '../../../../../platform/list/browser/listService.js'; -import { CursorAtBoundary, ICellViewModel, CellEditState, ICellOutputViewModel, CellRevealType, CellRevealRangeType, CursorAtLineBoundary, INotebookViewZoneChangeAccessor } from '../notebookBrowser.js'; +import { CursorAtBoundary, ICellViewModel, CellEditState, ICellOutputViewModel, CellRevealType, CellRevealRangeType, CursorAtLineBoundary, INotebookViewZoneChangeAccessor, INotebookCellOverlayChangeAccessor } from '../notebookBrowser.js'; import { CellViewModel, NotebookViewModel } from '../viewModel/notebookViewModelImpl.js'; import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, SelectionStateType, NOTEBOOK_EDITOR_CURSOR_LINE_BOUNDARY } from '../../common/notebookCommon.js'; import { ICellRange, cellRangesToIndexes, reduceCellRanges, cellRangesEqual } from '../../common/notebookRange.js'; @@ -36,6 +36,7 @@ import { NotebookOptions } from '../notebookOptions.js'; import { INotebookExecutionStateService } from '../../common/notebookExecutionStateService.js'; import { NotebookCellAnchor } from './notebookCellAnchor.js'; import { NotebookViewZones } from '../viewParts/notebookViewZones.js'; +import { NotebookCellOverlays } from '../viewParts/notebookCellOverlays.js'; const enum CellRevealPosition { Top, @@ -79,6 +80,7 @@ function validateWebviewBoundary(element: HTMLElement) { export class NotebookCellList extends WorkbenchList implements IDisposable, IStyleController, INotebookCellList { protected override readonly view!: NotebookCellListView; private viewZones!: NotebookViewZones; + private cellOverlays!: NotebookCellOverlays; get onWillScroll(): Event { return this.view.onWillScroll; } get rowsContainer(): HTMLElement { @@ -289,6 +291,7 @@ export class NotebookCellList extends WorkbenchList implements ID protected override createListView(container: HTMLElement, virtualDelegate: IListVirtualDelegate, renderers: IListRenderer[], viewOptions: IListViewOptions): IListView { const listView = new NotebookCellListView(container, virtualDelegate, renderers, viewOptions); this.viewZones = new NotebookViewZones(listView, this); + this.cellOverlays = new NotebookCellOverlays(listView); return listView; } @@ -340,6 +343,7 @@ export class NotebookCellList extends WorkbenchList implements ID // update whitespaces which are anchored to the model indexes this.viewZones.onCellsChanged(e); + this.cellOverlays.onCellsChanged(e); const currentRanges = this._hiddenRangeIds.map(id => this._viewModel!.getTrackedRange(id)).filter(range => range !== null) as ICellRange[]; const newVisibleViewCells: CellViewModel[] = getVisibleCells(this._viewModel!.viewCells as CellViewModel[], currentRanges); @@ -448,6 +452,8 @@ export class NotebookCellList extends WorkbenchList implements ID this._updateHiddenRangePrefixSum(newRanges); this.viewZones.onHiddenRangesChange(); this.viewZones.layout(); + this.cellOverlays.onHiddenRangesChange(); + this.cellOverlays.layout(); return false; } } @@ -461,12 +467,14 @@ export class NotebookCellList extends WorkbenchList implements ID this._updateHiddenRangePrefixSum(newRanges); // Update view zone positions after hidden ranges change this.viewZones.onHiddenRangesChange(); + this.cellOverlays.onHiddenRangesChange(); if (triggerViewUpdate) { this.updateHiddenAreasInView(oldRanges, newRanges); } this.viewZones.layout(); + this.cellOverlays.layout(); return true; } @@ -541,6 +549,7 @@ export class NotebookCellList extends WorkbenchList implements ID } this.viewZones.layout(); + this.cellOverlays.layout(); } getModelIndex(cell: CellViewModel): number | undefined { @@ -1036,7 +1045,7 @@ export class NotebookCellList extends WorkbenchList implements ID } const editorAttachedPromise = new Promise((resolve, reject) => { - element.onDidChangeEditorAttachState(() => { + Event.once(element.onDidChangeEditorAttachState)(() => { element.editorAttached ? resolve() : reject(); }); }); @@ -1168,7 +1177,8 @@ export class NotebookCellList extends WorkbenchList implements ID const wrapperBottom = this.getViewScrollBottom(); if (offset < scrollTop || offset > wrapperBottom) { - this.view.setScrollTop(offset - this.view.renderHeight / 2); + const newTop = Math.max(0, offset - this.view.renderHeight / 2); + this.view.setScrollTop(newTop); } } @@ -1231,12 +1241,14 @@ export class NotebookCellList extends WorkbenchList implements ID } this.view.updateElementHeight(index, size, anchorElementIndex); this.viewZones.layout(); + this.cellOverlays.layout(); return; } if (anchorElementIndex !== null) { this.view.updateElementHeight(index, size, anchorElementIndex); this.viewZones.layout(); + this.cellOverlays.layout(); return; } @@ -1250,12 +1262,14 @@ export class NotebookCellList extends WorkbenchList implements ID if (this._notebookCellAnchor.shouldAnchor(this.view, focus, heightDelta, this.element(index))) { this.view.updateElementHeight(index, size, focus); this.viewZones.layout(); + this.cellOverlays.layout(); return; } } this.view.updateElementHeight(index, size, null); this.viewZones.layout(); + this.cellOverlays.layout(); return; } @@ -1265,6 +1279,16 @@ export class NotebookCellList extends WorkbenchList implements ID } } + changeCellOverlays(callback: (accessor: INotebookCellOverlayChangeAccessor) => void): void { + if (this.cellOverlays.changeCellOverlays(callback)) { + this.cellOverlays.layout(); + } + } + + getViewZoneLayoutInfo(viewZoneId: string): { height: number; top: number } | null { + return this.viewZones.getViewZoneLayoutInfo(viewZoneId); + } + // override override domFocus() { const focused = this.getFocusedElements()[0]; @@ -1346,14 +1370,14 @@ export class NotebookCellList extends WorkbenchList implements ID if (styles.listFocusAndSelectionBackground) { content.push(` - .monaco-drag-image, + .monaco-drag-image${suffix}, .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; } `); } if (styles.listFocusAndSelectionForeground) { content.push(` - .monaco-drag-image, + .monaco-drag-image${suffix}, .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; } `); } @@ -1386,7 +1410,7 @@ export class NotebookCellList extends WorkbenchList implements ID if (styles.listFocusOutline) { content.push(` - .monaco-drag-image, + .monaco-drag-image${suffix}, .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; } `); } @@ -1438,6 +1462,7 @@ export class NotebookCellList extends WorkbenchList implements ID this._localDisposableStore.dispose(); this._notebookCellAnchor.dispose(); this.viewZones.dispose(); + this.cellOverlays.dispose(); super.dispose(); // un-ref diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts index 7034d6bb..8d64212d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts @@ -306,18 +306,11 @@ export class NotebookCellListView extends ListView { removeWhitespace(id: string): void { const scrollTop = this.scrollTop; const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); - const currentPosition = this.notebookRangeMap.getWhitespacePosition(id); - - if (currentPosition > scrollTop) { - this.notebookRangeMap.removeWhitespace(id); - this.render(previousRenderRange, scrollTop, this.lastRenderHeight, undefined, undefined, false); - this._rerender(scrollTop, this.renderHeight, false); - this.eventuallyUpdateScrollDimensions(); - } else { - this.notebookRangeMap.removeWhitespace(id); - this.eventuallyUpdateScrollDimensions(); - } + this.notebookRangeMap.removeWhitespace(id); + this.render(previousRenderRange, scrollTop, this.lastRenderHeight, undefined, undefined, false); + this._rerender(scrollTop, this.renderHeight, false); + this.eventuallyUpdateScrollDimensions(); } getWhitespacePosition(id: string): number { diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts index 94ced5b8..0d1adcd6 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts @@ -16,7 +16,7 @@ import { Selection } from '../../../../../editor/common/core/selection.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchListOptionsUpdate } from '../../../../../platform/list/browser/listService.js'; -import { CellRevealRangeType, CellRevealType, ICellOutputViewModel, ICellViewModel, INotebookViewZoneChangeAccessor } from '../notebookBrowser.js'; +import { CellRevealRangeType, CellRevealType, ICellOutputViewModel, ICellViewModel, INotebookCellOverlayChangeAccessor, INotebookViewZoneChangeAccessor } from '../notebookBrowser.js'; import { CellPartsCollection } from './cellPart.js'; import { CellViewModel, NotebookViewModel } from '../viewModel/notebookViewModelImpl.js'; import { ICellRange } from '../../common/notebookRange.js'; @@ -66,6 +66,8 @@ export interface INotebookCellList extends ICoordinatesConverter { revealOffsetInCenterIfOutsideViewport(offset: number): void; setHiddenAreas(_ranges: ICellRange[], triggerViewUpdate: boolean): boolean; changeViewZones(callback: (accessor: INotebookViewZoneChangeAccessor) => void): void; + changeCellOverlays(callback: (accessor: INotebookCellOverlayChangeAccessor) => void): void; + getViewZoneLayoutInfo(viewZoneId: string): { height: number; top: number } | null; domElementOfElement(element: ICellViewModel): HTMLElement | null; focusView(): void; triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 26e9dbba..b6c10143 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -762,7 +762,7 @@ export class BackLayerWebView extends Themable { } } - this.openerService.open(CellUri.generateCellOutputUri(this.documentUri, outputId)); + this.openerService.open(CellUri.generateCellOutputUriWithId(this.documentUri, outputId)); return; } if (uri.path === 'cellOutput.enableScrolling') { @@ -794,7 +794,8 @@ export class BackLayerWebView extends Themable { 'workbench.action.openSettings', '_notebook.selectKernel', // TODO@rebornix explore open output channel with name command - 'jupyter.viewOutput' + 'jupyter.viewOutput', + 'jupyter.createPythonEnvAndSelectController', ], }); return; @@ -1854,14 +1855,24 @@ export class BackLayerWebView extends Themable { } - deltaCellContainerClassNames(cellId: string, added: string[], removed: string[]) { + deltaCellOutputContainerClassNames(cellId: string, added: string[], removed: string[]) { this._sendMessageToWebview({ type: 'decorations', cellId, addedClassNames: added, removedClassNames: removed }); + } + deltaMarkupPreviewClassNames(cellId: string, added: string[], removed: string[]) { + if (this.markupPreviewMapping.get(cellId)) { + this._sendMessageToWebview({ + type: 'markupDecorations', + cellId, + addedClassNames: added, + removedClassNames: removed + }); + } } updateOutputRenderers() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 6c0a4f51..84589a5e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -84,7 +84,7 @@ export class NotebookCellListDelegate extends Disposable implements IListVirtual } } -abstract class AbstractCellRenderer { +abstract class AbstractCellRenderer extends Disposable { protected readonly editorOptions: CellEditorOptions; constructor( @@ -99,11 +99,12 @@ abstract class AbstractCellRenderer { language: string, protected dndController: CellDragAndDropController | undefined ) { - this.editorOptions = new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(language), this.notebookEditor.notebookOptions, configurationService); + super(); + this.editorOptions = this._register(new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(language), this.notebookEditor.notebookOptions, configurationService)); } - dispose() { - this.editorOptions.dispose(); + override dispose() { + super.dispose(); this.dndController = undefined; } } @@ -199,7 +200,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen editorContainer, foldingIndicator, templateDisposables, - elementDisposables: new DisposableStore(), + elementDisposables: templateDisposables.add(new DisposableStore()), cellParts, toJSON: () => { return {}; } }; @@ -225,7 +226,6 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen } disposeTemplate(templateData: MarkdownCellRenderTemplate): void { - templateData.elementDisposables.dispose(); templateData.templateDisposables.dispose(); } @@ -366,7 +366,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende outputShowMoreContainer, editor, templateDisposables, - elementDisposables: new DisposableStore(), + elementDisposables: templateDisposables.add(new DisposableStore()), cellParts, toJSON: () => { return {}; } }; @@ -397,7 +397,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende } disposeTemplate(templateData: CodeCellRenderTemplate): void { - templateData.templateDisposables.clear(); + templateData.templateDisposables.dispose(); } disposeElement(element: ICellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 073737f5..26149b87 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -305,7 +305,7 @@ export interface IUpdateRenderersMessage { } export interface IUpdateDecorationsMessage { - readonly type: 'decorations'; + readonly type: 'decorations' | 'markupDecorations'; readonly cellId: string; readonly addedClassNames: readonly string[]; readonly removedClassNames: readonly string[]; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 5b7077bf..ac3d98fd 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -1780,6 +1780,16 @@ async function webviewPreloads(ctx: PreloadContext) { outputContainer?.classList.remove(...event.data.removedClassNames); break; } + case 'markupDecorations': { + const markupCell = window.document.getElementById(event.data.cellId); + // The cell may not have been added yet if it is out of view. + // Decorations will be added when the cell is shown. + if (markupCell) { + markupCell?.classList.add(...event.data.addedClassNames); + markupCell?.classList.remove(...event.data.removedClassNames); + } + break; + } case 'customKernelMessage': onDidReceiveKernelMessage.fire(event.data.message); break; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index 8b9ca509..56a7c140 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -23,7 +23,7 @@ import { FoldingRegions } from '../../../../../editor/contrib/folding/browser/fo import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; import { CellFindMatchModel } from '../contrib/find/findModel.js'; -import { CellEditState, CellFindMatchWithIndex, CellFoldingState, EditorFoldingStateDelegate, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, IModelDecorationsChangeAccessor, INotebookDeltaCellStatusBarItems, INotebookDeltaDecoration, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewModel } from '../notebookBrowser.js'; +import { CellEditState, CellFindMatchWithIndex, CellFoldingState, EditorFoldingStateDelegate, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, IModelDecorationsChangeAccessor, INotebookDeltaCellStatusBarItems, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookViewModel, INotebookDeltaDecoration, isNotebookCellDecoration, INotebookDeltaViewZoneDecoration } from '../notebookBrowser.js'; import { NotebookLayoutInfo, NotebookMetadataChangedEvent } from '../notebookViewEvents.js'; import { NotebookCellSelectionCollection } from './cellSelectionCollection.js'; import { CodeCellViewModel } from './codeCellViewModel.js'; @@ -189,6 +189,9 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD private _decorationIdToCellMap = new Map(); private _statusBarItemIdToCellMap = new Map(); + private _lastOverviewRulerDecorationId: number = 0; + private _overviewRulerDecorations = new Map(); + constructor( public viewType: string, private _notebook: NotebookTextModel, @@ -495,6 +498,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._hiddenRanges; } + getOverviewRulerDecorations(): INotebookDeltaViewZoneDecoration[] { + return Array.from(this._overviewRulerDecorations.values()); + } + getCellByHandle(handle: number) { return this._handleToViewCellMapping.get(handle); } @@ -723,18 +730,29 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD cell?.deltaCellDecorations([id], []); this._decorationIdToCellMap.delete(id); } + + if (this._overviewRulerDecorations.has(id)) { + this._overviewRulerDecorations.delete(id); + } }); const result: string[] = []; newDecorations.forEach(decoration => { - const cell = this.getCellByHandle(decoration.handle); - const ret = cell?.deltaCellDecorations([], [decoration.options]) || []; - ret.forEach(id => { - this._decorationIdToCellMap.set(id, decoration.handle); - }); + if (isNotebookCellDecoration(decoration)) { + const cell = this.getCellByHandle(decoration.handle); + const ret = cell?.deltaCellDecorations([], [decoration.options]) || []; + ret.forEach(id => { + this._decorationIdToCellMap.set(id, decoration.handle); + }); + result.push(...ret); + } else { + const id = ++this._lastOverviewRulerDecorationId; + const decorationId = `_overview_${this.id};${id}`; + this._overviewRulerDecorations.set(decorationId, decoration); + result.push(decorationId); + } - result.push(...ret); }); return result; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookCellOverlays.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookCellOverlays.ts new file mode 100644 index 00000000..82c37b94 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookCellOverlays.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createFastDomNode, FastDomNode } from '../../../../../base/browser/fastDomNode.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { localize2 } from '../../../../../nls.js'; +import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; +import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { IsDevelopmentContext } from '../../../../../platform/contextkey/common/contextkeys.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { CellKind } from '../../common/notebookCommon.js'; +import { getNotebookEditorFromEditorPane, INotebookCellOverlay, INotebookCellOverlayChangeAccessor, INotebookViewCellsUpdateEvent } from '../notebookBrowser.js'; +import { NotebookCellListView } from '../view/notebookCellListView.js'; +import { CellViewModel } from '../viewModel/notebookViewModelImpl.js'; + +interface INotebookCellOverlayWidget { + overlayId: string; + overlay: INotebookCellOverlay; + domNode: FastDomNode; +} + +export class NotebookCellOverlays extends Disposable { + private _lastOverlayId = 0; + public domNode: FastDomNode; + private _overlays: { [key: string]: INotebookCellOverlayWidget } = Object.create(null); + + constructor( + private readonly listView: NotebookCellListView + ) { + super(); + this.domNode = createFastDomNode(document.createElement('div')); + this.domNode.setClassName('cell-overlays'); + this.domNode.setPosition('absolute'); + this.domNode.setAttribute('role', 'presentation'); + this.domNode.setAttribute('aria-hidden', 'true'); + this.domNode.setWidth('100%'); + + this.listView.containerDomNode.appendChild(this.domNode.domNode); + } + + changeCellOverlays(callback: (changeAccessor: INotebookCellOverlayChangeAccessor) => void): boolean { + let overlaysHaveChanged = false; + const changeAccessor: INotebookCellOverlayChangeAccessor = { + addOverlay: (overlay: INotebookCellOverlay): string => { + overlaysHaveChanged = true; + return this._addOverlay(overlay); + }, + removeOverlay: (id: string): void => { + overlaysHaveChanged = true; + this._removeOverlay(id); + }, + layoutOverlay: (id: string): void => { + overlaysHaveChanged = true; + this._layoutOverlay(id); + } + }; + + callback(changeAccessor); + + return overlaysHaveChanged; + } + + onCellsChanged(e: INotebookViewCellsUpdateEvent): void { + this.layout(); + } + + onHiddenRangesChange() { + this.layout(); + } + + layout() { + for (const id in this._overlays) { + this._layoutOverlay(id); + } + } + + private _addOverlay(overlay: INotebookCellOverlay): string { + const overlayId = `${++this._lastOverlayId}`; + + const overlayWidget = { + overlayId, + overlay, + domNode: createFastDomNode(overlay.domNode) + }; + + this._overlays[overlayId] = overlayWidget; + overlayWidget.domNode.setClassName('cell-overlay'); + overlayWidget.domNode.setPosition('absolute'); + this.domNode.appendChild(overlayWidget.domNode); + + return overlayId; + } + + private _removeOverlay(id: string): void { + const overlay = this._overlays[id]; + if (overlay) { + // overlay.overlay.dispose(); + try { + this.domNode.removeChild(overlay.domNode); + } catch { + // no op + } + + delete this._overlays[id]; + } + } + + private _layoutOverlay(id: string): void { + const overlay = this._overlays[id]; + if (!overlay) { + return; + } + + const isInHiddenRanges = this._isInHiddenRanges(overlay); + if (isInHiddenRanges) { + overlay.domNode.setDisplay('none'); + return; + } + + overlay.domNode.setDisplay('block'); + const index = this.listView.indexOf(overlay.overlay.cell as CellViewModel); + if (index === -1) { + // should not happen + return; + } + + const top = this.listView.elementTop(index); + overlay.domNode.setTop(top); + } + + private _isInHiddenRanges(zone: INotebookCellOverlayWidget) { + const index = this.listView.indexOf(zone.overlay.cell as CellViewModel); + if (index === -1) { + return true; + } + + return false; + } +} + + + +class ToggleNotebookCellOverlaysDeveloperAction extends Action2 { + static cellOverlayIds: string[] = []; + constructor() { + super({ + id: 'notebook.developer.addCellOverlays', + title: localize2('workbench.notebook.developer.addCellOverlays', "Toggle Notebook Cell Overlays"), + category: Categories.Developer, + precondition: IsDevelopmentContext, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!editor) { + return; + } + + if (ToggleNotebookCellOverlaysDeveloperAction.cellOverlayIds.length > 0) { + // remove all view zones + editor.changeCellOverlays(accessor => { + ToggleNotebookCellOverlaysDeveloperAction.cellOverlayIds.forEach(id => { + accessor.removeOverlay(id); + }); + ToggleNotebookCellOverlaysDeveloperAction.cellOverlayIds = []; + }); + } else { + editor.changeCellOverlays(accessor => { + const cells = editor.getCellsInRange(); + if (cells.length === 0) { + return; + } + + const cellOverlayIds: string[] = []; + for (let i = 0; i < cells.length; i++) { + if (cells[i].cellKind !== CellKind.Markup) { + continue; + } + const domNode = document.createElement('div'); + domNode.innerText = `Cell Overlay ${i}`; + domNode.style.top = '10px'; + domNode.style.right = '10px'; + domNode.style.backgroundColor = 'rgba(0, 255, 0, 0.5)'; + const overlayId = accessor.addOverlay({ + cell: cells[i], + domNode: domNode, + }); + + cellOverlayIds.push(overlayId); + } + ToggleNotebookCellOverlaysDeveloperAction.cellOverlayIds = cellOverlayIds; + }); + } + } +} + +registerAction2(ToggleNotebookCellOverlaysDeveloperAction); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index d59ee4f4..ea8227a1 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -47,7 +47,6 @@ export class NotebookStickyLine extends Disposable { this.toggleFoldRange(currentFoldingState); } })); - } private toggleFoldRange(currentState: CellFoldingState) { @@ -212,13 +211,17 @@ export class NotebookStickyScroll extends Disposable { await notebookCellOutline.computeFullSymbols(CancellationToken.None); // Initial content update - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight())); + const computed = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); + this.updateContent(computed); // Set up outline change listener this._disposables.add(notebookCellOutline.onDidChange(() => { - const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); - if (!this.compareStickyLineMaps(recompute, this.currentStickyLines)) { - this.updateContent(recompute); + const computed = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); + if (!this.compareStickyLineMaps(computed, this.currentStickyLines)) { + this.updateContent(computed); + } else { + // if we don't end up updating the content, we need to avoid leaking the map + this.disposeStickyLineMap(computed); } })); @@ -226,21 +229,36 @@ export class NotebookStickyScroll extends Disposable { this._disposables.add(this.notebookEditor.onDidAttachViewModel(async () => { // ensure recompute symbols when view model changes -- could be missed if outline is closed await notebookCellOutline.computeFullSymbols(CancellationToken.None); - this.updateContent(computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight())); + + const computed = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); + this.updateContent(computed); })); this._disposables.add(this.notebookEditor.onDidScroll(() => { const d = new Delayer(100); d.trigger(() => { d.dispose(); - const recompute = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); - if (!this.compareStickyLineMaps(recompute, this.currentStickyLines)) { - this.updateContent(recompute); + + const computed = computeContent(this.notebookEditor, this.notebookCellList, notebookCellOutline.entries, this.getCurrentStickyHeight()); + if (!this.compareStickyLineMaps(computed, this.currentStickyLines)) { + this.updateContent(computed); + } else { + // if we don't end up updating the content, we need to avoid leaking the map + this.disposeStickyLineMap(computed); } }); })); } + // Add helper method to dispose a map of sticky lines + private disposeStickyLineMap(map: Map) { + map.forEach(value => { + if (value.line) { + value.line.dispose(); + } + }); + } + // take in an cell index, and get the corresponding outline entry static getVisibleOutlineEntry(visibleIndex: number, notebookOutlineEntries: OutlineEntry[]): OutlineEntry | undefined { let left = 0; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts index d2467be2..0cee6535 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts @@ -72,6 +72,7 @@ export class NotebookEditorContextKeys { dispose(): void { this._disposables.dispose(); this._viewModelDisposables.dispose(); + this._selectedKernelDisposables.dispose(); this._notebookKernelCount.reset(); this._notebookKernelSourceCount.reset(); this._interruptibleKernel.reset(); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 3d2c44c6..3e08af27 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -33,7 +33,8 @@ import { URI } from '../../../../../base/common/uri.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { INotebookTextModel } from '../../common/notebookCommon.js'; import { SELECT_KERNEL_ID } from '../controller/coreActions.js'; -import { EnablementState } from '../../../../services/extensionManagement/common/extensionManagement.js'; +import { EnablementState, IExtensionManagementServerService } from '../../../../services/extensionManagement/common/extensionManagement.js'; +import { areSameExtensions } from '../../../../../platform/extensionManagement/common/extensionManagementUtil.js'; type KernelPick = IQuickPickItem & { kernel: INotebookKernel }; function isKernelPick(item: QuickPickInput): item is KernelPick { @@ -105,7 +106,8 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { protected readonly _logService: ILogService, protected readonly _extensionWorkbenchService: IExtensionsWorkbenchService, protected readonly _extensionService: IExtensionService, - protected readonly _commandService: ICommandService + protected readonly _commandService: ICommandService, + protected readonly _extensionManagementServerService: IExtensionManagementServerService ) { } async showQuickPick(editor: IActiveNotebookEditor, wantedId?: string, skipAutoRun?: boolean): Promise { @@ -254,6 +256,7 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { await this._showKernelExtension( this._extensionWorkbenchService, this._extensionService, + this._extensionManagementServerService, editor.textModel.viewType, [] ); @@ -262,6 +265,7 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { await this._showKernelExtension( this._extensionWorkbenchService, this._extensionService, + this._extensionManagementServerService, editor.textModel.viewType, pick.extensionIds, this._productService.quality !== 'stable' @@ -281,33 +285,48 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { protected async _showKernelExtension( extensionWorkbenchService: IExtensionsWorkbenchService, extensionService: IExtensionService, + extensionManagementServerService: IExtensionManagementServerService, viewType: string, extIds: string[], isInsiders?: boolean ) { // If extension id is provided attempt to install the extension as the user has requested the suggested ones be installed const extensionsToInstall: IExtension[] = []; + const extensionsToInstallOnRemote: IExtension[] = []; const extensionsToEnable: IExtension[] = []; for (const extId of extIds) { const extension = (await extensionWorkbenchService.getExtensions([{ id: extId }], CancellationToken.None))[0]; if (extension.enablementState === EnablementState.DisabledGlobally || extension.enablementState === EnablementState.DisabledWorkspace || extension.enablementState === EnablementState.DisabledByEnvironment) { extensionsToEnable.push(extension); - } else { + } else if (!extensionWorkbenchService.installed.some(e => areSameExtensions(e.identifier, extension.identifier))) { + // Install this extension only if it hasn't already been installed. const canInstall = await extensionWorkbenchService.canInstall(extension); if (canInstall === true) { extensionsToInstall.push(extension); } + } else if (extensionManagementServerService.remoteExtensionManagementServer) { + // already installed, check if it should be installed on remote since we are not getting any kernels or kernel providers. + if (extensionWorkbenchService.installed.some(e => areSameExtensions(e.identifier, extension.identifier) && e.server === extensionManagementServerService.remoteExtensionManagementServer)) { + // extension exists on remote server. should not happen + continue; + } else { + // extension doesn't exist on remote server + const canInstall = await extensionWorkbenchService.canInstall(extension); + if (canInstall) { + extensionsToInstallOnRemote.push(extension); + } + } } } - if (extensionsToInstall.length || extensionsToEnable.length) { + if (extensionsToInstall.length || extensionsToEnable.length || extensionsToInstallOnRemote.length) { await Promise.all([...extensionsToInstall.map(async extension => { await extensionWorkbenchService.install( extension, { installPreReleaseVersion: isInsiders ?? false, - context: { skipWalkthrough: true } + context: { skipWalkthrough: true }, }, ProgressLocation.Notification ); @@ -325,6 +344,8 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { default: break; } + }), ...extensionsToInstallOnRemote.map(async extension => { + await extensionWorkbenchService.installInServer(extension, this._extensionManagementServerService.remoteExtensionManagementServer!); })]); await extensionService.activateByEvent(`onNotebook:${viewType}`); @@ -435,6 +456,7 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { @ILogService _logService: ILogService, @IExtensionsWorkbenchService _extensionWorkbenchService: IExtensionsWorkbenchService, @IExtensionService _extensionService: IExtensionService, + @IExtensionManagementServerService _extensionManagementServerService: IExtensionManagementServerService, @ICommandService _commandService: ICommandService, @INotebookKernelHistoryService private readonly _notebookKernelHistoryService: INotebookKernelHistoryService, @IOpenerService private readonly _openerService: IOpenerService @@ -449,6 +471,7 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { _extensionWorkbenchService, _extensionService, _commandService, + _extensionManagementServerService, ); } @@ -609,6 +632,7 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { await this._showKernelExtension( this._extensionWorkbenchService, this._extensionService, + this._extensionManagementServerService, editor.textModel.viewType, [] ); @@ -617,6 +641,7 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { await this._showKernelExtension( this._extensionWorkbenchService, this._extensionService, + this._extensionManagementServerService, editor.textModel.viewType, selectedKernelPickItem.extensionIds, this._productService.quality !== 'stable' diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts index 2b2bd8bd..b4c6ca0c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts @@ -140,11 +140,13 @@ export class NotebooKernelActionViewItem extends ActionViewItem { @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, @INotebookKernelHistoryService private readonly _notebookKernelHistoryService: INotebookKernelHistoryService, ) { + const action = new Action('fakeAction', undefined, ThemeIcon.asClassName(selectKernelIcon), true, (event) => actualAction.run(event)); super( undefined, - new Action('fakeAction', undefined, ThemeIcon.asClassName(selectKernelIcon), true, (event) => actualAction.run(event)), + action, { ...options, label: false, icon: true } ); + this._register(action); this._register(_editor.onDidChangeModel(this._update, this)); this._register(_notebookKernelService.onDidAddKernel(this._update, this)); this._register(_notebookKernelService.onDidRemoveKernel(this._update, this)); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts index 4085b53a..1bab34e9 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts @@ -111,6 +111,45 @@ export class NotebookOverviewRuler extends Themable { currentFrom += cellHeight; } + + const overviewRulerDecorations = viewModel.getOverviewRulerDecorations(); + + for (let i = 0; i < overviewRulerDecorations.length; i++) { + const decoration = overviewRulerDecorations[i]; + if (!decoration.options.overviewRuler) { + continue; + } + const viewZoneInfo = this.notebookEditor.getViewZoneLayoutInfo(decoration.viewZoneId); + + if (!viewZoneInfo) { + continue; + } + + const fillStyle = this.getColor(decoration.options.overviewRuler.color) ?? '#000000'; + let x = 0; + switch (decoration.options.overviewRuler.position) { + case NotebookOverviewRulerLane.Left: + x = 0; + break; + case NotebookOverviewRulerLane.Center: + x = laneWidth; + break; + case NotebookOverviewRulerLane.Right: + x = laneWidth * 2; + break; + default: + break; + } + + const width = decoration.options.overviewRuler.position === NotebookOverviewRulerLane.Full ? laneWidth * 3 : laneWidth; + + ctx.fillStyle = fillStyle; + + const viewZoneHeight = (viewZoneInfo.height / scrollHeight) * ratio * height; + const viewZoneTop = (viewZoneInfo.top / scrollHeight) * ratio * height; + + ctx.fillRect(x, viewZoneTop, width, viewZoneHeight); + } } } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts index 8645d66d..b0ade627 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts @@ -6,7 +6,13 @@ import { FastDomNode, createFastDomNode } from '../../../../../base/browser/fastDomNode.js'; import { onUnexpectedError } from '../../../../../base/common/errors.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { INotebookViewCellsUpdateEvent, INotebookViewZone, INotebookViewZoneChangeAccessor } from '../notebookBrowser.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { localize2 } from '../../../../../nls.js'; +import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; +import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { IsDevelopmentContext } from '../../../../../platform/contextkey/common/contextkeys.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { getNotebookEditorFromEditorPane, INotebookViewCellsUpdateEvent, INotebookViewZone, INotebookViewZoneChangeAccessor } from '../notebookBrowser.js'; import { NotebookCellListView } from '../view/notebookCellListView.js'; import { ICoordinatesConverter } from '../view/notebookRenderingCommon.js'; import { CellViewModel } from '../viewModel/notebookViewModelImpl.js'; @@ -66,6 +72,16 @@ export class NotebookViewZones extends Disposable { return zonesHaveChanged; } + getViewZoneLayoutInfo(viewZoneId: string): { height: number; top: number } | null { + const zoneWidget = this._zones[viewZoneId]; + if (!zoneWidget) { + return null; + } + const top = this.listView.getWhitespacePosition(zoneWidget.whitespaceId); + const height = zoneWidget.zone.heightInPx; + return { height: height, top: top }; + } + onCellsChanged(e: INotebookViewCellsUpdateEvent): void { const splices = e.splices.slice().reverse(); splices.forEach(splice => { @@ -141,6 +157,16 @@ export class NotebookViewZones extends Disposable { private _removeZone(id: string): void { this.listView.removeWhitespace(id); + const zoneWidget = this._zones[id]; + if (zoneWidget) { + // safely remove the dom node from its parent + try { + this.domNode.removeChild(zoneWidget.domNode); + } catch { + // ignore the error + } + } + delete this._zones[id]; } @@ -186,3 +212,59 @@ function safeInvoke1Arg(func: Function, arg1: any): void { onUnexpectedError(e); } } + +class ToggleNotebookViewZoneDeveloperAction extends Action2 { + static viewZoneIds: string[] = []; + constructor() { + super({ + id: 'notebook.developer.addViewZones', + title: localize2('workbench.notebook.developer.addViewZones', "Toggle Notebook View Zones"), + category: Categories.Developer, + precondition: IsDevelopmentContext, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + + if (!editor) { + return; + } + + if (ToggleNotebookViewZoneDeveloperAction.viewZoneIds.length > 0) { + // remove all view zones + editor.changeViewZones(accessor => { + // remove all view zones in reverse order, to follow how we handle this in the prod code + ToggleNotebookViewZoneDeveloperAction.viewZoneIds.reverse().forEach(id => { + accessor.removeZone(id); + }); + ToggleNotebookViewZoneDeveloperAction.viewZoneIds = []; + }); + } else { + editor.changeViewZones(accessor => { + const cells = editor.getCellsInRange(); + if (cells.length === 0) { + return; + } + + const viewZoneIds: string[] = []; + for (let i = 0; i < cells.length; i++) { + const domNode = document.createElement('div'); + domNode.innerText = `View Zone ${i}`; + domNode.style.backgroundColor = 'rgba(0, 255, 0, 0.5)'; + const viewZoneId = accessor.addZone({ + afterModelPosition: i, + heightInPx: 200, + domNode: domNode, + }); + viewZoneIds.push(viewZoneId); + } + ToggleNotebookViewZoneDeveloperAction.viewZoneIds = viewZoneIds; + }); + } + } +} + +registerAction2(ToggleNotebookViewZoneDeveloperAction); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 8f84a01b..3970dd6b 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -209,9 +209,6 @@ export class NotebookCellTextModel extends Disposable implements ICell { this._outputs = outputs.map(op => new NotebookCellOutputTextModel(op)); this._metadata = metadata ?? {}; this._internalMetadata = internalMetadata ?? {}; - this._internalMetadata.cellId = this._internalMetadata.cellId ?? - this._metadata.id as string ?? - uri.fragment; } enableAutoLanguageDetection() { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index a7f1c786..237e0ff8 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -31,6 +31,7 @@ import { NotebookCellTextModel } from './notebookCellTextModel.js'; class StackOperation implements IWorkspaceUndoRedoElement { type: UndoRedoElementType.Workspace; + tag = 'notebookUndoRedoElement'; public get code() { return this._operations.length === 1 ? this._operations[0].code : 'undoredo.notebooks.stackOperation'; @@ -121,6 +122,7 @@ class StackOperation implements IWorkspaceUndoRedoElement { class NotebookOperationManager { private _pendingStackOperation: StackOperation | null = null; + private _isAppending: boolean = false; constructor( private readonly _textModel: NotebookTextModel, private _undoService: IUndoRedoService, @@ -136,14 +138,28 @@ class NotebookOperationManager { pushStackElement(alternativeVersionId: string, selectionState: ISelectionState | undefined) { if (this._pendingStackOperation && !this._pendingStackOperation.isEmpty) { this._pendingStackOperation.pushEndState(alternativeVersionId, selectionState); - this._undoService.pushElement(this._pendingStackOperation, this._pendingStackOperation.undoRedoGroup); + if (!this._isAppending) { + this._undoService.pushElement(this._pendingStackOperation, this._pendingStackOperation.undoRedoGroup); + } } + this._isAppending = false; this._pendingStackOperation = null; } + private _getOrCreateEditStackElement(beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, alternativeVersionId: string) { return this._pendingStackOperation ??= new StackOperation(this._textModel, undoRedoGroup, this._pauseableEmitter, this._postUndoRedo, beginSelectionState, alternativeVersionId || ''); } + appendPreviousOperation(): boolean { + const previous = this._undoService.getLastElement(this._textModel.uri) as StackOperation; + if (previous && previous.tag === 'notebookUndoRedoElement') { + this._pendingStackOperation = previous; + this._isAppending = true; + return true; + } + return false; + } + pushEditOperation(element: IUndoRedoElement, beginSelectionState: ISelectionState | undefined, resultSelectionState: ISelectionState | undefined, alternativeVersionId: string, undoRedoGroup: UndoRedoGroup | undefined) { const pendingStackOperation = this._getOrCreateEditStackElement(beginSelectionState, undoRedoGroup, alternativeVersionId); pendingStackOperation.pushEditOperation(element, beginSelectionState, resultSelectionState, alternativeVersionId); @@ -582,10 +598,40 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return result; } + private newCellsFromLastEdit = new Set(); + private isOnlyEditingMetadataOnNewCells(rawEdits: ICellEditOperation[]): boolean { + for (const edit of rawEdits) { + if (edit.editType === CellEditType.PartialInternalMetadata) { + continue; + } + if (edit.editType !== CellEditType.Metadata && edit.editType !== CellEditType.PartialMetadata) { + return false; + } + + if (('index' in edit) && !this.newCellsFromLastEdit.has(this.cells[edit.index].handle)) { + return false; + } + if ('handle' in edit && !this.newCellsFromLastEdit.has(edit.handle)) { + return false; + } + } + + return true; + } + applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo: boolean): boolean { this._pauseableEmitter.pause(); this._operationManager.pushStackElement(this._alternativeVersionId, undefined); + if (computeUndoRedo && this.isOnlyEditingMetadataOnNewCells(rawEdits)) { + if (!this._operationManager.appendPreviousOperation()) { + // we can't append the previous operation, so just don't compute undo/redo + computeUndoRedo = false; + } + } else if (computeUndoRedo) { + this.newCellsFromLastEdit.clear(); + } + try { this._doApplyEdits(rawEdits, synchronous, computeUndoRedo, beginSelectionState, undoRedoGroup); return true; @@ -816,6 +862,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const dirtyStateListener = cell.onDidChangeContent((e) => { this._bindCellContentHandler(cell, e); }); + + this.newCellsFromLastEdit.add(cell.handle); this._cellListeners.set(cell.handle, dirtyStateListener); this._register(cell); return cell; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index d87906fa..40c4bd34 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -19,7 +19,6 @@ import { ISplice } from '../../../../base/common/sequence.js'; import { ThemeColor } from '../../../../base/common/themables.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { Range } from '../../../../editor/common/core/range.js'; -import { ILineChange } from '../../../../editor/common/diff/legacyLinesDiffComputer.js'; import * as editorCommon from '../../../../editor/common/editorCommon.js'; import { Command, WorkspaceEditMetadata } from '../../../../editor/common/languages.js'; import { IReadonlyTextBuffer, ITextModel } from '../../../../editor/common/model.js'; @@ -34,7 +33,7 @@ import { ICellExecutionError } from './notebookExecutionStateService.js'; import { INotebookTextModelLike } from './notebookKernelService.js'; import { ICellRange } from './notebookRange.js'; import { RegisteredEditorPriority } from '../../../services/editor/common/editorResolverService.js'; -import { generateMetadataUri, generate as generateUri, parseMetadataUri, parse as parseUri } from '../../../services/notebook/common/notebookDocumentService.js'; +import { generateMetadataUri, generate as generateUri, extractCellOutputDetails, parseMetadataUri, parse as parseUri } from '../../../services/notebook/common/notebookDocumentService.js'; import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from '../../../services/workingCopy/common/workingCopy.js'; import { SnapshotContext } from '../../../services/workingCopy/common/fileWorkingCopy.js'; @@ -121,7 +120,12 @@ export interface NotebookCellMetadata { } export interface NotebookCellInternalMetadata { - cellId?: string; + /** + * Used only for diffing of Notebooks. + * This is not persisted and generally useful only when diffing two notebooks. + * Useful only after we've manually matched a few cells together so we know which cells are matching. + */ + internalId?: string; executionId?: string; executionOrder?: number; lastRunSuccess?: boolean; @@ -613,32 +617,37 @@ export namespace CellUri { return parseUri(cell); } - export function generateCellOutputUri(notebook: URI, outputId?: string) { + /** + * Generates a URI for a cell output in a notebook using the output ID. + * Used when URI should be opened as text in the editor. + */ + export function generateCellOutputUriWithId(notebook: URI, outputId?: string) { return notebook.with({ scheme: Schemas.vscodeNotebookCellOutput, - fragment: `op${outputId ?? ''},${notebook.scheme !== Schemas.file ? notebook.scheme : ''}` + query: new URLSearchParams({ + openIn: 'editor', + outputId: outputId ?? '', + notebookScheme: notebook.scheme !== Schemas.file ? notebook.scheme : '', + }).toString() + }); + } + /** + * Generates a URI for a cell output in a notebook using the output index. + * Used when URI should be opened in notebook editor. + */ + export function generateCellOutputUriWithIndex(notebook: URI, cellUri: URI, outputIndex: number): URI { + return notebook.with({ + scheme: Schemas.vscodeNotebookCellOutput, + fragment: cellUri.fragment, + query: new URLSearchParams({ + openIn: 'notebook', + outputIndex: String(outputIndex), + }).toString() }); } - export function parseCellOutputUri(uri: URI): { notebook: URI; outputId?: string } | undefined { - if (uri.scheme !== Schemas.vscodeNotebookCellOutput) { - return; - } - - const match = /^op([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})?\,(.*)$/i.exec(uri.fragment); - if (!match) { - return undefined; - } - - const outputId = (match[1] && match[1] !== '') ? match[1] : undefined; - const scheme = match[2]; - return { - outputId, - notebook: uri.with({ - scheme: scheme || Schemas.file, - fragment: null - }) - }; + export function parseCellOutputUri(uri: URI): { notebook: URI; openIn: string; outputId?: string; cellFragment?: string; outputIndex?: number; cellHandle?: number } | undefined { + return extractCellOutputDetails(uri); } export function generateCellPropertyUri(notebook: URI, handle: number, scheme: string): URI { @@ -955,7 +964,6 @@ export interface INotebookCellStatusBarItemProvider { export interface INotebookDiffResult { cellsDiff: IDiffResult; metadataChanged: boolean; - linesDiff?: { originalCellhandle: number; modifiedCellhandle: number; lineChanges: ILineChange[] }[]; } export interface INotebookCellStatusBarItem { diff --git a/src/vs/workbench/contrib/notebook/common/notebookDiff.ts b/src/vs/workbench/contrib/notebook/common/notebookDiff.ts new file mode 100644 index 00000000..fb8844cf --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookDiff.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDiffChange } from '../../../../base/common/diff/diff.js'; +import { CellKind, INotebookDiffResult } from './notebookCommon.js'; + +export type CellDiffInfo = { + originalCellIndex: number; + modifiedCellIndex: number; + type: 'unchanged' | 'modified'; +} | +{ + originalCellIndex: number; + type: 'delete'; +} | +{ + modifiedCellIndex: number; + type: 'insert'; +}; + +interface ICell { + cellKind: CellKind; + getHashValue(): number; + equal(cell: ICell): boolean; +} +// interface INotebookDiffResult { +// cellsDiff: IDiffResult; +// metadataChanged: boolean; +// } + +export function computeDiff(originalModel: { readonly cells: readonly ICell[] }, modifiedModel: { readonly cells: readonly ICell[] }, diffResult: INotebookDiffResult) { + const cellChanges = diffResult.cellsDiff.changes; + const cellDiffInfo: CellDiffInfo[] = []; + let originalCellIndex = 0; + let modifiedCellIndex = 0; + + let firstChangeIndex = -1; + + for (let i = 0; i < cellChanges.length; i++) { + const change = cellChanges[i]; + // common cells + + for (let j = 0; j < change.originalStart - originalCellIndex; j++) { + const originalCell = originalModel.cells[originalCellIndex + j]; + const modifiedCell = modifiedModel.cells[modifiedCellIndex + j]; + if (originalCell.getHashValue() === modifiedCell.getHashValue()) { + cellDiffInfo.push({ + originalCellIndex: originalCellIndex + j, + modifiedCellIndex: modifiedCellIndex + j, + type: 'unchanged' + }); + } else { + if (firstChangeIndex === -1) { + firstChangeIndex = cellDiffInfo.length; + } + cellDiffInfo.push({ + originalCellIndex: originalCellIndex + j, + modifiedCellIndex: modifiedCellIndex + j, + type: 'modified' + }); + } + } + + const modifiedLCS = computeModifiedLCS(change, originalModel, modifiedModel); + if (modifiedLCS.length && firstChangeIndex === -1) { + firstChangeIndex = cellDiffInfo.length; + } + + cellDiffInfo.push(...modifiedLCS); + originalCellIndex = change.originalStart + change.originalLength; + modifiedCellIndex = change.modifiedStart + change.modifiedLength; + } + + for (let i = originalCellIndex; i < originalModel.cells.length; i++) { + cellDiffInfo.push({ + originalCellIndex: i, + modifiedCellIndex: i - originalCellIndex + modifiedCellIndex, + type: 'unchanged' + }); + } + + return { + cellDiffInfo, + firstChangeIndex + }; +} + +function computeModifiedLCS(change: IDiffChange, originalModel: { readonly cells: readonly ICell[] }, modifiedModel: { readonly cells: readonly ICell[] }) { + const result: CellDiffInfo[] = []; + // modified cells + const modifiedLen = Math.min(change.originalLength, change.modifiedLength); + + for (let j = 0; j < modifiedLen; j++) { + const originalCell = originalModel.cells[change.originalStart + j]; + const modifiedCell = modifiedModel.cells[change.modifiedStart + j]; + if (originalCell.cellKind !== modifiedCell.cellKind) { + result.push({ + originalCellIndex: change.originalStart + j, + type: 'delete' + }); + result.push({ + modifiedCellIndex: change.modifiedStart + j, + type: 'insert' + }); + } else { + const isTheSame = originalCell.equal(modifiedCell); + result.push({ + originalCellIndex: change.originalStart + j, + modifiedCellIndex: change.modifiedStart + j, + type: isTheSame ? 'unchanged' : 'modified' + }); + } + } + + for (let j = modifiedLen; j < change.originalLength; j++) { + // deletion + result.push({ + originalCellIndex: change.originalStart + j, + type: 'delete' + }); + } + + for (let j = modifiedLen; j < change.modifiedLength; j++) { + result.push({ + modifiedCellIndex: change.modifiedStart + j, + type: 'insert' + }); + } + + return result; +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 68b112f2..d0e9724f 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -27,7 +27,6 @@ import { IFileWorkingCopyManager } from '../../../services/workingCopy/common/fi import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopySaveEvent, StoredFileWorkingCopyState } from '../../../services/workingCopy/common/storedFileWorkingCopy.js'; import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent, IUntitledFileWorkingCopyModelFactory } from '../../../services/workingCopy/common/untitledFileWorkingCopy.js'; import { WorkingCopyCapabilities } from '../../../services/workingCopy/common/workingCopy.js'; -import { INotebookSynchronizerService } from './notebookSynchronizerService.js'; //#region --- simple content provider @@ -56,7 +55,6 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE private readonly _workingCopyManager: IFileWorkingCopyManager, scratchpad: boolean, @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService, - @INotebookSynchronizerService private readonly _notebookSynchronizerService: INotebookSynchronizerService ) { super(); @@ -122,9 +120,6 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE async revert(options?: IRevertOptions): Promise { assertType(this.isResolved()); - if (this._workingCopy) { - await this._notebookSynchronizerService.revert(this._workingCopy); - } return this._workingCopy!.revert(options); } @@ -141,7 +136,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } else { this._workingCopy = await this._workingCopyManager.resolve({ untitledResource: this.resource, isScratchpad: this.scratchPad }); } - this._workingCopy.onDidRevert(() => this._onDidRevertUntitled.fire()); + this._register(this._workingCopy.onDidRevert(() => this._onDidRevertUntitled.fire())); } else { this._workingCopy = await this._workingCopyManager.resolve(this.resource, { limits: options?.limits, diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index a21a9575..862ca633 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -61,6 +61,15 @@ class NotebookModelReferenceCollection extends ReferenceCollection { // Untrack as being disposed this.modelsToDispose.delete(key); @@ -186,7 +195,7 @@ export class NotebookModelResolverServiceImpl implements INotebookEditorModelRes const suffix = NotebookProviderInfo.possibleFileEnding(info.selectors) ?? ''; for (let counter = 1; ; counter++) { const candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}${suffix}`, query: notebookType }); - if (!this._notebookService.getNotebookTextModel(candidate)) { + if (!this._notebookService.getNotebookTextModel(candidate) && !this._data.isListeningToModel(candidate)) { return candidate; } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookSynchronizerService.ts b/src/vs/workbench/contrib/notebook/common/notebookSynchronizerService.ts deleted file mode 100644 index 4fc50465..00000000 --- a/src/vs/workbench/contrib/notebook/common/notebookSynchronizerService.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IStoredFileWorkingCopy } from '../../../services/workingCopy/common/storedFileWorkingCopy.js'; -import { IUntitledFileWorkingCopy } from '../../../services/workingCopy/common/untitledFileWorkingCopy.js'; -import { NotebookFileWorkingCopyModel } from './notebookEditorModel.js'; - -export const INotebookSynchronizerService = createDecorator('notebookSynchronizerService'); - -export interface INotebookSynchronizerService { - readonly _serviceBrand: undefined; - revert(workingCopy: IStoredFileWorkingCopy | IUntitledFileWorkingCopy): Promise; -} diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookCellMatching.ts b/src/vs/workbench/contrib/notebook/common/services/notebookCellMatching.ts new file mode 100644 index 00000000..0df0bab4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookCellMatching.ts @@ -0,0 +1,344 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { computeLevenshteinDistance } from '../../../../../base/common/diff/diff.js'; +import { CellKind } from '../notebookCommon.js'; + + +type EditCount = number; +type OriginalIndex = number; +type ModifiedIndex = number; +type CellEditCountCache = { + modifiedToOriginal: Map>; + originalToModified: Map>; +}; + +type ICell = { + internalMetadata?: { + internalId?: string; + }; + getValue(): string; + getLinesContent(): string[]; + cellKind: CellKind; +}; + +/** + * Given a set of modified cells and original cells, this function will attempt to match the modified cells with the original cells. + * E.g. Assume you have (original on left and modified on right): + * ================= + * Cell A | Cell a + * Cell B | Cell b + * Cell C | Cell d + * Cell D | Cell e + * ================= + * Here we know that `Cell C` has been removed and `Cell e` has been added. + * The mapping from modified to original will be as follows: + * Cell a => Cell A + * Cell b => Cell B + * Cell d => Cell D + * Cell e => + * Cell C in original was not matched, hence it was deleted. + * + * Thus the return value is as follows: + * [ + * { modified: 0, original: 0 }, + * { modified: 1, original: 1 }, + * { modified: 2, original: 3 }, + * { modified: 3, original: -1 }, + * ] + * @returns + */ +export function matchCellBasedOnSimilarties(modifiedCells: ICell[], originalCells: ICell[]): { modified: number; original: number; percentage: number }[] { + const cache: CellEditCountCache = { + modifiedToOriginal: new Map>(), + originalToModified: new Map>(), + }; + const results: { modified: number; original: number; dist: number; percentage: number; possibleOriginal: number }[] = []; + const mappedOriginalCellToModifiedCell = new Map(); + const mappedModifiedIndexes = new Set(); + const originalIndexWithMostEdits = new Map(); + const canOriginalIndexBeMappedToModifiedIndex = (originalIndex: number, value: { editCount: EditCount }) => { + if (mappedOriginalCellToModifiedCell.has(originalIndex)) { + return false; + } + const existingEdits = originalIndexWithMostEdits.get(originalIndex)?.dist ?? Number.MAX_SAFE_INTEGER; + return value.editCount < existingEdits; + }; + const trackMappedIndexes = (modifiedIndex: number, originalIndex: number) => { + mappedOriginalCellToModifiedCell.set(originalIndex, modifiedIndex); + mappedModifiedIndexes.add(modifiedIndex); + }; + + for (let i = 0; i < modifiedCells.length; i++) { + const modifiedCell = modifiedCells[i]; + const { index, editCount: dist, percentage } = computeClosestCell({ cell: modifiedCell, index: i }, originalCells, true, cache, canOriginalIndexBeMappedToModifiedIndex); + if (index >= 0 && dist === 0) { + trackMappedIndexes(i, index); + results.push({ modified: i, original: index, dist, percentage, possibleOriginal: index }); + } else { + originalIndexWithMostEdits.set(index, { dist: dist, modifiedIndex: i }); + results.push({ modified: i, original: -1, dist: dist, percentage, possibleOriginal: index }); + } + } + + results.forEach((result, i) => { + if (result.original >= 0) { + return; + } + + /** + * I.e. Assume you have the following + * ================= + * A a (this has ben matched) + * B b + * C c + * D d (these two have been matched) + * e e + * f f + * ================= + * Just match A => a, B => b, C => c + */ + // Find the next cell that has been matched. + const previousMatchedCell = i > 0 ? results.slice(0, i).reverse().find(r => r.original >= 0) : undefined; + const previousMatchedOriginalIndex = previousMatchedCell?.original ?? -1; + const previousMatchedModifiedIndex = previousMatchedCell?.modified ?? -1; + const matchedCell = results.slice(i + 1).find(r => r.original >= 0); + const unavailableIndexes = new Set(); + const nextMatchedModifiedIndex = results.findIndex((item, idx) => idx > i && item.original >= 0); + const nextMatchedOriginalIndex = nextMatchedModifiedIndex >= 0 ? results[nextMatchedModifiedIndex].original : -1; + // Find the available indexes that we can match with. + // We are only interested in b and c (anything after d is of no use). + originalCells.forEach((_, i) => { + if (mappedOriginalCellToModifiedCell.has(i)) { + unavailableIndexes.add(i); + return; + } + if (matchedCell && i >= matchedCell.original) { + unavailableIndexes.add(i); + } + if (nextMatchedOriginalIndex >= 0 && i > nextMatchedOriginalIndex) { + unavailableIndexes.add(i); + } + }); + + + const modifiedCell = modifiedCells[i]; + /** + * I.e. Assume you have the following + * ================= + * A a (this has ben matched) + * B b + * C c + * D d (these two have been matched) + * e e + * f f + * ================= + * Given that we have a probable match for B => b, we can match it. + */ + if (result.original === -1 && result.possibleOriginal >= 0 && !unavailableIndexes.has(result.possibleOriginal) && canOriginalIndexBeMappedToModifiedIndex(result.possibleOriginal, { editCount: result.dist })) { + trackMappedIndexes(i, result.possibleOriginal); + result.original = result.possibleOriginal; + return; + } + + + /** + * I.e. Assume you have the following + * ================= + * A a (this has ben matched) + * B b + * C c + * D d (these two have been matched) + * ================= + * Its possible that B matches better with c and C matches better with b. + * However given the fact that we have matched A => a and D => d. + * & if the indexes are an exact match. + * I.e. index of D in Modified === index of d in Original, and index of A in Modified === index of a in Original. + * Then this means there are absolutely no modifications. + * Hence we can just assign the indexes as is. + * + * NOTE: For this, we must ensure we have exactly the same number of items on either side. + * I.e. we have B, C remaining in Modified, and b, c remaining in Original. + * Thats 2 Modified items === 2 Original Items. + * If its not the same, then this means something has been deleted/inserted, and we cannot blindly map the indexes. + */ + if (previousMatchedOriginalIndex > 0 && previousMatchedModifiedIndex > 0 && previousMatchedOriginalIndex === previousMatchedModifiedIndex) { + if ((nextMatchedModifiedIndex >= 0 ? nextMatchedModifiedIndex : modifiedCells.length - 1) === (nextMatchedOriginalIndex >= 0 ? nextMatchedOriginalIndex : originalCells.length - 1) && !unavailableIndexes.has(i) && i < originalCells.length) { + const remainingModifiedItems = (nextMatchedModifiedIndex >= 0 ? nextMatchedModifiedIndex : modifiedCells.length) - previousMatchedModifiedIndex; + const remainingOriginalItems = (nextMatchedOriginalIndex >= 0 ? nextMatchedOriginalIndex : originalCells.length) - previousMatchedOriginalIndex; + if (remainingModifiedItems === remainingOriginalItems && modifiedCell.cellKind === originalCells[i].cellKind) { + trackMappedIndexes(i, i); + result.original = i; + return; + } + } + } + /** + * I.e. Assume you have the following + * ================= + * A a (this has ben matched) + * B b + * C c + * D d (these two have been matched) + * e e + * f f + * ================= + * We can now try to match B with b and c and figure out which is best. + * RULE 1. Its possible that B will match best with c, howevber C matches better with c, meaning we should match B with b. + * To do this, we need to see if c has a better match with something else. + */ + // RULE 1 + // Try to find the next best match, but exclucde items that have a better match. + const { index, percentage } = computeClosestCell({ cell: modifiedCell, index: i }, originalCells, false, cache, (originalIndex: number, originalValue: { editCount: EditCount }) => { + if (unavailableIndexes.has(originalIndex)) { + return false; + } + + if (nextMatchedModifiedIndex > 0 || previousMatchedOriginalIndex > 0) { + // See if we have a beter match for this. + const matchesForThisOriginalIndex = cache.originalToModified.get(originalIndex); + if (matchesForThisOriginalIndex && previousMatchedOriginalIndex < originalIndex) { + const betterMatch = Array.from(matchesForThisOriginalIndex).find(([modifiedIndex, value]) => { + if (modifiedIndex === i) { + // This is the same modifeid entry. + return false; + } + if (modifiedIndex >= nextMatchedModifiedIndex) { + // We're only interested in matches that are before the next matched index. + return false; + } + if (mappedModifiedIndexes.has(i)) { + // This has already been matched. + return false; + } + return value.editCount < originalValue.editCount; + }); + if (betterMatch) { + // We do have a better match for this, hence do not use this. + return false; + } + } + } + return !unavailableIndexes.has(originalIndex); + }); + + /** + * I.e. Assume you have the following + * ================= + * A a (this has ben matched) + * B bbbbbbbbbbbbbb + * C cccccccccccccc + * D d (these two have been matched) + * e e + * f f + * ================= + * RULE 1 . Now when attempting to match `bbbbbbbbbbbb` with B, the number of edits is very high and the percentage is also very high. + * Basically majority of the text needs to be changed. + * However if the indexes line up perfectly well, and this is the best match, then use it. + * + * Similarly its possible we're trying to match b with `BBBBBBBBBBBB` and the number of edits is very high, but the indexes line up perfectly well. + * + * RULE 2. However it is also possible that there's a better match with another cell + * Assume we have + * ================= + * AAAA a (this has been matched) + * bbbbbbbb b + * bbbb c + * dddd d (these two have been matched) + * ================= + * In this case if we use the algorithm of (1) above, we'll end up matching bbbb with b, and bbbbbbbb with c. + * But we're not really sure if this is the best match. + * In such cases try to match with the same cell index. + * + */ + // RULE 1 (got a match and the indexes line up perfectly well, use it regardless of the number of edits). + if (index >= 0 && i > 0 && results[i - 1].original === index - 1) { + trackMappedIndexes(i, index); + results[i].original = index; + return; + } + + // RULE 2 + // Here we know that `AAAA => a` + // Check if the previous cell has been matched. + // And if the next modified and next original cells are a match. + const nextOriginalCell = (i > 0 && originalCells.length > results[i - 1].original) ? results[i - 1].original + 1 : -1; + const nextOriginalCellValue = i > 0 && nextOriginalCell >= 0 && nextOriginalCell < originalCells.length ? originalCells[nextOriginalCell].getValue() : undefined; + if (index >= 0 && i > 0 && typeof nextOriginalCellValue === 'string' && !mappedOriginalCellToModifiedCell.has(nextOriginalCell)) { + if (modifiedCell.getValue().includes(nextOriginalCellValue) || nextOriginalCellValue.includes(modifiedCell.getValue())) { + trackMappedIndexes(i, nextOriginalCell); + results[i].original = nextOriginalCell; + return; + } + } + + if (percentage < 90 || (i === 0 && results.length === 1)) { + trackMappedIndexes(i, index); + results[i].original = index; + return; + } + }); + + return results; +} + +function computeClosestCell({ cell, index: cellIndex }: { cell: ICell; index: number }, arr: readonly ICell[], ignoreEmptyCells: boolean, cache: CellEditCountCache, canOriginalIndexBeMappedToModifiedIndex: (originalIndex: number, value: { editCount: EditCount }) => boolean): { index: number; editCount: number; percentage: number } { + let min_edits = Infinity; + let min_index = -1; + + // Always give preference to internal Cell Id if found. + const internalId = cell.internalMetadata?.internalId; + if (internalId) { + const internalIdIndex = arr.findIndex(cell => cell.internalMetadata?.internalId === internalId); + if (internalIdIndex >= 0) { + return { index: internalIdIndex, editCount: 0, percentage: Number.MAX_SAFE_INTEGER }; + } + } + + for (let i = 0; i < arr.length; i++) { + // Skip cells that are not of the same kind. + if (arr[i].cellKind !== cell.cellKind) { + continue; + } + const str = arr[i].getValue(); + const cacheEntry = cache.modifiedToOriginal.get(cellIndex) ?? new Map(); + const value = cacheEntry.get(i) ?? { editCount: computeNumberOfEdits(cell, arr[i]), }; + cacheEntry.set(i, value); + cache.modifiedToOriginal.set(cellIndex, cacheEntry); + + const originalCacheEntry = cache.originalToModified.get(i) ?? new Map(); + originalCacheEntry.set(cellIndex, value); + cache.originalToModified.set(i, originalCacheEntry); + + if (!canOriginalIndexBeMappedToModifiedIndex(i, value)) { + continue; + } + if (str.length === 0 && ignoreEmptyCells) { + continue; + } + if (str === cell.getValue() && cell.getValue().length > 0) { + return { index: i, editCount: 0, percentage: 0 }; + } + + if (value.editCount < min_edits) { + min_edits = value.editCount; + min_index = i; + } + } + + if (min_index === -1) { + return { index: -1, editCount: Number.MAX_SAFE_INTEGER, percentage: Number.MAX_SAFE_INTEGER }; + } + const percentage = !cell.getValue().length && !arr[min_index].getValue().length ? 0 : (cell.getValue().length ? (min_edits * 100 / cell.getValue().length) : Number.MAX_SAFE_INTEGER); + return { index: min_index, editCount: min_edits, percentage }; +} + +function computeNumberOfEdits(modified: ICell, original: ICell) { + if (modified.getValue() === original.getValue()) { + return 0; + } + + return computeLevenshteinDistance(modified.getValue(), original.getValue()); +} diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts deleted file mode 100644 index c5b12612..00000000 --- a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts +++ /dev/null @@ -1,370 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { IDiffResult, ISequence, LcsDiff } from '../../../../../base/common/diff/diff.js'; -import { doHash, numberHash } from '../../../../../base/common/hash.js'; -import { IDisposable } from '../../../../../base/common/lifecycle.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { IRequestHandler, IWorkerServer } from '../../../../../base/common/worker/simpleWorker.js'; -import { PieceTreeTextBufferBuilder } from '../../../../../editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.js'; -import { CellKind, IMainCellDto, INotebookDiffResult, IOutputDto, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookDocumentMetadata, TransientDocumentMetadata } from '../notebookCommon.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { SearchParams } from '../../../../../editor/common/model/textModelSearch.js'; -import { MirrorModel } from '../../../../../editor/common/services/textModelSync/textModelSync.impl.js'; -import { DefaultEndOfLine } from '../../../../../editor/common/model.js'; -import { IModelChangedEvent } from '../../../../../editor/common/model/mirrorTextModel.js'; -import { filter } from '../../../../../base/common/objects.js'; - -class MirrorCell { - private readonly textModel: MirrorModel; - private _hash?: Promise; - public get eol() { - return this._eol === '\r\n' ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF; - } - constructor( - public readonly handle: number, - uri: URI, - source: string[], - private readonly _eol: string, - versionId: number, - public language: string, - public cellKind: CellKind, - public outputs: IOutputDto[], - public metadata?: NotebookCellMetadata, - public internalMetadata?: NotebookCellInternalMetadata, - - ) { - this.textModel = new MirrorModel(uri, source, _eol, versionId); - } - - onEvents(e: IModelChangedEvent) { - this.textModel.onEvents(e); - this._hash = undefined; - } - getValue(): string { - return this.textModel.getValue(); - } - - async getComparisonValue(): Promise { - return this._hash ??= this._getHash(); - } - - private async _getHash() { - let hashValue = numberHash(104579, 0); - - hashValue = doHash(this.language, hashValue); - hashValue = doHash(this.getValue(), hashValue); - hashValue = doHash(this.metadata, hashValue); - hashValue = doHash({ ...this.internalMetadata, 'cellId': '' }, hashValue); - for (const op of this.outputs) { - hashValue = doHash(op.metadata, hashValue); - for (const output of op.outputs) { - hashValue = doHash(output.mime, hashValue); - } - } - - // note: hash has not updated within the Promise.all since we must retain order - const digests = await Promise.all(this.outputs.flatMap(op => - op.outputs.map(o => crypto.subtle.digest('sha-1', o.data.buffer)) - )); - for (const digest of digests) { - hashValue = numberHash(new Int32Array(digest)[0], hashValue); - } - - - return hashValue; - } -} - -class MirrorNotebookDocument { - constructor( - readonly uri: URI, - public cells: MirrorCell[], - public metadata: NotebookDocumentMetadata, - public transientDocumentMetadata: TransientDocumentMetadata, - ) { - } - - acceptModelChanged(event: NotebookCellsChangedEventDto) { - // note that the cell content change is not applied to the MirrorCell - // but it's fine as if a cell content is modified after the first diff, its position will not change any more - // TODO@rebornix, but it might lead to interesting bugs in the future. - event.rawEvents.forEach(e => { - if (e.kind === NotebookCellsChangeType.ModelChange) { - this._spliceNotebookCells(e.changes); - } else if (e.kind === NotebookCellsChangeType.Move) { - const cells = this.cells.splice(e.index, 1); - this.cells.splice(e.newIdx, 0, ...cells); - } else if (e.kind === NotebookCellsChangeType.Output) { - const cell = this.cells[e.index]; - cell.outputs = e.outputs; - } else if (e.kind === NotebookCellsChangeType.ChangeCellLanguage) { - this._assertIndex(e.index); - const cell = this.cells[e.index]; - cell.language = e.language; - } else if (e.kind === NotebookCellsChangeType.ChangeCellMetadata) { - this._assertIndex(e.index); - const cell = this.cells[e.index]; - cell.metadata = e.metadata; - } else if (e.kind === NotebookCellsChangeType.ChangeCellInternalMetadata) { - this._assertIndex(e.index); - const cell = this.cells[e.index]; - cell.internalMetadata = e.internalMetadata; - } else if (e.kind === NotebookCellsChangeType.ChangeDocumentMetadata) { - this.metadata = e.metadata; - } - }); - } - - private _assertIndex(index: number): void { - if (index < 0 || index >= this.cells.length) { - throw new Error(`Illegal index ${index}. Cells length: ${this.cells.length}`); - } - } - - _spliceNotebookCells(splices: NotebookCellTextModelSplice[]) { - splices.reverse().forEach(splice => { - const cellDtos = splice[2]; - const newCells = cellDtos.map(cell => { - return new MirrorCell( - cell.handle, - URI.parse(cell.url), - cell.source, - cell.eol, - cell.versionId, - cell.language, - cell.cellKind, - cell.outputs, - cell.metadata, - ); - }); - - this.cells.splice(splice[0], splice[1], ...newCells); - }); - } -} - -class CellSequence implements ISequence { - - static async create(textModel: MirrorNotebookDocument) { - const hashValue = new Int32Array(textModel.cells.length); - await Promise.all(textModel.cells.map(async (c, i) => { - hashValue[i] = await c.getComparisonValue(); - })); - return new CellSequence(hashValue); - } - - static async createWithCellId(textModel: MirrorNotebookDocument): Promise> { - const hashValue = new Map(); - await Promise.all(textModel.cells.map(async (c, i) => { - const value = await c.getComparisonValue(); - const id: string = (c.metadata?.id || '') as string; - hashValue.set(id, value); - })); - return hashValue; - } - - constructor(readonly hashValue: Int32Array) { } - - getElements(): string[] | number[] | Int32Array { - return this.hashValue; - } -} - -export class NotebookEditorSimpleWorker implements IRequestHandler, IDisposable { - _requestHandlerBrand: any; - - private _models: { [uri: string]: MirrorNotebookDocument }; - - constructor() { - this._models = Object.create(null); - } - dispose(): void { - } - - public $acceptNewModel(uri: string, metadata: NotebookDocumentMetadata, transientDocumentMetadata: TransientDocumentMetadata, cells: IMainCellDto[]): void { - this._models[uri] = new MirrorNotebookDocument(URI.parse(uri), cells.map(dto => new MirrorCell( - dto.handle, - URI.parse(dto.url), - dto.source, - dto.eol, - dto.versionId, - dto.language, - dto.cellKind, - dto.outputs, - dto.metadata, - dto.internalMetadata - )), metadata, transientDocumentMetadata); - } - - public $acceptModelChanged(strURL: string, event: NotebookCellsChangedEventDto) { - const model = this._models[strURL]; - model?.acceptModelChanged(event); - } - - public $acceptCellModelChanged(strURL: string, handle: number, event: IModelChangedEvent) { - const model = this._models[strURL]; - model.cells.find(cell => cell.handle === handle)?.onEvents(event); - } - - public $acceptRemovedModel(strURL: string): void { - if (!this._models[strURL]) { - return; - } - delete this._models[strURL]; - } - - async $computeDiff(originalUrl: string, modifiedUrl: string): Promise { - const original = this._getModel(originalUrl); - const modified = this._getModel(modifiedUrl); - - const [originalSeq, modifiedSeq] = await Promise.all([ - CellSequence.create(original), - CellSequence.create(modified), - ]); - - const diff = new LcsDiff(originalSeq, modifiedSeq); - const diffResult = diff.ComputeDiff(false); - - const originalMetadata = filter(original.metadata, key => !original.transientDocumentMetadata[key]); - const modifiedMetadata = filter(modified.metadata, key => !modified.transientDocumentMetadata[key]); - return { - metadataChanged: JSON.stringify(originalMetadata) !== JSON.stringify(modifiedMetadata), - cellsDiff: await this.$computeDiffWithCellIds(original, modified) || diffResult, - // linesDiff: cellLineChanges - }; - } - - async $computeDiffWithCellIds(original: MirrorNotebookDocument, modified: MirrorNotebookDocument): Promise { - const originalCellIndexIds = original.cells.map((cell, index) => ({ index, id: (cell.metadata?.id || '') as string })); - const modifiedCellIndexIds = modified.cells.map((cell, index) => ({ index, id: (cell.metadata?.id || '') as string })); - const originalCellIds = originalCellIndexIds.map(c => c.id); - const modifiedCellIds = modifiedCellIndexIds.map(c => c.id); - const orderOrOriginalCellIds = originalCellIds.filter(id => modifiedCellIds.includes(id)).join(','); - const orderOrModifiedCellIds = modifiedCellIds.filter(id => originalCellIds.includes(id)).join(','); - if (originalCellIndexIds.some(c => !c.id) || modifiedCellIndexIds.some(c => !c.id) || orderOrOriginalCellIds !== orderOrModifiedCellIds) { - return; - } - - const diffResult: IDiffResult = { changes: [], quitEarly: false, }; - - const computeCellHashesById = async (notebook: MirrorNotebookDocument) => { - const hashValue = new Map(); - await Promise.all(notebook.cells.map(async (c, i) => { - const value = await c.getComparisonValue(); - // Verified earlier that these cannot be empty. - const id: string = (c.metadata?.id || '') as string; - hashValue.set(id, value); - })); - return hashValue; - }; - - const [originalSeq, modifiedSeq] = await Promise.all([computeCellHashesById(original), computeCellHashesById(modified)]); - - while (modifiedCellIndexIds.length) { - const modifiedCell = modifiedCellIndexIds.shift()!; - const originalCell = originalCellIndexIds.find(c => c.id === modifiedCell.id); - if (originalCell) { - // Everything before this cell is a deletion - const index = originalCellIndexIds.indexOf(originalCell); - const deletedFromOriginal = originalCellIndexIds.splice(0, index + 1); - - if (deletedFromOriginal.length === 1) { - if (originalSeq.get(originalCell.id) === modifiedSeq.get(originalCell.id)) { - // Cell contents are the same. - // No changes, hence ignore this cell. - } - else { - diffResult.changes.push({ - originalStart: originalCell.index, - originalLength: 1, - modifiedStart: modifiedCell.index, - modifiedLength: 1 - }); - } - } else { - // This means we have some cells before this and they were removed. - diffResult.changes.push({ - originalStart: deletedFromOriginal[0].index, - originalLength: deletedFromOriginal.length - 1, - modifiedStart: modifiedCell.index, - modifiedLength: 0 - }); - } - continue; - } - else { - // This is a new cell. - diffResult.changes.push({ - originalStart: originalCellIndexIds.length ? originalCellIndexIds[0].index : original.cells.length, - originalLength: 0, - modifiedStart: modifiedCell.index, - modifiedLength: 1 - }); - } - } - - // If we still have some original cells, then those have been removed. - if (originalCellIndexIds.length) { - diffResult.changes.push({ - originalStart: originalCellIndexIds[0].index, - originalLength: originalCellIndexIds.length, - modifiedStart: modifiedCellIndexIds.length, - modifiedLength: 0 - }); - } - - return diffResult; - } - - $canPromptRecommendation(modelUrl: string): boolean { - const model = this._getModel(modelUrl); - const cells = model.cells; - - for (let i = 0; i < cells.length; i++) { - const cell = cells[i]; - if (cell.cellKind === CellKind.Markup) { - continue; - } - - if (cell.language !== 'python') { - continue; - } - - const searchParams = new SearchParams('import\\s*pandas|from\\s*pandas', true, false, null); - const searchData = searchParams.parseSearchRequest(); - - if (!searchData) { - continue; - } - - const builder = new PieceTreeTextBufferBuilder(); - builder.acceptChunk(cell.getValue()); - const bufferFactory = builder.finish(true); - const textBuffer = bufferFactory.create(cell.eol).textBuffer; - - const lineCount = textBuffer.getLineCount(); - const maxLineCount = Math.min(lineCount, 20); - const range = new Range(1, 1, maxLineCount, textBuffer.getLineLength(maxLineCount) + 1); - const cellMatches = textBuffer.findMatchesLineByLine(range, searchData, true, 1); - if (cellMatches.length > 0) { - return true; - } - } - - return false; - } - - protected _getModel(uri: string): MirrorNotebookDocument { - return this._models[uri]; - } -} - -/** - * Defines the worker entry point. Must be exported and named `create`. - * @skipMangle - */ -export function create(workerServer: IWorkerServer): IRequestHandler { - return new NotebookEditorSimpleWorker(); -} diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookWebWorker.ts b/src/vs/workbench/contrib/notebook/common/services/notebookWebWorker.ts new file mode 100644 index 00000000..d95e43c5 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookWebWorker.ts @@ -0,0 +1,576 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { IDiffChange, ISequence, LcsDiff } from '../../../../../base/common/diff/diff.js'; +import { doHash, hash, numberHash } from '../../../../../base/common/hash.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IWebWorkerServerRequestHandler } from '../../../../../base/common/worker/webWorker.js'; +import { PieceTreeTextBufferBuilder } from '../../../../../editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.js'; +import { CellKind, IMainCellDto, INotebookDiffResult, IOutputDto, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookDocumentMetadata, TransientDocumentMetadata } from '../notebookCommon.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { SearchParams } from '../../../../../editor/common/model/textModelSearch.js'; +import { MirrorModel } from '../../../../../editor/common/services/textModelSync/textModelSync.impl.js'; +import { DefaultEndOfLine } from '../../../../../editor/common/model.js'; +import { IModelChangedEvent } from '../../../../../editor/common/model/mirrorTextModel.js'; +import { filter } from '../../../../../base/common/objects.js'; +import { matchCellBasedOnSimilarties } from './notebookCellMatching.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { DiffChange } from '../../../../../base/common/diff/diffChange.js'; +import { computeDiff } from '../notebookDiff.js'; + +const PREFIX_FOR_UNMATCHED_ORIGINAL_CELLS = `unmatchedOriginalCell`; + +class MirrorCell { + private readonly textModel: MirrorModel; + private _hash?: number; + public get eol() { + return this._eol === '\r\n' ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF; + } + constructor( + public readonly handle: number, + uri: URI, + source: string[], + private readonly _eol: string, + versionId: number, + public language: string, + public cellKind: CellKind, + public outputs: IOutputDto[], + public metadata?: NotebookCellMetadata, + public internalMetadata?: NotebookCellInternalMetadata, + + ) { + this.textModel = new MirrorModel(uri, source, _eol, versionId); + } + + onEvents(e: IModelChangedEvent) { + this.textModel.onEvents(e); + this._hash = undefined; + } + getValue(): string { + return this.textModel.getValue(); + } + + getLinesContent(): string[] { + return this.textModel.getLinesContent(); + } + getComparisonValue(): number { + return this._hash ??= this._getHash(); + } + + private _getHash() { + let hashValue = numberHash(104579, 0); + + hashValue = doHash(this.language, hashValue); + hashValue = doHash(this.getValue(), hashValue); + hashValue = doHash(this.metadata, hashValue); + // For purpose of diffing only cellId matters, rest do not + hashValue = doHash(this.internalMetadata?.internalId || '', hashValue); + for (const op of this.outputs) { + hashValue = doHash(op.metadata, hashValue); + for (const output of op.outputs) { + hashValue = doHash(output.mime, hashValue); + } + } + + const digests = this.outputs.flatMap(op => + op.outputs.map(o => hash(Array.from(o.data.buffer))) + ); + for (const digest of digests) { + hashValue = numberHash(digest, hashValue); + } + + return hashValue; + } +} + +class MirrorNotebookDocument { + constructor( + readonly uri: URI, + public cells: MirrorCell[], + public metadata: NotebookDocumentMetadata, + public transientDocumentMetadata: TransientDocumentMetadata, + ) { + } + + acceptModelChanged(event: NotebookCellsChangedEventDto) { + // note that the cell content change is not applied to the MirrorCell + // but it's fine as if a cell content is modified after the first diff, its position will not change any more + // TODO@rebornix, but it might lead to interesting bugs in the future. + event.rawEvents.forEach(e => { + if (e.kind === NotebookCellsChangeType.ModelChange) { + this._spliceNotebookCells(e.changes); + } else if (e.kind === NotebookCellsChangeType.Move) { + const cells = this.cells.splice(e.index, 1); + this.cells.splice(e.newIdx, 0, ...cells); + } else if (e.kind === NotebookCellsChangeType.Output) { + const cell = this.cells[e.index]; + cell.outputs = e.outputs; + } else if (e.kind === NotebookCellsChangeType.ChangeCellLanguage) { + this._assertIndex(e.index); + const cell = this.cells[e.index]; + cell.language = e.language; + } else if (e.kind === NotebookCellsChangeType.ChangeCellMetadata) { + this._assertIndex(e.index); + const cell = this.cells[e.index]; + cell.metadata = e.metadata; + } else if (e.kind === NotebookCellsChangeType.ChangeCellInternalMetadata) { + this._assertIndex(e.index); + const cell = this.cells[e.index]; + cell.internalMetadata = e.internalMetadata; + } else if (e.kind === NotebookCellsChangeType.ChangeDocumentMetadata) { + this.metadata = e.metadata; + } + }); + } + + private _assertIndex(index: number): void { + if (index < 0 || index >= this.cells.length) { + throw new Error(`Illegal index ${index}. Cells length: ${this.cells.length}`); + } + } + + _spliceNotebookCells(splices: NotebookCellTextModelSplice[]) { + splices.reverse().forEach(splice => { + const cellDtos = splice[2]; + const newCells = cellDtos.map(cell => { + return new MirrorCell( + cell.handle, + URI.parse(cell.url), + cell.source, + cell.eol, + cell.versionId, + cell.language, + cell.cellKind, + cell.outputs, + cell.metadata, + ); + }); + + this.cells.splice(splice[0], splice[1], ...newCells); + }); + } +} + +class CellSequence implements ISequence { + + static create(textModel: MirrorNotebookDocument) { + const hashValue = textModel.cells.map(c => c.getComparisonValue()); + return new CellSequence(hashValue); + } + static createWithCellId(cells: MirrorCell[], includeCellContents?: boolean) { + const hashValue = cells.map((c) => { + if (includeCellContents) { + return `${doHash(c.internalMetadata?.internalId, numberHash(104579, 0))}#${c.getComparisonValue()}`; + } else { + return `${doHash(c.internalMetadata?.internalId, numberHash(104579, 0))}}`; + } + }); + return new CellSequence(hashValue); + } + + constructor(readonly hashValue: number[] | string[]) { } + + getElements(): string[] | number[] | Int32Array { + return this.hashValue; + } +} + +export class NotebookWorker implements IWebWorkerServerRequestHandler, IDisposable { + _requestHandlerBrand: any; + + private _models: { [uri: string]: MirrorNotebookDocument }; + + constructor() { + this._models = Object.create(null); + } + dispose(): void { + } + + public $acceptNewModel(uri: string, metadata: NotebookDocumentMetadata, transientDocumentMetadata: TransientDocumentMetadata, cells: IMainCellDto[]): void { + this._models[uri] = new MirrorNotebookDocument(URI.parse(uri), cells.map(dto => new MirrorCell( + dto.handle, + URI.parse(dto.url), + dto.source, + dto.eol, + dto.versionId, + dto.language, + dto.cellKind, + dto.outputs, + dto.metadata, + dto.internalMetadata + )), metadata, transientDocumentMetadata); + } + + public $acceptModelChanged(strURL: string, event: NotebookCellsChangedEventDto) { + const model = this._models[strURL]; + model?.acceptModelChanged(event); + } + + public $acceptCellModelChanged(strURL: string, handle: number, event: IModelChangedEvent) { + const model = this._models[strURL]; + model.cells.find(cell => cell.handle === handle)?.onEvents(event); + } + + public $acceptRemovedModel(strURL: string): void { + if (!this._models[strURL]) { + return; + } + delete this._models[strURL]; + } + + async $computeDiff(originalUrl: string, modifiedUrl: string): Promise { + const original = this._getModel(originalUrl); + const modified = this._getModel(modifiedUrl); + + const originalModel = new NotebookTextModelFacade(original); + const modifiedModel = new NotebookTextModelFacade(modified); + + const originalMetadata = filter(original.metadata, key => !original.transientDocumentMetadata[key]); + const modifiedMetadata = filter(modified.metadata, key => !modified.transientDocumentMetadata[key]); + const metadataChanged = JSON.stringify(originalMetadata) !== JSON.stringify(modifiedMetadata); + // TODO@DonJayamanne + // In the future we might want to avoid computing LCS of outputs + // That will make this faster. + const originalDiff = new LcsDiff(CellSequence.create(original), CellSequence.create(modified)).ComputeDiff(false); + if (originalDiff.changes.length === 0) { + return { + metadataChanged, + cellsDiff: originalDiff + }; + } + + // This will return the mapping of the cells and what cells were inserted/deleted. + // We do not care much about accuracy of the diff, but care about the mapping of unmodified cells. + // That can be used as anchor points to find the cells that have changed. + // And on cells that have changed, we can use similarity algorithms to find the mapping. + // Eg as mentioned earlier, its possible after similarity algorithms we find that cells weren't inserted/deleted but were just modified. + const cellMapping = computeDiff(originalModel, modifiedModel, { cellsDiff: { changes: originalDiff.changes, quitEarly: false }, metadataChanged: false, }).cellDiffInfo; + + // If we have no insertions/deletions, then this is a good diffing. + if (cellMapping.every(c => c.type === 'modified')) { + return { + metadataChanged, + cellsDiff: originalDiff + }; + } + + let diffUsingCellIds = this.canComputeDiffWithCellIds(original, modified); + if (!diffUsingCellIds) { + /** + * Assume we have cells as follows + * Original Modified + * A A + * B B + * C e + * D F + * E + * F + * + * Using LCS we know easily that A, B cells match. + * Using LCS it would look like C changed to e + * Using LCS D & E were removed. + * + * A human would be able to tell that cell C, D were removed. + * A human can tell that E changed to e because the code in the cells are very similar. + * Note the words `similar`, humans try to match cells based on certain heuristics. + * & the most obvious one is the similarity of the code in the cells. + * + * LCS has no notion of similarity, it only knows about equality. + * We can use other algorithms to find similarity. + * So if we eliminate A, B, we are left with C, D, E, F and we need to find what they map to in `e, F` in modifed document. + * We can use a similarity algorithm to find that. + * + * The purpose of using LCS first is to find the cells that have not changed. + * This avoids the need to use similarity algorithms on all cells. + * + * At the end of the day what we need is as follows + * A <=> A + * B <=> B + * C => Deleted + * D => Deleted + * E => e + * F => F + */ + + + + // Note, if cells are swapped, then this compilicates things + // Trying to solve diff manually is not easy. + // Lets instead use LCS find the cells that haven't changed, + // & the cells that have. + // For the range of cells that have change, lets see if we can get better results using similarity algorithms. + // Assume we have + // Code Cell = print("Hello World") + // Code Cell = print("Foo Bar") + // We now change this to + // MD Cell = # Description + // Code Cell = print("Hello WorldZ") + // Code Cell = print("Foo BarZ") + // LCS will tell us that everything changed. + // But using similarity algorithms we can tell that the first cell is new and last 2 changed. + + + + // Lets try the similarity algorithms on all cells. + // We might fare better. + const result = matchCellBasedOnSimilarties(modified.cells, original.cells); + // If we have at least one match, then great. + if (result.some(c => c.original !== -1)) { + // We have managed to find similarities between cells. + // Now we can definitely find what cell is new/removed. + this.updateCellIdsBasedOnMappings(result, original.cells, modified.cells); + diffUsingCellIds = true; + } + } + + if (!diffUsingCellIds) { + return { + metadataChanged, + cellsDiff: originalDiff + }; + } + + // At this stage we can use internalMetadata.cellId for tracking changes. + // I.e. we compute LCS diff and the hashes of some cells from original will be equal to that in modified as we're using cellId. + // Thus we can find what cells are new/deleted. + // After that we can find whether the contents of the cells changed. + const cellsInsertedOrDeletedDiff = new LcsDiff(CellSequence.createWithCellId(original.cells), CellSequence.createWithCellId(modified.cells)).ComputeDiff(false); + const cellDiffInfo = computeDiff(originalModel, modifiedModel, { cellsDiff: { changes: cellsInsertedOrDeletedDiff.changes, quitEarly: false }, metadataChanged: false, }).cellDiffInfo; + + let processedIndex = 0; + const changes: IDiffChange[] = []; + cellsInsertedOrDeletedDiff.changes.forEach(change => { + if (!change.originalLength && change.modifiedLength) { + // Inserted. + // Find all modified cells before this. + const changeIndex = cellDiffInfo.findIndex(c => c.type === 'insert' && c.modifiedCellIndex === change.modifiedStart); + cellDiffInfo.slice(processedIndex, changeIndex).forEach(c => { + if (c.type === 'unchanged' || c.type === 'modified') { + const originalCell = original.cells[c.originalCellIndex]; + const modifiedCell = modified.cells[c.modifiedCellIndex]; + const changed = c.type === 'modified' || originalCell.getComparisonValue() !== modifiedCell.getComparisonValue(); + if (changed) { + changes.push(new DiffChange(c.originalCellIndex, 1, c.modifiedCellIndex, 1)); + } + } + }); + changes.push(change); + processedIndex = changeIndex + 1; + } else if (change.originalLength && !change.modifiedLength) { + // Deleted. + // Find all modified cells before this. + const changeIndex = cellDiffInfo.findIndex(c => c.type === 'delete' && c.originalCellIndex === change.originalStart); + cellDiffInfo.slice(processedIndex, changeIndex).forEach(c => { + if (c.type === 'unchanged' || c.type === 'modified') { + const originalCell = original.cells[c.originalCellIndex]; + const modifiedCell = modified.cells[c.modifiedCellIndex]; + const changed = c.type === 'modified' || originalCell.getComparisonValue() !== modifiedCell.getComparisonValue(); + if (changed) { + changes.push(new DiffChange(c.originalCellIndex, 1, c.modifiedCellIndex, 1)); + } + } + }); + changes.push(change); + processedIndex = changeIndex + 1; + } else { + // This could be a situation where a cell has been deleted on left and inserted on the right. + // E.g. markdown cell deleted and code cell inserted. + // But LCS shows them as a modification. + const changeIndex = cellDiffInfo.findIndex(c => (c.type === 'delete' && c.originalCellIndex === change.originalStart) || (c.type === 'insert' && c.modifiedCellIndex === change.modifiedStart)); + cellDiffInfo.slice(processedIndex, changeIndex).forEach(c => { + if (c.type === 'unchanged' || c.type === 'modified') { + const originalCell = original.cells[c.originalCellIndex]; + const modifiedCell = modified.cells[c.modifiedCellIndex]; + const changed = c.type === 'modified' || originalCell.getComparisonValue() !== modifiedCell.getComparisonValue(); + if (changed) { + changes.push(new DiffChange(c.originalCellIndex, 1, c.modifiedCellIndex, 1)); + } + } + }); + changes.push(change); + processedIndex = changeIndex + 1; + } + }); + cellDiffInfo.slice(processedIndex).forEach(c => { + if (c.type === 'unchanged' || c.type === 'modified') { + const originalCell = original.cells[c.originalCellIndex]; + const modifiedCell = modified.cells[c.modifiedCellIndex]; + const changed = c.type === 'modified' || originalCell.getComparisonValue() !== modifiedCell.getComparisonValue(); + if (changed) { + changes.push(new DiffChange(c.originalCellIndex, 1, c.modifiedCellIndex, 1)); + } + } + }); + + return { + metadataChanged, + cellsDiff: { + changes, + quitEarly: false + } + }; + } + + canComputeDiffWithCellIds(original: MirrorNotebookDocument, modified: MirrorNotebookDocument): boolean { + return this.canComputeDiffWithCellInternalIds(original, modified) || this.canComputeDiffWithCellMetadataIds(original, modified); + } + + canComputeDiffWithCellInternalIds(original: MirrorNotebookDocument, modified: MirrorNotebookDocument): boolean { + const originalCellIndexIds = original.cells.map((cell, index) => ({ index, id: (cell.internalMetadata?.internalId || '') as string })); + const modifiedCellIndexIds = modified.cells.map((cell, index) => ({ index, id: (cell.internalMetadata?.internalId || '') as string })); + // If we have a cell without an id, do not use metadata.id for diffing. + if (originalCellIndexIds.some(c => !c.id) || modifiedCellIndexIds.some(c => !c.id)) { + return false; + } + // If none of the ids in original can be found in modified, then we can't use metadata.id for diffing. + // I.e. everything is new, no point trying. + return originalCellIndexIds.some(c => modifiedCellIndexIds.find(m => m.id === c.id)); + } + + canComputeDiffWithCellMetadataIds(original: MirrorNotebookDocument, modified: MirrorNotebookDocument): boolean { + const originalCellIndexIds = original.cells.map((cell, index) => ({ index, id: (cell.metadata?.id || '') as string })); + const modifiedCellIndexIds = modified.cells.map((cell, index) => ({ index, id: (cell.metadata?.id || '') as string })); + // If we have a cell without an id, do not use metadata.id for diffing. + if (originalCellIndexIds.some(c => !c.id) || modifiedCellIndexIds.some(c => !c.id)) { + return false; + } + // If none of the ids in original can be found in modified, then we can't use metadata.id for diffing. + // I.e. everything is new, no point trying. + if (originalCellIndexIds.every(c => !modifiedCellIndexIds.find(m => m.id === c.id))) { + return false; + } + + // Internally we use internalMetadata.cellId for diffing, hence update the internalMetadata.cellId + original.cells.map((cell, index) => { + cell.internalMetadata = cell.internalMetadata || {}; + cell.internalMetadata.internalId = cell.metadata?.id as string || ''; + }); + modified.cells.map((cell, index) => { + cell.internalMetadata = cell.internalMetadata || {}; + cell.internalMetadata.internalId = cell.metadata?.id as string || ''; + }); + return true; + } + + + isOriginalCellMatchedWithModifiedCell(originalCell: MirrorCell) { + return (originalCell.internalMetadata?.internalId as string || '').startsWith(PREFIX_FOR_UNMATCHED_ORIGINAL_CELLS); + } + updateCellIdsBasedOnMappings(mappings: { modified: number; original: number }[], originalCells: MirrorCell[], modifiedCells: MirrorCell[]): boolean { + const uuids = new Map(); + originalCells.map((cell, index) => { + cell.internalMetadata = cell.internalMetadata || { internalId: '' }; + cell.internalMetadata.internalId = `${PREFIX_FOR_UNMATCHED_ORIGINAL_CELLS}${generateUuid()}`; + const found = mappings.find(r => r.original === index); + if (found) { + // Do not use the indexes as ids. + // If we do, then the hashes will be very similar except for last digit. + cell.internalMetadata.internalId = generateUuid(); + uuids.set(found.modified, cell.internalMetadata.internalId as string); + } + }); + modifiedCells.map((cell, index) => { + cell.internalMetadata = cell.internalMetadata || { internalId: '' }; + cell.internalMetadata.internalId = uuids.get(index) ?? generateUuid(); + }); + return true; + } + + $canPromptRecommendation(modelUrl: string): boolean { + const model = this._getModel(modelUrl); + const cells = model.cells; + + for (let i = 0; i < cells.length; i++) { + const cell = cells[i]; + if (cell.cellKind === CellKind.Markup) { + continue; + } + + if (cell.language !== 'python') { + continue; + } + + const searchParams = new SearchParams('import\\s*pandas|from\\s*pandas', true, false, null); + const searchData = searchParams.parseSearchRequest(); + + if (!searchData) { + continue; + } + + const builder = new PieceTreeTextBufferBuilder(); + builder.acceptChunk(cell.getValue()); + const bufferFactory = builder.finish(true); + const textBuffer = bufferFactory.create(cell.eol).textBuffer; + + const lineCount = textBuffer.getLineCount(); + const maxLineCount = Math.min(lineCount, 20); + const range = new Range(1, 1, maxLineCount, textBuffer.getLineLength(maxLineCount) + 1); + const cellMatches = textBuffer.findMatchesLineByLine(range, searchData, true, 1); + if (cellMatches.length > 0) { + return true; + } + } + + return false; + } + + protected _getModel(uri: string): MirrorNotebookDocument { + return this._models[uri]; + } +} + +export function create(): IWebWorkerServerRequestHandler { + return new NotebookWorker(); +} + +export type CellDiffInfo = { + originalCellIndex: number; + modifiedCellIndex: number; + type: 'unchanged' | 'modified'; +} | +{ + originalCellIndex: number; + type: 'delete'; +} | +{ + modifiedCellIndex: number; + type: 'insert'; +}; + +interface ICell { + cellKind: CellKind; + getHashValue(): number; + equal(cell: ICell): boolean; +} + +class NotebookTextModelFacade { + public readonly cells: readonly ICell[]; + constructor( + readonly notebook: MirrorNotebookDocument + ) { + + this.cells = notebook.cells.map(cell => new NotebookCellTextModelFacade(cell)); + } + +} +class NotebookCellTextModelFacade implements ICell { + get cellKind(): CellKind { + return this.cell.cellKind; + } + constructor( + private readonly cell: MirrorCell + ) { + } + getHashValue(): number { + return this.cell.getComparisonValue(); + } + equal(cell: ICell): boolean { + if (cell.cellKind !== this.cellKind) { + return false; + } + return this.getHashValue() === cell.getHashValue(); + } + +} diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorkerMain.ts b/src/vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain.ts similarity index 65% rename from src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorkerMain.ts rename to src/vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain.ts index edebb727..80021b49 100644 --- a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorkerMain.ts +++ b/src/vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { bootstrapSimpleWorker } from '../../../../../base/common/worker/simpleWorkerBootstrap.js'; -import { create } from './notebookSimpleWorker.js'; +import { bootstrapWebWorker } from '../../../../../base/common/worker/webWorkerBootstrap.js'; +import { create } from './notebookWebWorker.js'; -bootstrapSimpleWorker(create); +bootstrapWebWorker(create); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts index 73ddc03a..5c218da6 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts @@ -14,13 +14,14 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { IMarkerData, IMarkerService } from '../../../../../../platform/markers/common/markers.js'; -import { ChatAgentLocation, IChatAgent, IChatAgentData, IChatAgentService } from '../../../../chat/common/chatAgents.js'; +import { IChatAgent, IChatAgentData, IChatAgentService } from '../../../../chat/common/chatAgents.js'; import { CellDiagnostics } from '../../../browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.js'; import { CodeCellViewModel } from '../../../browser/viewModel/codeCellViewModel.js'; import { CellKind, NotebookSetting } from '../../../common/notebookCommon.js'; import { ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebookCellExecution, INotebookExecutionStateService, NotebookExecutionType } from '../../../common/notebookExecutionStateService.js'; import { setupInstantiationService, TestNotebookExecutionStateService, withTestNotebook } from '../testNotebookEditor.js'; import { nullExtensionDescription } from '../../../../../services/extensions/common/extensions.js'; +import { ChatAgentLocation } from '../../../../chat/common/constants.js'; suite('notebookCellDiagnostics', () => { @@ -71,7 +72,7 @@ suite('notebookCellDiagnostics', () => { extensionPublisherId: '', name: 'testEditorAgent', isDefault: true, - locations: [ChatAgentLocation.Editor], + locations: [ChatAgentLocation.Notebook], metadata: {}, slashCommands: [], disambiguation: [], diff --git a/src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiffService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiffService.test.ts new file mode 100644 index 00000000..29a71805 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiffService.test.ts @@ -0,0 +1,14750 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { Mimes } from '../../../../../../base/common/mime.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { CellKind, IMainCellDto, IOutputDto, NotebookCellMetadata } from '../../../common/notebookCommon.js'; +import { matchCellBasedOnSimilarties } from '../../../common/services/notebookCellMatching.js'; +import { NotebookWorker } from '../../../common/services/notebookWebWorker.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IDiffChange } from '../../../../../../base/common/diff/diff.js'; + + +suite('NotebookDiff Diff Service', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + let worker: NotebookWorker; + suiteSetup(() => { + worker = new NotebookWorker(); + }); + suiteTeardown(() => { + worker.dispose(); + }); + + test('No changes', async () => { + const { mapping, diff } = await mapCells([ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes.length, 0); + }); + + test('diff different source', async () => { + const { mapping, diff } = await mapCells([ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange + ]); + }); + + test('diff different source (2 cells)', async () => { + const { mapping, diff } = await mapCells([ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['z'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange + ]); + }); + + test('diff different source (first cell changed)', async () => { + const { mapping, diff } = await mapCells([ + [['import sys'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['import pandas'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange + ]); + }); + + + test('diff test small source', async () => { + const { mapping, diff } = await mapCells([ + [['123456789'], 'javascript', CellKind.Code, [], {}] + ], [ + [['987654321'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange + ]); + }); + + test('diff test data single cell', async () => { + const { mapping, diff } = await mapCells([ + [[ + '# This version has a bug\n', + 'def mult(a, b):\n', + ' return a / b' + ], 'javascript', CellKind.Code, [], {}] + ], [ + [[ + 'def mult(a, b):\n', + ' \'This version is debugged.\'\n', + ' return a * b' + ], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange + ]); + }); + + test('diff foo/foe (swapped cells)', async () => { + const { mapping, diff } = await mapCells([ + [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], + [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { metadata: { collapsed: false }, executionOrder: 6 }], + [[''], 'javascript', CellKind.Code, [], {}] + ], [ + [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { metadata: { collapsed: false }, executionOrder: 6 }], + [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], + [[''], 'javascript', CellKind.Code, [], {}] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 2 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 1, originalLength: 1, modifiedStart: 2, modifiedLength: 0 } satisfies IDiffChange, + // { originalStart: 1, originalLength: 1, modifiedStart: 2, modifiedLength: 0 } satisfies IDiffChange + ]); + }); + + test('diff foo/foe (swapped cells with changes in output)', async () => { + const { mapping, diff } = await mapCells([ + [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], + [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { metadata: { collapsed: false }, executionOrder: 6 }], + [[''], 'javascript', CellKind.Code, [], {}] + ], [ + [['def foo(x, y):\n', ' return x * y\n', 'foo(1, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([6])) }] }], { metadata: { collapsed: false }, executionOrder: 5 }], + [['def foe(x, y):\n', ' return x + y\n', 'foe(3, 2)'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([2])) }] }], { metadata: { collapsed: false }, executionOrder: 6 }], + [[''], 'javascript', CellKind.Code, [], {}] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 2 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 0, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 1, originalLength: 1, modifiedStart: 2, modifiedLength: 0 } satisfies IDiffChange, + ]); + }); + + test('diff markdown', async () => { + const { mapping, diff } = await mapCells([ + [['This is a test notebook with only markdown cells'], 'markdown', CellKind.Markup, [], {}], + [['Lorem ipsum dolor sit amet'], 'markdown', CellKind.Markup, [], {}], + [['In other news'], 'markdown', CellKind.Markup, [], {}], + ], [ + [['This is a test notebook with markdown cells only'], 'markdown', CellKind.Markup, [], {}], + [['Lorem ipsum dolor sit amet'], 'markdown', CellKind.Markup, [], {}], + [['In the news'], 'markdown', CellKind.Markup, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('diff insert 1 at the top', async () => { + const { mapping, diff } = await mapCells([ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}] + ], [ + [['var h = 8;'], 'javascript', CellKind.Code, [], {}], + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('diff insert 1 at the top with lots of cells', async () => { + + const { mapping, diff } = await mapCells([ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ], [ + [['var h = 8;'], 'javascript', CellKind.Code, [], {}], + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 1 }, + { modified: 3, original: 2 }, + { modified: 4, original: 3 }, + { modified: 5, original: 4 }, + { modified: 6, original: 5 }, + { modified: 7, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('diff insert 3 at the top with lots of cells', async () => { + + const { mapping, diff } = await mapCells([ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ], [ + [['var h = 8;'], 'javascript', CellKind.Code, [], {}], + [['var i = 8;'], 'javascript', CellKind.Code, [], {}], + [['var j = 8;'], 'javascript', CellKind.Code, [], {}], + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 }, + { modified: 1, original: -1 }, + { modified: 2, original: -1 }, + { modified: 3, original: 0 }, + { modified: 4, original: 1 }, + { modified: 5, original: 2 }, + { modified: 6, original: 3 }, + { modified: 7, original: 4 }, + { modified: 8, original: 5 }, + { modified: 9, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 3 } satisfies IDiffChange, + ]); + }); + + test('diff insert 1 in the middle', async () => { + + const { mapping, diff } = await mapCells([ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ], [ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var h = 8;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: -1 }, + { modified: 5, original: 4 }, + { modified: 6, original: 5 }, + { modified: 7, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 4, originalLength: 0, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('LCS (swap cells with outputs)', async () => { + const { mapping, diff } = await mapCells([ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }] + ], [ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('LCS (outputs changed)', async () => { + const { mapping, diff } = await mapCells([ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['y'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }] + ], [ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true } }], + [['y'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Swap cells with output and with same code in each cell', async () => { + const { mapping, diff } = await mapCells([ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + [['x = 5'], 'javascript', CellKind.Code, [], {}], + [['x'], 'javascript', CellKind.Code, [], {}], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], {}], + ], [ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['x = 5'], 'javascript', CellKind.Code, [], {}], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], {}], + [['x'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 6, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Change outputs of cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['y'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + [['x = 5'], 'javascript', CellKind.Code, [], {}], + [['x'], 'javascript', CellKind.Code, [], {}], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], {}], + ], [ + [['# Description'], 'markdown', CellKind.Markup, [], { metadata: {} }], + [['x = 3'], 'javascript', CellKind.Code, [], { metadata: { collapsed: true }, executionOrder: 1 }], + [['x'], 'javascript', CellKind.Code, [], { metadata: { collapsed: false } }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 1 }], + [['x = 5'], 'javascript', CellKind.Code, [], {}], + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], {}], + [['y'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 6, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Insert a new cell', async () => { + const { mapping, diff } = await mapCells([ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ], [ + [['var a = 1;'], 'javascript', CellKind.Code, [], {}], + [['var b = 2;'], 'javascript', CellKind.Code, [], {}], + [['var c = 3;'], 'javascript', CellKind.Code, [], {}], + [['var d = 4;'], 'javascript', CellKind.Code, [], {}], + [['var h = 8;'], 'javascript', CellKind.Code, [], {}], + [['var e = 5;'], 'javascript', CellKind.Code, [], {}], + [['var f = 6;'], 'javascript', CellKind.Code, [], {}], + [['var g = 7;'], 'javascript', CellKind.Code, [], {}], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: -1 },// + { modified: 5, original: 4 }, + { modified: 6, original: 5 }, + { modified: 7, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 4, originalLength: 0, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('diff output', async () => { + const { mapping, diff } = await mapCells([ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('diff output fast check', async () => { + const { mapping, diff } = await mapCells([ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([4])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ], [ + [['x'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([3])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + [['y'], 'javascript', CellKind.Code, [{ outputId: 'someOtherId', outputs: [{ mime: Mimes.text, data: VSBuffer.wrap(new Uint8Array([5])) }] }], { metadata: { collapsed: false }, executionOrder: 3 }], + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('No cells', async () => { + const { mapping, diff } = await mapCells([ + ], [ + ]); + + assert.deepStrictEqual(mapping, [ + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + ]); + }); + test('No cells and add 1 cell', async () => { + const { mapping, diff } = await mapCells([ + ], [ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('One cells and delete 1 cell', async () => { + const { mapping, diff } = await mapCells([ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + ]); + + assert.deepStrictEqual(mapping, [ + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 0 } satisfies IDiffChange, + ]); + }); + test('No changes with 1 cell', async () => { + const { mapping, diff } = await mapCells([ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + // { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 0 } satisfies IDiffChange, + ]); + }); + test('One changes with 1 cell', async () => { + const { mapping, diff } = await mapCells([ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], + [ + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('No changes with 2 cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + // { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('One changes with 2 cells, 1st cell changed', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('2 cells, 1st cell deleted', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 0 } satisfies IDiffChange, + ]); + }); + test('2 cells, inserted a cell in the middle', async () => { + const { mapping, diff } = await mapCells([ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined] + ], [ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("Bar Baz")'], 'python', CellKind.Code, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: -1 }, + { modified: 2, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 0, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('One changes with 2 cells, 2nd cell changed', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ], [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined] + ]); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Two cells, all cells changed', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined] + ] + , [ + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined] + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 2, modifiedStart: 0, modifiedLength: 2 } satisfies IDiffChange, + ]); + }); + test('Insert a md cell and modify the next code cell (few cells)', async () => { + const { mapping, diff } = await mapCells([ + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Description'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello worldz")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 0, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Insert a md cell and modify the next code cell', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz1234")'], 'python', CellKind.Code, [], undefined] + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['# New Markdown cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo baz")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz1234")'], 'python', CellKind.Code, [], undefined] + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: -1 }, + { modified: 4, original: 3 }, + { modified: 5, original: 4 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 3, originalLength: 0, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Delete code cell and insert MD cell, both will not match', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['sys.executable'], 'python', CellKind.Code, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['# Header'], 'python', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: -1 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Delete MD cell and insert code cell, both will not match', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['sys.executable'], 'python', CellKind.Code, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['# Header'], 'python', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: -1 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Insert a md cell and modify the next 2 code cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz")'], 'python', CellKind.Code, [], undefined] + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['# New Markdown cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("Foo BaZ")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz Modified")'], 'python', CellKind.Code, [], undefined] + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: -1 }, + { modified: 4, original: 3 }, + { modified: 5, original: 4 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 3, originalLength: 0, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Insert a md cell, delete a cell and modify the next 2 code cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz")'], 'python', CellKind.Code, [], undefined], + [['print("Fox Trot")'], 'python', CellKind.Code, [], undefined] + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['# New Markdown cell'], 'markdown', CellKind.Markup, [], undefined], + // [['print("Foo BaZ")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz Modified")'], 'python', CellKind.Code, [], undefined], + [['print("Fox Trots")'], 'python', CellKind.Code, [], undefined] + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: -1 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Insert a md cell, delete a code cell and modify the next 2 code cells (deleted and inserted lineup)', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['print("Foo Bar")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz")'], 'python', CellKind.Code, [], undefined], + [['print("Fox Trot")'], 'python', CellKind.Code, [], undefined] + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['sys.executable'], 'python', CellKind.Code, [], undefined], + // [['print("Foo BaZ")'], 'python', CellKind.Code, [], undefined], + [['print("bar baz Modified")'], 'python', CellKind.Code, [], undefined], + [['print("Fox Trot")'], 'python', CellKind.Code, [], undefined] + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + + test('Insert a few md cells and modify the next few code cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# New 1'], 'markdown', CellKind.Markup, [], undefined], + [['# New 2'], 'markdown', CellKind.Markup, [], undefined], + [['# Another MD cell (modified1)'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell (modified)'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: -1 }, + { modified: 5, original: -1 }, + { modified: 6, original: 4 }, + { modified: 7, original: 5 }, + { modified: 8, original: 6 }, + { modified: 9, original: 7 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 4, originalLength: 0, modifiedStart: 4, modifiedLength: 2 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 6, originalLength: 1, modifiedStart: 8, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Insert & delete a few md cells and modify the next few code cells', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# New 1'], 'markdown', CellKind.Markup, [], undefined], + [['# New 2'], 'markdown', CellKind.Markup, [], undefined], + [['# Another MD cell (modified1)'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell (modified)'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: -1 }, + { modified: 5, original: -1 }, + { modified: 6, original: 4 }, + { modified: 7, original: 5 }, + { modified: 8, original: 6 }, + { modified: 9, original: 7 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 4, originalLength: 0, modifiedStart: 4, modifiedLength: 2 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 6, originalLength: 1, modifiedStart: 8, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Modify, then insert ', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo BaZ'], 'markdown', CellKind.Markup, [], undefined], + [['# Header'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: -1 }, + { modified: 4, original: 3 }, + { modified: 5, original: 4 }, + { modified: 6, original: 5 }, + { modified: 7, original: 6 }, + { modified: 8, original: 7 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 0, modifiedStart: 3, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 6, originalLength: 1, modifiedStart: 8, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Modify, then delete ', async () => { + const { mapping, diff } = await mapCells([ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo Bar'], 'markdown', CellKind.Markup, [], undefined], + [['print("foo bar")'], 'python', CellKind.Code, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['sys.executable'], 'python', CellKind.Code, [], undefined], + ] + , [ + [['# Hello World'], 'markdown', CellKind.Markup, [], undefined], + [['print("Hello world")'], 'python', CellKind.Code, [], undefined], + [['# Foo BaZ'], 'markdown', CellKind.Markup, [], undefined], + [['# Another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['import sys'], 'python', CellKind.Code, [], undefined], + [['# Yet another MD cell'], 'markdown', CellKind.Markup, [], undefined], + [['sys.executable'], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 4 }, + { modified: 4, original: 5 }, + { modified: 5, original: 6 }, + { modified: 6, original: 7 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 3, originalLength: 1, modifiedStart: 3, modifiedLength: 0 } satisfies IDiffChange, + // { originalStart: 6, originalLength: 1, modifiedStart: 8, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Update code cell', async () => { + const { mapping, diff } = await mapCells([ + [ + [ + "# This is a simple notebook\n", + "\n", + "There's nothing special here, I am just writing some text and plotting a function" + ], 'markdown', CellKind.Markup, [], undefined], + [ + [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "\n", + "%matplotlib inline" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "x = np.linspace(0, 4*np.pi,50)\n", + "y = np.sin(x)\n", + "\n", + "plt.plot(x, y)" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "df = pd.DataFrame({f\"column_{c}\": np.random.normal(size=100) for c in range(100)})\n", + "df" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "You could actually do some neat stuff with it, pandas like changing the colors (values above 1 should be green)" + ], 'python', CellKind.Markup, [], undefined], + [ + [ + "df.style.applymap(lambda x: \"background-color: green\" if x>1 else \"background-color: white\")" + ], 'python', CellKind.Code, [], undefined], + [ + [], 'python', CellKind.Code, [], undefined], + ] + , [ + [ + [ + "# This is a simple notebook\n", + "\n", + "There's nothing special here, I am just writing some text and plotting a function" + ], 'markdown', CellKind.Markup, [], undefined], + [ + [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "\n", + "%matplotlib inline" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "x = np.linspace(0, 4*np.pi,50)\n", + "y = 2 * np.sin(x)\n", + "\n", + "plt.plot(x, y)" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "df = pd.DataFrame({f\"column_{c}\": np.random.normal(size=100) for c in range(100)})\n", + "df" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "You could actually do some neat stuff with it, pandas like changing the colors (values above 1 should be green)" + ], 'python', CellKind.Markup, [], undefined], + [ + [ + "df.style.applymap(lambda x: \"background-color: green\" if x>1 else \"background-color: white\")" + ], 'python', CellKind.Code, [], undefined], + [ + [], 'python', CellKind.Code, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + ]); + + }); + test('Update code cell 1', async () => { + const { mapping, diff } = await mapCells([ + [ + [ + "# Hello World" + ], 'markdown', CellKind.Markup, [], undefined], + [ + [ + "import os\n", + "from pprint import pprint \n", + "from sklearn.preprocessing import LabelEncoder\n", + "import ast\n", + "import pandas as pd\n", + "from clipper import clipper\n", + "import ffmpeg" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "data_dir = '../../../app/data/'\n", + "output_dir = 'output/'\n", + "video_dir = 'videos/'\n", + "\n", + "file_path = data_dir+output_dir+data_file\n", + "video_path = data_dir+video_dir+video_file\n", + "\n", + "df = pd.read_csv(file_path)\n", + "df.head()" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "# convert the string representation of results to a list\n", + "df['list_categories'] = df['categories'].apply(ast.literal_eval)\n", + "\n", + "# get most likely label from each list\n", + "df['label'] = df['list_categories'].apply(lambda x: x[0] if x else None)" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "# initialize the LabelEncoder\n", + "label_encoder = LabelEncoder()\n", + "\n", + "# fit and transform the label to a numerical value\n", + "numerical_data = label_encoder.fit_transform(df['label'])\n", + "\n", + "df['label_value'] = numerical_data" + ], 'python', CellKind.Markup, [], undefined], + ] + , [ + [ + [ + "# Updated markdown cell" + ], 'markdown', CellKind.Markup, [], undefined], + [ + [ + "from sklearn.preprocessing import LabelEncoder\n", + "import ast\n", + "import pandas as pd\n", + "from clipper import clipper\n", + "import ffmpeg" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "data_dir = '../../../app/data/'\n", + "output_dir = 'output/'\n", + "video_dir = 'videos/'\n", + "\n", + "file_path = data_dir+output_dir+data_file\n", + "video_path = data_dir+video_dir+video_file\n", + "\n", + "df = pd.read_csv(file_path)\n", + "df.head()" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "# convert the string representation of results to a list\n", + "df['list_categories'] = df['categories'].apply(ast.literal_eval)\n", + "\n", + "# get most likely label from each list\n", + "df['label'] = df['list_categories'].apply(lambda x: x[0] if x else None)" + ], 'python', CellKind.Code, [], undefined], + [ + [ + "# initialize the LabelEncoder\n", + "label_encoder = LabelEncoder()\n", + "\n", + "# fit and transform the label to a numerical value\n", + "numerical_data = label_encoder.fit_transform(df['label'])\n", + "\n", + "df['label_value'] = numerical_data" + ], 'python', CellKind.Markup, [], undefined], + ] + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 1, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + ]); + }); + test('Detect modification and insertion of cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "Make sure your [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) is working properly.\n", + "To avoid errors that may arise from the version of the dependency package, it is recommended to use the **JupyterLab** instead of the Jupyter notebook to display image results.\n", + "```\n", + "- pip install --upgrade pip && pip install -r requirements.txt\n", + "- jupyter labextension install --no-build @jupyter-widgets/jupyterlab-manager\n", + "- jupyter labextension install --no-build jupyter-datawidgets/extension\n", + "- jupyter labextension install jupyter-threejs\n", + "- jupyter labextension list\n", + "```\n", + "\n", + "You should see:\n", + "```\n", + "JupyterLab v...\n", + " ...\n", + " jupyterlab-datawidgets v... enabled OK\n", + " @jupyter-widgets/jupyterlab-manager v... enabled OK\n", + " jupyter-threejs v... enabled OK\n", + "```\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using a Jupyter Notebook release. Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino-dev>=2024.0.0\" \"opencv-python\" \"torch\" \"onnx<1.16.2\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "with open(\"notebook_utils.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/engine3js.py\",\n", + ")\n", + "with open(\"engine3js.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "import notebook_utils as utils\n", + "import engine3js as engine" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We use `omz_downloader`, which is a command line tool from the `openvino-dev` package. `omz_downloader` automatically creates a directory structure and downloads the selected model." + ] + }, + { + "cell_type": "code", + "source": [ + "# directory where model will be downloaded\n", + "base_model_dir = \"model\"\n", + "\n", + "# model name as named in Open Model Zoo\n", + "model_name = \"human-pose-estimation-3d-0001\"\n", + "# selected precision (FP32, FP16)\n", + "precision = \"FP32\"\n", + "\n", + "BASE_MODEL_NAME = f\"{base_model_dir}/public/{model_name}/{model_name}\"\n", + "model_path = Path(BASE_MODEL_NAME).with_suffix(\".pth\")\n", + "onnx_path = Path(BASE_MODEL_NAME).with_suffix(\".onnx\")\n", + "\n", + "ir_model_path = Path(f\"model/public/{model_name}/{precision}/{model_name}.xml\")\n", + "model_weights_path = Path(f\"model/public/{model_name}/{precision}/{model_name}.bin\")\n", + "\n", + "if not model_path.exists():\n", + " download_command = f\"omz_downloader \" f\"--name {model_name} \" f\"--output_dir {base_model_dir}\"\n", + " ! $download_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The selected model comes from the public directory, which means it must be converted into OpenVINO Intermediate Representation (OpenVINO IR). We use `omz_converter` to convert the ONNX format model to the OpenVINO IR format." + ] + }, + { + "cell_type": "code", + "source": [ + "if not onnx_path.exists():\n", + " convert_command = (\n", + " f\"omz_converter \" f\"--name {model_name} \" f\"--precisions {precision} \" f\"--download_dir {base_model_dir} \" f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $convert_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "device = utils.device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(model=ir_model_path, weights=model_weights_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)\n", + "infer_request = compiled_model.create_infer_request()\n", + "input_tensor_name = model.inputs[0].get_any_name()\n", + "\n", + "# get input and output names of nodes\n", + "input_layer = compiled_model.input(0)\n", + "output_layers = list(compiled_model.outputs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The input for the model is data from the input image and the outputs are heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "input_layer.any_name, [o.any_name for o in output_layers]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[None,]\n", + " infer_request.infer({input_tensor_name: img})\n", + " # A set of three inference results is obtained\n", + " results = {name: infer_request.get_tensor(name).data[:] for name in {\"features\", \"heatmaps\", \"pafs\"}}\n", + " # Get the results\n", + " results = (results[\"features\"][0], results[\"heatmaps\"][0], results[\"pafs\"][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1],\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15],\n", + " [15, 16], # nose - l_eye - l_ear\n", + " [1, 17],\n", + " [17, 18], # nose - r_eye - r_ear\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16],\n", + " [16, 18], # nose - l_eye - l_ear\n", + " [1, 15],\n", + " [15, 17], # nose - r_eye - r_ear\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(source, flip=flip, fps=30, skip_first_frames=skip_frames)\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(grid=True, axis=True, view_width=windows_width, view_height=windows_height)\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(format=\"jpg\", height=windows_height, width=windows_width)\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2]))\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = engine.parse_poses(inference_result, 1, stride, focal_length, True)\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://storage.openvinotoolkit.org/data/test_data/videos/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + , [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "Make sure your [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) is working properly.\n", + "To avoid errors that may arise from the version of the dependency package, it is recommended to use the **JupyterLab** instead of the Jupyter notebook to display image results.\n", + "```\n", + "- pip install --upgrade pip && pip install -r requirements.txt\n", + "- jupyter labextension install --no-build @jupyter-widgets/jupyterlab-manager\n", + "- jupyter labextension install --no-build jupyter-datawidgets/extension\n", + "- jupyter labextension install jupyter-threejs\n", + "- jupyter labextension list\n", + "```\n", + "\n", + "You should see:\n", + "```\n", + "JupyterLab v...\n", + " ...\n", + " jupyterlab-datawidgets v... enabled OK\n", + " @jupyter-widgets/jupyterlab-manager v... enabled OK\n", + " jupyter-threejs v... enabled OK\n", + "```\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using a Jupyter Notebook release. Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino>=2024.4.0\" \"opencv-python\" \"torch\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "with open(\"notebook_utils.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/engine3js.py\",\n", + ")\n", + "with open(\"engine3js.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "import notebook_utils as utils\n", + "import engine3js as engine" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "source": [ + "from notebook_utils import download_file\n", + "import tarfile\n", + "\n", + "\n", + "# directory where model will be downloaded\n", + "base_model_dir = Path(\"model\")\n", + "\n", + "download_file(\"https://storage.openvinotoolkit.org/repositories/open_model_zoo/public/2022.1/human-pose-estimation-3d-0001/human-pose-estimation-3d.tar.gz\", directory=base_model_dir)\n", + "\n", + "ckpt_file = base_model_dir / \"human-pose-estimation-3d-0001.pth\"\n", + "\n", + "if not ckpt_file.exists():\n", + " with tarfile.open(base_model_dir / \"human-pose-estimation-3d.tar.gz\") as f:\n", + " f.extractall(base_model_dir)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "source": [ + "import torch\n", + "import openvino as ov\n", + "\n", + "ov_model_path = Path(base_model_dir) / \"human-pose-estimation-3d-0001.xml\"\n", + "\n", + "if not ov_model_path.exists():\n", + " from model.model import PoseEstimationWithMobileNet\n", + "\n", + " pose_estimation_model = PoseEstimationWithMobileNet(is_convertible_by_mo=True)\n", + " pose_estimation_model.loas_state_dict(torch.load(ckpt_file, map_location=\"cpu\"))\n", + " pose_estimation_model.eval()\n", + "\n", + " with torch.no_grad():\n", + " ov_model = ov.convert_model(pose_estimation_model, example_input=torch.zeros([1, 3, 256, 448]), input=[1, 3, 256, 448])\n", + " ov.save_model(ov_model, ov_model_path)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "device = utils.device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(ov_model_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " mean_value = 128.0\n", + " scale_value = 255.0\n", + "\n", + " img = (img - mean_value) / scale_value\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[None,]\n", + " result = compiled_model(img)\n", + " # Get the results\n", + " results = (result[0][0], results[1][0], results[2][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1],\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15],\n", + " [15, 16], # nose - l_eye - l_ear\n", + " [1, 17],\n", + " [17, 18], # nose - r_eye - r_ear\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16],\n", + " [16, 18], # nose - l_eye - l_ear\n", + " [1, 15],\n", + " [15, 17], # nose - r_eye - r_ear\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(source, flip=flip, fps=30, skip_first_frames=skip_frames)\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(grid=True, axis=True, view_width=windows_width, view_height=windows_height)\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(format=\"jpg\", height=windows_height, width=windows_width)\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2]))\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = engine.parse_poses(inference_result, 1, stride, focal_length, True)\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://storage.openvinotoolkit.org/data/test_data/videos/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 15 }, + { modified: 14, original: 16 }, + { modified: 15, original: 17 }, + { modified: 16, original: 18 }, + { modified: 17, original: 19 }, + { modified: 18, original: 20 }, + { modified: 19, original: 21 }, + { modified: 20, original: 22 }, + { modified: 21, original: 23 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 2, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + { + modifiedLength: 1, + modifiedStart: 6, + originalLength: 1, + originalStart: 6 + }, + { + modifiedLength: 1, + modifiedStart: 7, + originalLength: 1, + originalStart: 7 + }, + { + modifiedLength: 1, + modifiedStart: 8, + originalLength: 1, + originalStart: 8 + }, + { + modifiedLength: 1, + modifiedStart: 12, + originalLength: 1, + originalStart: 12 + }, + { + modifiedLength: 0, + modifiedStart: 13, + originalLength: 2, + originalStart: 13 + }, + { + modifiedLength: 1, + modifiedStart: 14, + originalLength: 1, + originalStart: 16 + } + ]); + }); + test('Detect modification in 2 cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using the latest Jupyter Notebook release (2.4.1). Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino-dev>=2024.0.0\" \"opencv-python\" \"torch\" \"onnx\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import sys\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "import notebook_utils as utils\n", + "\n", + "sys.path.append(\"./engine\")\n", + "import engine.engine3js as engine\n", + "from engine.parse_poses import parse_poses" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We use `omz_downloader`, which is a command line tool from the `openvino-dev` package. `omz_downloader` automatically creates a directory structure and downloads the selected model." + ] + }, + { + "cell_type": "code", + "source": [ + "# directory where model will be downloaded\n", + "base_model_dir = \"model\"\n", + "\n", + "# model name as named in Open Model Zoo\n", + "model_name = \"human-pose-estimation-3d-0001\"\n", + "# selected precision (FP32, FP16)\n", + "precision = \"FP32\"\n", + "\n", + "BASE_MODEL_NAME = f\"{base_model_dir}/public/{model_name}/{model_name}\"\n", + "model_path = Path(BASE_MODEL_NAME).with_suffix(\".pth\")\n", + "onnx_path = Path(BASE_MODEL_NAME).with_suffix(\".onnx\")\n", + "\n", + "ir_model_path = f\"model/public/{model_name}/{precision}/{model_name}.xml\"\n", + "model_weights_path = f\"model/public/{model_name}/{precision}/{model_name}.bin\"\n", + "\n", + "if not model_path.exists():\n", + " download_command = f\"omz_downloader \" f\"--name {model_name} \" f\"--output_dir {base_model_dir}\"\n", + " ! $download_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The selected model comes from the public directory, which means it must be converted into OpenVINO Intermediate Representation (OpenVINO IR). We use `omz_converter` to convert the ONNX format model to the OpenVINO IR format." + ] + }, + { + "cell_type": "code", + "source": [ + "if not onnx_path.exists():\n", + " convert_command = (\n", + " f\"omz_converter \" f\"--name {model_name} \" f\"--precisions {precision} \" f\"--download_dir {base_model_dir} \" f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $convert_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()\n", + "\n", + "device = widgets.Dropdown(\n", + " options=core.available_devices + [\"AUTO\"],\n", + " value=\"AUTO\",\n", + " description=\"Device:\",\n", + " disabled=False,\n", + ")\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(model=ir_model_path, weights=model_weights_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)\n", + "infer_request = compiled_model.create_infer_request()\n", + "input_tensor_name = model.inputs[0].get_any_name()\n", + "\n", + "# get input and output names of nodes\n", + "input_layer = compiled_model.input(0)\n", + "output_layers = list(compiled_model.outputs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The input for the model is data from the input image and the outputs are heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "input_layer.any_name, [o.any_name for o in output_layers]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[None,]\n", + " infer_request.infer({input_tensor_name: img})\n", + " # A set of three inference results is obtained\n", + " results = {name: infer_request.get_tensor(name).data[:] for name in {\"features\", \"heatmaps\", \"pafs\"}}\n", + " # Get the results\n", + " results = (results[\"features\"][0], results[\"heatmaps\"][0], results[\"pafs\"][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1],\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15],\n", + " [15, 16], # nose - l_eye - l_ear\n", + " [1, 17],\n", + " [17, 18], # nose - r_eye - r_ear\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16],\n", + " [16, 18], # nose - l_eye - l_ear\n", + " [1, 15],\n", + " [15, 17], # nose - r_eye - r_ear\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(source, flip=flip, fps=30, skip_first_frames=skip_frames)\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(grid=True, axis=True, view_width=windows_width, view_height=windows_height)\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(format=\"jpg\", height=windows_height, width=windows_width)\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2]))\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = parse_poses(inference_result, 1, stride, focal_length, True)\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://github.com/intel-iot-devkit/sample-videos/raw/master/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + , [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using a Jupyter Notebook release. Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino-dev>=2024.0.0\" \"opencv-python\" \"torch\" \"onnx\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "with open(\"notebook_utils.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/engine3js.py\",\n", + ")\n", + "with open(\"engine3js.py\", \"w\") as f:\n", + " f.write(r.text)\n", + "\n", + "import notebook_utils as utils\n", + "import engine3js as engine" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We use `omz_downloader`, which is a command line tool from the `openvino-dev` package. `omz_downloader` automatically creates a directory structure and downloads the selected model." + ] + }, + { + "cell_type": "code", + "source": [ + "# directory where model will be downloaded\n", + "base_model_dir = \"model\"\n", + "\n", + "# model name as named in Open Model Zoo\n", + "model_name = \"human-pose-estimation-3d-0001\"\n", + "# selected precision (FP32, FP16)\n", + "precision = \"FP32\"\n", + "\n", + "BASE_MODEL_NAME = f\"{base_model_dir}/public/{model_name}/{model_name}\"\n", + "model_path = Path(BASE_MODEL_NAME).with_suffix(\".pth\")\n", + "onnx_path = Path(BASE_MODEL_NAME).with_suffix(\".onnx\")\n", + "\n", + "ir_model_path = f\"model/public/{model_name}/{precision}/{model_name}.xml\"\n", + "model_weights_path = f\"model/public/{model_name}/{precision}/{model_name}.bin\"\n", + "\n", + "if not model_path.exists():\n", + " download_command = f\"omz_downloader \" f\"--name {model_name} \" f\"--output_dir {base_model_dir}\"\n", + " ! $download_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The selected model comes from the public directory, which means it must be converted into OpenVINO Intermediate Representation (OpenVINO IR). We use `omz_converter` to convert the ONNX format model to the OpenVINO IR format." + ] + }, + { + "cell_type": "code", + "source": [ + "if not onnx_path.exists():\n", + " convert_command = (\n", + " f\"omz_converter \" f\"--name {model_name} \" f\"--precisions {precision} \" f\"--download_dir {base_model_dir} \" f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $convert_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()\n", + "\n", + "device = widgets.Dropdown(\n", + " options=core.available_devices + [\"AUTO\"],\n", + " value=\"AUTO\",\n", + " description=\"Device:\",\n", + " disabled=False,\n", + ")\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(model=ir_model_path, weights=model_weights_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)\n", + "infer_request = compiled_model.create_infer_request()\n", + "input_tensor_name = model.inputs[0].get_any_name()\n", + "\n", + "# get input and output names of nodes\n", + "input_layer = compiled_model.input(0)\n", + "output_layers = list(compiled_model.outputs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The input for the model is data from the input image and the outputs are heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "input_layer.any_name, [o.any_name for o in output_layers]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[None,]\n", + " infer_request.infer({input_tensor_name: img})\n", + " # A set of three inference results is obtained\n", + " results = {name: infer_request.get_tensor(name).data[:] for name in {\"features\", \"heatmaps\", \"pafs\"}}\n", + " # Get the results\n", + " results = (results[\"features\"][0], results[\"heatmaps\"][0], results[\"pafs\"][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1],\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15],\n", + " [15, 16], # nose - l_eye - l_ear\n", + " [1, 17],\n", + " [17, 18], # nose - r_eye - r_ear\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16],\n", + " [16, 18], # nose - l_eye - l_ear\n", + " [1, 15],\n", + " [15, 17], # nose - r_eye - r_ear\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(source, flip=flip, fps=30, skip_first_frames=skip_frames)\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(grid=True, axis=True, view_width=windows_width, view_height=windows_height)\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(format=\"jpg\", height=windows_height, width=windows_width)\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2]))\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = engine.parse_poses(inference_result, 1, stride, focal_length, True)\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://github.com/intel-iot-devkit/sample-videos/raw/master/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 1, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 4, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 20, originalLength: 1, modifiedStart: 20, modifiedLength: 1 } satisfies IDiffChange, + ]); + + }); + test('Detect modification in multiple cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using the latest Jupyter Notebook release (2.4.1). Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino-dev>=2024.0.0\" \"opencv-python\" \"torch\" \"onnx\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import sys\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "r = requests.get(\n", + " url='https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py',\n", + ")\n", + "open('notebook_utils.py', 'w').write(r.text)\n", + "import notebook_utils as utils\n", + "\n", + "sys.path.append(\"./engine\")\n", + "import engine.engine3js as engine\n", + "from engine.parse_poses import parse_poses" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We use `omz_downloader`, which is a command line tool from the `openvino-dev` package. `omz_downloader` automatically creates a directory structure and downloads the selected model." + ] + }, + { + "cell_type": "code", + "source": [ + "# directory where model will be downloaded\n", + "base_model_dir = \"model\"\n", + "\n", + "# model name as named in Open Model Zoo\n", + "model_name = \"human-pose-estimation-3d-0001\"\n", + "# selected precision (FP32, FP16)\n", + "precision = \"FP32\"\n", + "\n", + "BASE_MODEL_NAME = f\"{base_model_dir}/public/{model_name}/{model_name}\"\n", + "model_path = Path(BASE_MODEL_NAME).with_suffix(\".pth\")\n", + "onnx_path = Path(BASE_MODEL_NAME).with_suffix(\".onnx\")\n", + "\n", + "ir_model_path = f\"model/public/{model_name}/{precision}/{model_name}.xml\"\n", + "model_weights_path = f\"model/public/{model_name}/{precision}/{model_name}.bin\"\n", + "\n", + "if not model_path.exists():\n", + " download_command = (\n", + " f\"omz_downloader \" f\"--name {model_name} \" f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $download_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The selected model comes from the public directory, which means it must be converted into OpenVINO Intermediate Representation (OpenVINO IR). We use `omz_converter` to convert the ONNX format model to the OpenVINO IR format." + ] + }, + { + "cell_type": "code", + "source": [ + "if not onnx_path.exists():\n", + " convert_command = (\n", + " f\"omz_converter \"\n", + " f\"--name {model_name} \"\n", + " f\"--precisions {precision} \"\n", + " f\"--download_dir {base_model_dir} \"\n", + " f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $convert_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()\n", + "\n", + "device = widgets.Dropdown(\n", + " options=core.available_devices + [\"AUTO\"],\n", + " value='AUTO',\n", + " description='Device:',\n", + " disabled=False,\n", + ")\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(model=ir_model_path, weights=model_weights_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)\n", + "infer_request = compiled_model.create_infer_request()\n", + "input_tensor_name = model.inputs[0].get_any_name()\n", + "\n", + "# get input and output names of nodes\n", + "input_layer = compiled_model.input(0)\n", + "output_layers = list(compiled_model.outputs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The input for the model is data from the input image and the outputs are heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "input_layer.any_name, [o.any_name for o in output_layers]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[\n", + " None,\n", + " ]\n", + " infer_request.infer({input_tensor_name: img})\n", + " # A set of three inference results is obtained\n", + " results = {\n", + " name: infer_request.get_tensor(name).data[:]\n", + " for name in {\"features\", \"heatmaps\", \"pafs\"}\n", + " }\n", + " # Get the results\n", + " results = (results[\"features\"][0], results[\"heatmaps\"][0], results[\"pafs\"][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1], \n", + " [0, 9], [9, 10], [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3], [3, 4], [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15], [15, 16], # nose - l_eye - l_ear\n", + " [1, 17], [17, 18], # nose - r_eye - r_ear\n", + " [0, 6], [6, 7], [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12], [12, 13], [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16], [16, 18], # nose - l_eye - l_ear\n", + " [1, 15], [15, 17], # nose - r_eye - r_ear\n", + " [0, 3], [3, 4], [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9], [9, 10], [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6], [6, 7], [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12], [12, 13], [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ") \n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(source, flip=flip, fps=30, skip_first_frames=skip_frames)\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(grid=True, axis=True, view_width=windows_width, view_height=windows_height)\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(\n", + " format=\"jpg\", height=windows_height, width=windows_width\n", + " )\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2]))\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = parse_poses(inference_result, 1, stride, focal_length, True)\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://github.com/intel-iot-devkit/sample-videos/raw/master/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + , [ + { + "cell_type": "markdown", + "source": [ + "# Live 3D Human Pose Estimation with OpenVINO\n", + "\n", + "This notebook demonstrates live 3D Human Pose Estimation with OpenVINO via a webcam. We utilize the model [human-pose-estimation-3d-0001](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001) from [Open Model Zoo](https://github.com/openvinotoolkit/open_model_zoo/). At the end of this notebook, you will see live inference results from your webcam (if available). Alternatively, you can also upload a video file to test out the algorithms.\n", + "**Make sure you have properly installed the [Jupyter extension](https://github.com/jupyter-widgets/pythreejs#jupyterlab) and been using JupyterLab to run the demo as suggested in the `README.md`**\n", + "\n", + "> **NOTE**: _To use a webcam, you must run this Jupyter notebook on a computer with a webcam. If you run on a remote server, the webcam will not work. However, you can still do inference on a video file in the final step. This demo utilizes the Python interface in `Three.js` integrated with WebGL to process data from the model inference. These results are processed and displayed in the notebook._\n", + "\n", + "_To ensure that the results are displayed correctly, run the code in a recommended browser on one of the following operating systems:_\n", + "_Ubuntu, Windows: Chrome_\n", + "_macOS: Safari_\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Imports](#Imports)\n", + "- [The model](#The-model)\n", + " - [Download the model](#Download-the-model)\n", + " - [Convert Model to OpenVINO IR format](#Convert-Model-to-OpenVINO-IR-format)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Load the model](#Load-the-model)\n", + "- [Processing](#Processing)\n", + " - [Model Inference](#Model-Inference)\n", + " - [Draw 2D Pose Overlays](#Draw-2D-Pose-Overlays)\n", + " - [Main Processing Function](#Main-Processing-Function)\n", + "- [Run](#Run)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "**The `pythreejs` extension may not display properly when using the latest Jupyter Notebook release (2.4.1). Therefore, it is recommended to use Jupyter Lab instead.**" + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install pythreejs \"openvino-dev>=2024.0.0\" \"opencv-python\" \"torch\" \"onnx\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Imports\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "import collections\n", + "import sys\n", + "import time\n", + "from pathlib import Path\n", + "\n", + "import cv2\n", + "import ipywidgets as widgets\n", + "import numpy as np\n", + "from IPython.display import clear_output, display\n", + "import openvino as ov\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "import notebook_utils as utils\n", + "\n", + "sys.path.append(\"./engine\")\n", + "import engine.engine3js as engine\n", + "from engine.parse_poses import parse_poses" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## The model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Download the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We use `omz_downloader`, which is a command line tool from the `openvino-dev` package. `omz_downloader` automatically creates a directory structure and downloads the selected model." + ] + }, + { + "cell_type": "code", + "source": [ + "# directory where model will be downloaded\n", + "base_model_dir = \"model\"\n", + "\n", + "# model name as named in Open Model Zoo\n", + "model_name = \"human-pose-estimation-3d-0001\"\n", + "# selected precision (FP32, FP16)\n", + "precision = \"FP32\"\n", + "\n", + "BASE_MODEL_NAME = f\"{base_model_dir}/public/{model_name}/{model_name}\"\n", + "model_path = Path(BASE_MODEL_NAME).with_suffix(\".pth\")\n", + "onnx_path = Path(BASE_MODEL_NAME).with_suffix(\".onnx\")\n", + "\n", + "ir_model_path = f\"model/public/{model_name}/{precision}/{model_name}.xml\"\n", + "model_weights_path = f\"model/public/{model_name}/{precision}/{model_name}.bin\"\n", + "\n", + "if not model_path.exists():\n", + " download_command = (\n", + " f\"omz_downloader \" f\"--name {model_name} \" f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $download_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Convert Model to OpenVINO IR format\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The selected model comes from the public directory, which means it must be converted into OpenVINO Intermediate Representation (OpenVINO IR). We use `omz_converter` to convert the ONNX format model to the OpenVINO IR format." + ] + }, + { + "cell_type": "code", + "source": [ + "if not onnx_path.exists():\n", + " convert_command = (\n", + " f\"omz_converter \"\n", + " f\"--name {model_name} \"\n", + " f\"--precisions {precision} \"\n", + " f\"--download_dir {base_model_dir} \"\n", + " f\"--output_dir {base_model_dir}\"\n", + " )\n", + " ! $convert_command" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()\n", + "\n", + "device = widgets.Dropdown(\n", + " options=core.available_devices + [\"AUTO\"],\n", + " value=\"AUTO\",\n", + " description=\"Device:\",\n", + " disabled=False,\n", + ")\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Converted models are located in a fixed structure, which indicates vendor, model name and precision.\n", + "\n", + "First, initialize the inference engine, OpenVINO Runtime. Then, read the network architecture and model weights from the `.bin` and `.xml` files to compile for the desired device. An inference request is then created to infer the compiled model." + ] + }, + { + "cell_type": "code", + "source": [ + "# initialize inference engine\n", + "core = ov.Core()\n", + "# read the network and corresponding weights from file\n", + "model = core.read_model(model=ir_model_path, weights=model_weights_path)\n", + "# load the model on the specified device\n", + "compiled_model = core.compile_model(model=model, device_name=device.value)\n", + "infer_request = compiled_model.create_infer_request()\n", + "input_tensor_name = model.inputs[0].get_any_name()\n", + "\n", + "# get input and output names of nodes\n", + "input_layer = compiled_model.input(0)\n", + "output_layers = list(compiled_model.outputs)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The input for the model is data from the input image and the outputs are heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "input_layer.any_name, [o.any_name for o in output_layers]" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Processing\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "### Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Frames captured from video files or the live webcam are used as the input for the 3D model. This is how you obtain the output heat maps, PAF (part affinity fields) and features." + ] + }, + { + "cell_type": "code", + "source": [ + "def model_infer(scaled_img, stride):\n", + " \"\"\"\n", + " Run model inference on the input image\n", + "\n", + " Parameters:\n", + " scaled_img: resized image according to the input size of the model\n", + " stride: int, the stride of the window\n", + " \"\"\"\n", + "\n", + " # Remove excess space from the picture\n", + " img = scaled_img[\n", + " 0 : scaled_img.shape[0] - (scaled_img.shape[0] % stride),\n", + " 0 : scaled_img.shape[1] - (scaled_img.shape[1] % stride),\n", + " ]\n", + "\n", + " img = np.transpose(img, (2, 0, 1))[None,]\n", + " infer_request.infer({input_tensor_name: img})\n", + " # A set of three inference results is obtained\n", + " results = {\n", + " name: infer_request.get_tensor(name).data[:]\n", + " for name in {\"features\", \"heatmaps\", \"pafs\"}\n", + " }\n", + " # Get the results\n", + " results = (results[\"features\"][0], results[\"heatmaps\"][0], results[\"pafs\"][0])\n", + "\n", + " return results" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Draw 2D Pose Overlays\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We need to define some connections between the joints in advance, so that we can draw the structure of the human body in the resulting image after obtaining the inference results.\n", + "Joints are drawn as circles and limbs are drawn as lines. The code is based on the [3D Human Pose Estimation Demo](https://github.com/openvinotoolkit/open_model_zoo/tree/master/demos/human_pose_estimation_3d_demo/python) from Open Model Zoo." + ] + }, + { + "cell_type": "code", + "source": [ + "# 3D edge index array\n", + "body_edges = np.array(\n", + " [\n", + " [0, 1],\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [1, 15],\n", + " [15, 16], # nose - l_eye - l_ear\n", + " [1, 17],\n", + " [17, 18], # nose - r_eye - r_ear\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "body_edges_2d = np.array(\n", + " [\n", + " [0, 1], # neck - nose\n", + " [1, 16],\n", + " [16, 18], # nose - l_eye - l_ear\n", + " [1, 15],\n", + " [15, 17], # nose - r_eye - r_ear\n", + " [0, 3],\n", + " [3, 4],\n", + " [4, 5], # neck - l_shoulder - l_elbow - l_wrist\n", + " [0, 9],\n", + " [9, 10],\n", + " [10, 11], # neck - r_shoulder - r_elbow - r_wrist\n", + " [0, 6],\n", + " [6, 7],\n", + " [7, 8], # neck - l_hip - l_knee - l_ankle\n", + " [0, 12],\n", + " [12, 13],\n", + " [13, 14], # neck - r_hip - r_knee - r_ankle\n", + " ]\n", + ")\n", + "\n", + "\n", + "def draw_poses(frame, poses_2d, scaled_img, use_popup):\n", + " \"\"\"\n", + " Draw 2D pose overlays on the image to visualize estimated poses.\n", + " Joints are drawn as circles and limbs are drawn as lines.\n", + "\n", + " :param frame: the input image\n", + " :param poses_2d: array of human joint pairs\n", + " \"\"\"\n", + " for pose in poses_2d:\n", + " pose = np.array(pose[0:-1]).reshape((-1, 3)).transpose()\n", + " was_found = pose[2] > 0\n", + "\n", + " pose[0], pose[1] = (\n", + " pose[0] * frame.shape[1] / scaled_img.shape[1],\n", + " pose[1] * frame.shape[0] / scaled_img.shape[0],\n", + " )\n", + "\n", + " # Draw joints.\n", + " for edge in body_edges_2d:\n", + " if was_found[edge[0]] and was_found[edge[1]]:\n", + " cv2.line(\n", + " frame,\n", + " tuple(pose[0:2, edge[0]].astype(np.int32)),\n", + " tuple(pose[0:2, edge[1]].astype(np.int32)),\n", + " (255, 255, 0),\n", + " 4,\n", + " cv2.LINE_AA,\n", + " )\n", + " # Draw limbs.\n", + " for kpt_id in range(pose.shape[1]):\n", + " if pose[2, kpt_id] != -1:\n", + " cv2.circle(\n", + " frame,\n", + " tuple(pose[0:2, kpt_id].astype(np.int32)),\n", + " 3,\n", + " (0, 255, 255),\n", + " -1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " return frame" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Main Processing Function\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run 3D pose estimation on the specified source. It could be either a webcam feed or a video file." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "def run_pose_estimation(source=0, flip=False, use_popup=False, skip_frames=0):\n", + " \"\"\"\n", + " 2D image as input, using OpenVINO as inference backend,\n", + " get joints 3D coordinates, and draw 3D human skeleton in the scene\n", + "\n", + " :param source: The webcam number to feed the video stream with primary webcam set to \"0\", or the video path.\n", + " :param flip: To be used by VideoPlayer function for flipping capture image.\n", + " :param use_popup: False for showing encoded frames over this notebook, True for creating a popup window.\n", + " :param skip_frames: Number of frames to skip at the beginning of the video.\n", + " \"\"\"\n", + "\n", + " focal_length = -1 # default\n", + " stride = 8\n", + " player = None\n", + " skeleton_set = None\n", + "\n", + " try:\n", + " # create video player to play with target fps video_path\n", + " # get the frame from camera\n", + " # You can skip first N frames to fast forward video. change 'skip_first_frames'\n", + " player = utils.VideoPlayer(\n", + " source, flip=flip, fps=30, skip_first_frames=skip_frames\n", + " )\n", + " # start capturing\n", + " player.start()\n", + "\n", + " input_image = player.next()\n", + " # set the window size\n", + " resize_scale = 450 / input_image.shape[1]\n", + " windows_width = int(input_image.shape[1] * resize_scale)\n", + " windows_height = int(input_image.shape[0] * resize_scale)\n", + "\n", + " # use visualization library\n", + " engine3D = engine.Engine3js(\n", + " grid=True, axis=True, view_width=windows_width, view_height=windows_height\n", + " )\n", + "\n", + " if use_popup:\n", + " # display the 3D human pose in this notebook, and origin frame in popup window\n", + " display(engine3D.renderer)\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(title, cv2.WINDOW_KEEPRATIO | cv2.WINDOW_AUTOSIZE)\n", + " else:\n", + " # set the 2D image box, show both human pose and image in the notebook\n", + " imgbox = widgets.Image(\n", + " format=\"jpg\", height=windows_height, width=windows_width\n", + " )\n", + " display(widgets.HBox([engine3D.renderer, imgbox]))\n", + "\n", + " skeleton = engine.Skeleton(body_edges=body_edges)\n", + "\n", + " processing_times = collections.deque()\n", + "\n", + " while True:\n", + " # grab the frame\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + "\n", + " # resize image and change dims to fit neural network input\n", + " # (see https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/human-pose-estimation-3d-0001)\n", + " scaled_img = cv2.resize(\n", + " frame, dsize=(model.inputs[0].shape[3], model.inputs[0].shape[2])\n", + " )\n", + "\n", + " if focal_length < 0: # Focal length is unknown\n", + " focal_length = np.float32(0.8 * scaled_img.shape[1])\n", + "\n", + " # inference start\n", + " start_time = time.time()\n", + " # get results\n", + " inference_result = model_infer(scaled_img, stride)\n", + "\n", + " # inference stop\n", + " stop_time = time.time()\n", + " processing_times.append(stop_time - start_time)\n", + " # Process the point to point coordinates of the data\n", + " poses_3d, poses_2d = parse_poses(\n", + " inference_result, 1, stride, focal_length, True\n", + " )\n", + "\n", + " # use processing times from last 200 frames\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + "\n", + " if len(poses_3d) > 0:\n", + " # From here, you can rotate the 3D point positions using the function \"draw_poses\",\n", + " # or you can directly make the correct mapping below to properly display the object image on the screen\n", + " poses_3d_copy = poses_3d.copy()\n", + " x = poses_3d_copy[:, 0::4]\n", + " y = poses_3d_copy[:, 1::4]\n", + " z = poses_3d_copy[:, 2::4]\n", + " poses_3d[:, 0::4], poses_3d[:, 1::4], poses_3d[:, 2::4] = (\n", + " -z + np.ones(poses_3d[:, 2::4].shape) * 200,\n", + " -y + np.ones(poses_3d[:, 2::4].shape) * 100,\n", + " -x,\n", + " )\n", + "\n", + " poses_3d = poses_3d.reshape(poses_3d.shape[0], 19, -1)[:, :, 0:3]\n", + " people = skeleton(poses_3d=poses_3d)\n", + "\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " except Exception:\n", + " pass\n", + "\n", + " engine3D.scene_add(people)\n", + " skeleton_set = people\n", + "\n", + " # draw 2D\n", + " frame = draw_poses(frame, poses_2d, scaled_img, use_popup)\n", + "\n", + " else:\n", + " try:\n", + " engine3D.scene_remove(skeleton_set)\n", + " skeleton_set = None\n", + " except Exception:\n", + " pass\n", + "\n", + " cv2.putText(\n", + " frame,\n", + " f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " (10, 30),\n", + " cv2.FONT_HERSHEY_COMPLEX,\n", + " 0.7,\n", + " (0, 0, 255),\n", + " 1,\n", + " cv2.LINE_AA,\n", + " )\n", + "\n", + " if use_popup:\n", + " cv2.imshow(title, frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27, use ESC to exit\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # encode numpy array to jpg\n", + " imgbox.value = cv2.imencode(\n", + " \".jpg\",\n", + " frame,\n", + " params=[cv2.IMWRITE_JPEG_QUALITY, 90],\n", + " )[1].tobytes()\n", + "\n", + " engine3D.renderer.render(engine3D.scene, engine3D.cam)\n", + "\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " clear_output()\n", + " if player is not None:\n", + " # stop capturing\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()\n", + " if skeleton_set:\n", + " engine3D.scene_remove(skeleton_set)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Run\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Run, using a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + "> **NOTE**:\n", + ">\n", + "> *1. To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a server (e.g. Binder), the webcam will not work.*\n", + ">\n", + "> *2. Popup mode may not work if you run this notebook on a remote computer (e.g. Binder).*\n", + "\n", + "If you do not have a webcam, you can still run this demo with a video file. Any [format supported by OpenCV](https://docs.opencv.org/4.5.1/dd/d43/tutorial_py_video_display.html) will work." + ] + }, + { + "cell_type": "markdown", + "source": [ + "Using the following method, you can click and move your mouse over the picture on the left to interact." + ] + }, + { + "cell_type": "code", + "metadata": { + "tags": [] + }, + "source": [ + "USE_WEBCAM = False\n", + "\n", + "cam_id = 0\n", + "video_path = \"https://github.com/intel-iot-devkit/sample-videos/raw/master/face-demographics-walking.mp4\"\n", + "\n", + "source = cam_id if USE_WEBCAM else video_path\n", + "\n", + "run_pose_estimation(source=source, flip=isinstance(source, int), use_popup=False)" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 4, originalLength: 1, modifiedStart: 4, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 10, originalLength: 1, modifiedStart: 10, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 16, originalLength: 1, modifiedStart: 16, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 18, originalLength: 1, modifiedStart: 18, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 20, originalLength: 1, modifiedStart: 20, modifiedLength: 1 } satisfies IDiffChange, + ]); + + }); + test('Modification with Insertion, detected as deletion of a cell', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "source": [ + "# Video generation with ZeroScope and OpenVINO\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Install and import required packages](#Install-and-import-required-packages)\n", + "- [Load the model](#Load-the-model)\n", + "- [Convert the model](#Convert-the-model)\n", + " - [Define the conversion function](#Define-the-conversion-function)\n", + " - [UNet](#UNet)\n", + " - [VAE](#VAE)\n", + " - [Text encoder](#Text-encoder)\n", + "- [Build a pipeline](#Build-a-pipeline)\n", + "- [Inference with OpenVINO](#Inference-with-OpenVINO)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Define a prompt](#Define-a-prompt)\n", + " - [Video generation](#Video-generation)\n", + "- [Interactive demo](#Interactive-demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The ZeroScope model is a free and open-source text-to-video model that can generate realistic and engaging videos from text descriptions. It is based on the [Modelscope](https://modelscope.cn/models/damo/text-to-video-synthesis/summary) model, but it has been improved to produce higher-quality videos with a 16:9 aspect ratio and no Shutterstock watermark. The ZeroScope model is available in two versions: ZeroScope_v2 576w, which is optimized for rapid content creation at a resolution of 576x320 pixels, and ZeroScope_v2 XL, which upscales videos to a high-definition resolution of 1024x576.\n", + "\n", + "The ZeroScope model is trained on a dataset of over 9,000 videos and 29,000 tagged frames. It uses a diffusion model to generate videos, which means that it starts with a random noise image and gradually adds detail to it until it matches the text description. The ZeroScope model is still under development, but it has already been used to create some impressive videos. For example, it has been used to create videos of people dancing, playing sports, and even driving cars.\n", + "\n", + "The ZeroScope model is a powerful tool that can be used to create various videos, from simple animations to complex scenes. It is still under development, but it has the potential to revolutionize the way we create and consume video content.\n", + "\n", + "Both versions of the ZeroScope model are available on Hugging Face:\n", + " - [ZeroScope_v2 576w](https://huggingface.co/cerspense/zeroscope_v2_576w)\n", + " - [ZeroScope_v2 XL](https://huggingface.co/cerspense/zeroscope_v2_XL)\n", + "\n", + "We will use the first one." + ] + }, + { + "cell_type": "markdown", + "source": [ + "
\n", + " This tutorial requires at least 24GB of free memory to generate a video with a frame size of 432x240 and 16 frames. Increasing either of these values will require more memory and take more time.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Install and import required packages\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "To work with text-to-video synthesis model, we will use Hugging Face's [Diffusers](https://github.com/huggingface/diffusers) library. It provides already pretrained model from `cerspense`." + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install -q --extra-index-url https://download.pytorch.org/whl/cpu \"diffusers>=0.18.0\" \"torch>=2.1\" transformers \"openvino>=2023.1.0\" numpy \"gradio>=4.19\"" + ] + }, + { + "cell_type": "code", + "source": [ + "import gc\n", + "from typing import Optional, Union, List, Callable\n", + "import base64\n", + "import tempfile\n", + "import warnings\n", + "\n", + "import diffusers\n", + "import transformers\n", + "import numpy as np\n", + "import IPython\n", + "import torch\n", + "import PIL\n", + "import gradio as gr\n", + "\n", + "import openvino as ov" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Original 576x320 inference requires a lot of RAM (>100GB), so let's run our example on a smaller frame size, keeping the same aspect ratio. Try reducing values below to reduce the memory consumption." + ] + }, + { + "cell_type": "code", + "source": [ + "WIDTH = 432 # must be divisible by 8\n", + "HEIGHT = 240 # must be divisible by 8\n", + "NUM_FRAMES = 16" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The model is loaded from HuggingFace using `.from_pretrained` method of `diffusers.DiffusionPipeline`." + ] + }, + { + "cell_type": "code", + "source": [ + "pipe = diffusers.DiffusionPipeline.from_pretrained(\"cerspense/zeroscope_v2_576w\")" + ] + }, + { + "cell_type": "code", + "source": [ + "unet = pipe.unet\n", + "unet.eval()\n", + "vae = pipe.vae\n", + "vae.eval()\n", + "text_encoder = pipe.text_encoder\n", + "text_encoder.eval()\n", + "tokenizer = pipe.tokenizer\n", + "scheduler = pipe.scheduler\n", + "vae_scale_factor = pipe.vae_scale_factor\n", + "unet_in_channels = pipe.unet.config.in_channels\n", + "sample_width = WIDTH // vae_scale_factor\n", + "sample_height = HEIGHT // vae_scale_factor\n", + "del pipe\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Convert the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The architecture for generating videos from text comprises three distinct sub-networks: one for extracting text features, another for translating text features into the video latent space using a diffusion model, and a final one for mapping the video latent space to the visual space. The collective parameters of the entire model amount to approximately 1.7 billion. It's capable of processing English input. The diffusion model is built upon the Unet3D model and achieves video generation by iteratively denoising a starting point of pure Gaussian noise video." + ] + }, + { + "cell_type": "markdown", + "source": [] + }, + { + "cell_type": "markdown", + "source": [ + "### Define the conversion function\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Model components are PyTorch modules, that can be converted with `ov.convert_model` function directly. We also use `ov.save_model` function to serialize the result of conversion." + ] + }, + { + "cell_type": "code", + "source": [ + "warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)" + ] + }, + { + "cell_type": "code", + "source": [ + "from pathlib import Path\n", + "\n", + "\n", + "def convert(model: torch.nn.Module, xml_path: str, **convert_kwargs) -> Path:\n", + " xml_path = Path(xml_path)\n", + " if not xml_path.exists():\n", + " xml_path.parent.mkdir(parents=True, exist_ok=True)\n", + " with torch.no_grad():\n", + " converted_model = ov.convert_model(model, **convert_kwargs)\n", + " ov.save_model(converted_model, xml_path)\n", + " del converted_model\n", + " gc.collect()\n", + " torch._C._jit_clear_class_registry()\n", + " torch.jit._recursive.concrete_type_store = torch.jit._recursive.ConcreteTypeStore()\n", + " torch.jit._state._clear_class_state()\n", + " return xml_path" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### UNet\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text-to-video generation pipeline main component is a conditional 3D UNet model that takes a noisy sample, conditional state, and a timestep and returns a sample shaped output." + ] + }, + { + "cell_type": "code", + "source": [ + "unet_xml_path = convert(\n", + " unet,\n", + " \"models/unet.xml\",\n", + " example_input={\n", + " \"sample\": torch.randn(2, 4, 2, int(sample_height // 2), int(sample_width // 2)),\n", + " \"timestep\": torch.tensor(1),\n", + " \"encoder_hidden_states\": torch.randn(2, 77, 1024),\n", + " },\n", + " input=[\n", + " (\"sample\", (2, 4, NUM_FRAMES, sample_height, sample_width)),\n", + " (\"timestep\", ()),\n", + " (\"encoder_hidden_states\", (2, 77, 1024)),\n", + " ],\n", + ")\n", + "del unet\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### VAE\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Variational autoencoder (VAE) uses UNet output to decode latents to visual representations. Our VAE model has KL loss for encoding images into latents and decoding latent representations into images. For inference, we need only decoder part." + ] + }, + { + "cell_type": "code", + "source": [ + "class VaeDecoderWrapper(torch.nn.Module):\n", + " def __init__(self, vae):\n", + " super().__init__()\n", + " self.vae = vae\n", + "\n", + " def forward(self, z: torch.FloatTensor):\n", + " return self.vae.decode(z)" + ] + }, + { + "cell_type": "code", + "source": [ + "vae_decoder_xml_path = convert(\n", + " VaeDecoderWrapper(vae),\n", + " \"models/vae.xml\",\n", + " example_input=torch.randn(2, 4, 32, 32),\n", + " input=((NUM_FRAMES, 4, sample_height, sample_width)),\n", + ")\n", + "del vae\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Text encoder\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text encoder is used to encode the input prompt to tensor. Default tensor length is 77." + ] + }, + { + "cell_type": "code", + "source": [ + "text_encoder_xml = convert(\n", + " text_encoder,\n", + " \"models/text_encoder.xml\",\n", + " example_input=torch.ones(1, 77, dtype=torch.int64),\n", + " input=((1, 77), ov.Type.i64),\n", + ")\n", + "del text_encoder\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Build a pipeline\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def tensor2vid(video: torch.Tensor, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) -> List[np.ndarray]:\n", + " # This code is copied from https://github.com/modelscope/modelscope/blob/1509fdb973e5871f37148a4b5e5964cafd43e64d/modelscope/pipelines/multi_modal/text_to_video_synthesis_pipeline.py#L78\n", + " # reshape to ncfhw\n", + " mean = torch.tensor(mean, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " std = torch.tensor(std, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " # unnormalize back to [0,1]\n", + " video = video.mul_(std).add_(mean)\n", + " video.clamp_(0, 1)\n", + " # prepare the final outputs\n", + " i, c, f, h, w = video.shape\n", + " images = video.permute(2, 3, 0, 4, 1).reshape(f, h, i * w, c) # 1st (frames, h, batch_size, w, c) 2nd (frames, h, batch_size * w, c)\n", + " images = images.unbind(dim=0) # prepare a list of indvidual (consecutive frames)\n", + " images = [(image.cpu().numpy() * 255).astype(\"uint8\") for image in images] # f h w c\n", + " return images" + ] + }, + { + "cell_type": "code", + "source": [ + "try:\n", + " from diffusers.utils import randn_tensor\n", + "except ImportError:\n", + " from diffusers.utils.torch_utils import randn_tensor\n", + "\n", + "\n", + "class OVTextToVideoSDPipeline(diffusers.DiffusionPipeline):\n", + " def __init__(\n", + " self,\n", + " vae_decoder: ov.CompiledModel,\n", + " text_encoder: ov.CompiledModel,\n", + " tokenizer: transformers.CLIPTokenizer,\n", + " unet: ov.CompiledModel,\n", + " scheduler: diffusers.schedulers.DDIMScheduler,\n", + " ):\n", + " super().__init__()\n", + "\n", + " self.vae_decoder = vae_decoder\n", + " self.text_encoder = text_encoder\n", + " self.tokenizer = tokenizer\n", + " self.unet = unet\n", + " self.scheduler = scheduler\n", + " self.vae_scale_factor = vae_scale_factor\n", + " self.unet_in_channels = unet_in_channels\n", + " self.width = WIDTH\n", + " self.height = HEIGHT\n", + " self.num_frames = NUM_FRAMES\n", + "\n", + " def __call__(\n", + " self,\n", + " prompt: Union[str, List[str]] = None,\n", + " num_inference_steps: int = 50,\n", + " guidance_scale: float = 9.0,\n", + " negative_prompt: Optional[Union[str, List[str]]] = None,\n", + " eta: float = 0.0,\n", + " generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,\n", + " latents: Optional[torch.FloatTensor] = None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " output_type: Optional[str] = \"np\",\n", + " return_dict: bool = True,\n", + " callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,\n", + " callback_steps: int = 1,\n", + " ):\n", + " r\"\"\"\n", + " Function invoked when calling the pipeline for generation.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts to guide the video generation. If not defined, one has to pass `prompt_embeds`.\n", + " instead.\n", + " num_inference_steps (`int`, *optional*, defaults to 50):\n", + " The number of denoising steps. More denoising steps usually lead to a higher quality videos at the\n", + " expense of slower inference.\n", + " guidance_scale (`float`, *optional*, defaults to 7.5):\n", + " Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).\n", + " `guidance_scale` is defined as `w` of equation 2. of [Imagen\n", + " Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >\n", + " 1`. Higher guidance scale encourages to generate videos that are closely linked to the text `prompt`,\n", + " usually at the expense of lower video quality.\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the video generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " eta (`float`, *optional*, defaults to 0.0):\n", + " Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to\n", + " [`schedulers.DDIMScheduler`], will be ignored for others.\n", + " generator (`torch.Generator` or `List[torch.Generator]`, *optional*):\n", + " One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)\n", + " to make generation deterministic.\n", + " latents (`torch.FloatTensor`, *optional*):\n", + " Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for video\n", + " generation. Can be used to tweak the same generation with different prompts. If not provided, a latents\n", + " tensor will ge generated by sampling using the supplied random `generator`. Latents should be of shape\n", + " `(batch_size, num_channel, num_frames, height, width)`.\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " output_type (`str`, *optional*, defaults to `\"np\"`):\n", + " The output format of the generate video. Choose between `torch.FloatTensor` or `np.array`.\n", + " return_dict (`bool`, *optional*, defaults to `True`):\n", + " Whether or not to return a [`~pipelines.stable_diffusion.TextToVideoSDPipelineOutput`] instead of a\n", + " plain tuple.\n", + " callback (`Callable`, *optional*):\n", + " A function that will be called every `callback_steps` steps during inference. The function will be\n", + " called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`.\n", + " callback_steps (`int`, *optional*, defaults to 1):\n", + " The frequency at which the `callback` function will be called. If not specified, the callback will be\n", + " called at every step.\n", + "\n", + " Returns:\n", + " `List[np.ndarray]`: generated video frames\n", + " \"\"\"\n", + "\n", + " num_images_per_prompt = 1\n", + "\n", + " # 1. Check inputs. Raise error if not correct\n", + " self.check_inputs(\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt,\n", + " prompt_embeds,\n", + " negative_prompt_embeds,\n", + " )\n", + "\n", + " # 2. Define call parameters\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)\n", + " # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`\n", + " # corresponds to doing no classifier free guidance.\n", + " do_classifier_free_guidance = guidance_scale > 1.0\n", + "\n", + " # 3. Encode input prompt\n", + " prompt_embeds = self._encode_prompt(\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt,\n", + " prompt_embeds=prompt_embeds,\n", + " negative_prompt_embeds=negative_prompt_embeds,\n", + " )\n", + "\n", + " # 4. Prepare timesteps\n", + " self.scheduler.set_timesteps(num_inference_steps)\n", + " timesteps = self.scheduler.timesteps\n", + "\n", + " # 5. Prepare latent variables\n", + " num_channels_latents = self.unet_in_channels\n", + " latents = self.prepare_latents(\n", + " batch_size * num_images_per_prompt,\n", + " num_channels_latents,\n", + " prompt_embeds.dtype,\n", + " generator,\n", + " latents,\n", + " )\n", + "\n", + " # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline\n", + " extra_step_kwargs = {\"generator\": generator, \"eta\": eta}\n", + "\n", + " # 7. Denoising loop\n", + " num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order\n", + " with self.progress_bar(total=num_inference_steps) as progress_bar:\n", + " for i, t in enumerate(timesteps):\n", + " # expand the latents if we are doing classifier free guidance\n", + " latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents\n", + " latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)\n", + "\n", + " # predict the noise residual\n", + " noise_pred = self.unet(\n", + " {\n", + " \"sample\": latent_model_input,\n", + " \"timestep\": t,\n", + " \"encoder_hidden_states\": prompt_embeds,\n", + " }\n", + " )[0]\n", + " noise_pred = torch.tensor(noise_pred)\n", + "\n", + " # perform guidance\n", + " if do_classifier_free_guidance:\n", + " noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)\n", + " noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)\n", + "\n", + " # reshape latents\n", + " bsz, channel, frames, width, height = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + " noise_pred = noise_pred.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + "\n", + " # compute the previous noisy sample x_t -> x_t-1\n", + " latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample\n", + "\n", + " # reshape latents back\n", + " latents = latents[None, :].reshape(bsz, frames, channel, width, height).permute(0, 2, 1, 3, 4)\n", + "\n", + " # call the callback, if provided\n", + " if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):\n", + " progress_bar.update()\n", + " if callback is not None and i % callback_steps == 0:\n", + " callback(i, t, latents)\n", + "\n", + " video_tensor = self.decode_latents(latents)\n", + "\n", + " if output_type == \"pt\":\n", + " video = video_tensor\n", + " else:\n", + " video = tensor2vid(video_tensor)\n", + "\n", + " if not return_dict:\n", + " return (video,)\n", + "\n", + " return {\"frames\": video}\n", + "\n", + " # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt\n", + " def _encode_prompt(\n", + " self,\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt=None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " ):\n", + " r\"\"\"\n", + " Encodes the prompt into text encoder hidden states.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " prompt to be encoded\n", + " num_images_per_prompt (`int`):\n", + " number of images that should be generated per prompt\n", + " do_classifier_free_guidance (`bool`):\n", + " whether to use classifier free guidance or not\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the image generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " \"\"\"\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " if prompt_embeds is None:\n", + " text_inputs = self.tokenizer(\n", + " prompt,\n", + " padding=\"max_length\",\n", + " max_length=self.tokenizer.model_max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + " text_input_ids = text_inputs.input_ids\n", + " untruncated_ids = self.tokenizer(prompt, padding=\"longest\", return_tensors=\"pt\").input_ids\n", + "\n", + " if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):\n", + " removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1])\n", + " print(\n", + " \"The following part of your input was truncated because CLIP can only handle sequences up to\"\n", + " f\" {self.tokenizer.model_max_length} tokens: {removed_text}\"\n", + " )\n", + "\n", + " prompt_embeds = self.text_encoder(text_input_ids)\n", + " prompt_embeds = prompt_embeds[0]\n", + " prompt_embeds = torch.tensor(prompt_embeds)\n", + "\n", + " bs_embed, seq_len, _ = prompt_embeds.shape\n", + " # duplicate text embeddings for each generation per prompt, using mps friendly method\n", + " prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # get unconditional embeddings for classifier free guidance\n", + " if do_classifier_free_guidance and negative_prompt_embeds is None:\n", + " uncond_tokens: List[str]\n", + " if negative_prompt is None:\n", + " uncond_tokens = [\"\"] * batch_size\n", + " elif type(prompt) is not type(negative_prompt):\n", + " raise TypeError(f\"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=\" f\" {type(prompt)}.\")\n", + " elif isinstance(negative_prompt, str):\n", + " uncond_tokens = [negative_prompt]\n", + " elif batch_size != len(negative_prompt):\n", + " raise ValueError(\n", + " f\"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:\"\n", + " f\" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches\"\n", + " \" the batch size of `prompt`.\"\n", + " )\n", + " else:\n", + " uncond_tokens = negative_prompt\n", + "\n", + " max_length = prompt_embeds.shape[1]\n", + " uncond_input = self.tokenizer(\n", + " uncond_tokens,\n", + " padding=\"max_length\",\n", + " max_length=max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + "\n", + " negative_prompt_embeds = self.text_encoder(uncond_input.input_ids)\n", + " negative_prompt_embeds = negative_prompt_embeds[0]\n", + " negative_prompt_embeds = torch.tensor(negative_prompt_embeds)\n", + "\n", + " if do_classifier_free_guidance:\n", + " # duplicate unconditional embeddings for each generation per prompt, using mps friendly method\n", + " seq_len = negative_prompt_embeds.shape[1]\n", + "\n", + " negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # For classifier free guidance, we need to do two forward passes.\n", + " # Here we concatenate the unconditional and text embeddings into a single batch\n", + " # to avoid doing two forward passes\n", + " prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])\n", + "\n", + " return prompt_embeds\n", + "\n", + " def prepare_latents(\n", + " self,\n", + " batch_size,\n", + " num_channels_latents,\n", + " dtype,\n", + " generator,\n", + " latents=None,\n", + " ):\n", + " shape = (\n", + " batch_size,\n", + " num_channels_latents,\n", + " self.num_frames,\n", + " self.height // self.vae_scale_factor,\n", + " self.width // self.vae_scale_factor,\n", + " )\n", + " if isinstance(generator, list) and len(generator) != batch_size:\n", + " raise ValueError(\n", + " f\"You have passed a list of generators of length {len(generator)}, but requested an effective batch\"\n", + " f\" size of {batch_size}. Make sure the batch size matches the length of the generators.\"\n", + " )\n", + "\n", + " if latents is None:\n", + " latents = randn_tensor(shape, generator=generator, dtype=dtype)\n", + "\n", + " # scale the initial noise by the standard deviation required by the scheduler\n", + " latents = latents * self.scheduler.init_noise_sigma\n", + " return latents\n", + "\n", + " def check_inputs(\n", + " self,\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt=None,\n", + " prompt_embeds=None,\n", + " negative_prompt_embeds=None,\n", + " ):\n", + " if self.height % 8 != 0 or self.width % 8 != 0:\n", + " raise ValueError(f\"`height` and `width` have to be divisible by 8 but are {self.height} and {self.width}.\")\n", + "\n", + " if (callback_steps is None) or (callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0)):\n", + " raise ValueError(f\"`callback_steps` has to be a positive integer but is {callback_steps} of type\" f\" {type(callback_steps)}.\")\n", + "\n", + " if prompt is not None and prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to\" \" only forward one of the two.\"\n", + " )\n", + " elif prompt is None and prompt_embeds is None:\n", + " raise ValueError(\"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined.\")\n", + " elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):\n", + " raise ValueError(f\"`prompt` has to be of type `str` or `list` but is {type(prompt)}\")\n", + "\n", + " if negative_prompt is not None and negative_prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:\"\n", + " f\" {negative_prompt_embeds}. Please make sure to only forward one of the two.\"\n", + " )\n", + "\n", + " if prompt_embeds is not None and negative_prompt_embeds is not None:\n", + " if prompt_embeds.shape != negative_prompt_embeds.shape:\n", + " raise ValueError(\n", + " \"`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but\"\n", + " f\" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`\"\n", + " f\" {negative_prompt_embeds.shape}.\"\n", + " )\n", + "\n", + " def decode_latents(self, latents):\n", + " scale_factor = 0.18215\n", + " latents = 1 / scale_factor * latents\n", + "\n", + " batch_size, channels, num_frames, height, width = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_frames, channels, height, width)\n", + " image = self.vae_decoder(latents)[0]\n", + " image = torch.tensor(image)\n", + " video = (\n", + " image[None, :]\n", + " .reshape(\n", + " (\n", + " batch_size,\n", + " num_frames,\n", + " -1,\n", + " )\n", + " + image.shape[2:]\n", + " )\n", + " .permute(0, 2, 1, 3, 4)\n", + " )\n", + " # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16\n", + " video = video.float()\n", + " return video" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Inference with OpenVINO\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "from notebook_utils import device_widget\n", + "\n", + "device = device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_unet = core.compile_model(unet_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_vae_decoder = core.compile_model(vae_decoder_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_text_encoder = core.compile_model(text_encoder_xml, device_name=device.value)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Here we replace the pipeline parts with versions converted to OpenVINO IR and compiled to specific device. Note that we use original pipeline tokenizer and scheduler." + ] + }, + { + "cell_type": "code", + "source": [ + "ov_pipe = OVTextToVideoSDPipeline(ov_vae_decoder, ov_text_encoder, tokenizer, ov_unet, scheduler)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Define a prompt\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "prompt = \"A panda eating bamboo on a rock.\"" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Let's generate a video for our prompt. For full list of arguments, see `__call__` function definition of `OVTextToVideoSDPipeline` class in [Build a pipeline](#Build-a-pipeline) section." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Video generation\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "frames = ov_pipe(prompt, num_inference_steps=25)[\"frames\"]" + ] + }, + { + "cell_type": "code", + "source": [ + "images = [PIL.Image.fromarray(frame) for frame in frames]\n", + "images[0].save(\"output.gif\", save_all=True, append_images=images[1:], duration=125, loop=0)\n", + "with open(\"output.gif\", \"rb\") as gif_file:\n", + " b64 = f\"data:image/gif;base64,{base64.b64encode(gif_file.read()).decode()}\"\n", + "IPython.display.HTML(f'')" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Interactive demo\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def generate(prompt, seed, num_inference_steps, _=gr.Progress(track_tqdm=True)):\n", + " generator = torch.Generator().manual_seed(seed)\n", + " frames = ov_pipe(\n", + " prompt,\n", + " num_inference_steps=num_inference_steps,\n", + " generator=generator,\n", + " )[\"frames\"]\n", + " out_file = tempfile.NamedTemporaryFile(suffix=\".gif\", delete=False)\n", + " images = [PIL.Image.fromarray(frame) for frame in frames]\n", + " images[0].save(out_file, save_all=True, append_images=images[1:], duration=125, loop=0)\n", + " return out_file.name\n", + "\n", + "\n", + "demo = gr.Interface(\n", + " generate,\n", + " [\n", + " gr.Textbox(label=\"Prompt\"),\n", + " gr.Slider(0, 1000000, value=42, label=\"Seed\", step=1),\n", + " gr.Slider(10, 50, value=25, label=\"Number of inference steps\", step=1),\n", + " ],\n", + " gr.Image(label=\"Result\"),\n", + " examples=[\n", + " [\"An astronaut riding a horse.\", 0, 25],\n", + " [\"A panda eating bamboo on a rock.\", 0, 25],\n", + " [\"Spiderman is surfing.\", 0, 25],\n", + " ],\n", + " allow_flagging=\"never\",\n", + ")\n", + "\n", + "try:\n", + " demo.queue().launch(debug=True)\n", + "except Exception:\n", + " demo.queue().launch(share=True, debug=True)\n", + "# if you are launching remotely, specify server_name and server_port\n", + "# demo.launch(server_name='your server name', server_port='server port in int')\n", + "# Read more in the docs: https://gradio.app/docs/" + ] + } + ].map(fromJupyterCell) + , + [ + { + "cell_type": "markdown", + "source": [ + "# Video generation with ZeroScope and OpenVINO\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Install and import required packages](#Install-and-import-required-packages)\n", + "- [Load the model](#Load-the-model)\n", + "- [Convert the model](#Convert-the-model)\n", + " - [Define the conversion function](#Define-the-conversion-function)\n", + " - [UNet](#UNet)\n", + " - [VAE](#VAE)\n", + " - [Text encoder](#Text-encoder)\n", + "- [Build a pipeline](#Build-a-pipeline)\n", + "- [Inference with OpenVINO](#Inference-with-OpenVINO)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Define a prompt](#Define-a-prompt)\n", + " - [Video generation](#Video-generation)\n", + "- [Interactive demo](#Interactive-demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The ZeroScope model is a free and open-source text-to-video model that can generate realistic and engaging videos from text descriptions. It is based on the [Modelscope](https://modelscope.cn/models/damo/text-to-video-synthesis/summary) model, but it has been improved to produce higher-quality videos with a 16:9 aspect ratio and no Shutterstock watermark. The ZeroScope model is available in two versions: ZeroScope_v2 576w, which is optimized for rapid content creation at a resolution of 576x320 pixels, and ZeroScope_v2 XL, which upscales videos to a high-definition resolution of 1024x576.\n", + "\n", + "The ZeroScope model is trained on a dataset of over 9,000 videos and 29,000 tagged frames. It uses a diffusion model to generate videos, which means that it starts with a random noise image and gradually adds detail to it until it matches the text description. The ZeroScope model is still under development, but it has already been used to create some impressive videos. For example, it has been used to create videos of people dancing, playing sports, and even driving cars.\n", + "\n", + "The ZeroScope model is a powerful tool that can be used to create various videos, from simple animations to complex scenes. It is still under development, but it has the potential to revolutionize the way we create and consume video content.\n", + "\n", + "Both versions of the ZeroScope model are available on Hugging Face:\n", + " - [ZeroScope_v2 576w](https://huggingface.co/cerspense/zeroscope_v2_576w)\n", + " - [ZeroScope_v2 XL](https://huggingface.co/cerspense/zeroscope_v2_XL)\n", + "\n", + "We will use the first one." + ] + }, + { + "cell_type": "markdown", + "source": [ + "
\n", + " This tutorial requires at least 24GB of free memory to generate a video with a frame size of 432x240 and 16 frames. Increasing either of these values will require more memory and take more time.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Install and import required packages\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "To work with text-to-video synthesis model, we will use Hugging Face's [Diffusers](https://github.com/huggingface/diffusers) library. It provides already pretrained model from `cerspense`." + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install -q --extra-index-url https://download.pytorch.org/whl/cpu \"diffusers>=0.18.0\" \"torch>=2.1\" transformers \"openvino>=2023.1.0\" numpy \"gradio>=4.19\"" + ] + }, + { + "cell_type": "code", + "source": [ + "import gc\n", + "from typing import Optional, Union, List, Callable\n", + "import base64\n", + "import tempfile\n", + "import warnings\n", + "\n", + "import diffusers\n", + "import transformers\n", + "import numpy as np\n", + "import IPython\n", + "import torch\n", + "import PIL\n", + "import gradio as gr\n", + "\n", + "import openvino as ov" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Original 576x320 inference requires a lot of RAM (>100GB), so let's run our example on a smaller frame size, keeping the same aspect ratio. Try reducing values below to reduce the memory consumption." + ] + }, + { + "cell_type": "code", + "source": [ + "WIDTH = 432 # must be divisible by 8\n", + "HEIGHT = 240 # must be divisible by 8\n", + "NUM_FRAMES = 16" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The model is loaded from HuggingFace using `.from_pretrained` method of `diffusers.DiffusionPipeline`." + ] + }, + { + "cell_type": "code", + "source": [ + "pipe = diffusers.DiffusionPipeline.from_pretrained(\"cerspense/zeroscope_v2_576w\")" + ] + }, + { + "cell_type": "code", + "source": [ + "unet = pipe.unet\n", + "unet.eval()\n", + "vae = pipe.vae\n", + "vae.eval()\n", + "text_encoder = pipe.text_encoder\n", + "text_encoder.eval()\n", + "tokenizer = pipe.tokenizer\n", + "scheduler = pipe.scheduler\n", + "vae_scale_factor = pipe.vae_scale_factor\n", + "unet_in_channels = pipe.unet.config.in_channels\n", + "sample_width = WIDTH // vae_scale_factor\n", + "sample_height = HEIGHT // vae_scale_factor\n", + "del pipe\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Convert the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The architecture for generating videos from text comprises three distinct sub-networks: one for extracting text features, another for translating text features into the video latent space using a diffusion model, and a final one for mapping the video latent space to the visual space. The collective parameters of the entire model amount to approximately 1.7 billion. It's capable of processing English input. The diffusion model is built upon the Unet3D model and achieves video generation by iteratively denoising a starting point of pure Gaussian noise video." + ] + }, + { + "cell_type": "markdown", + "source": [] + }, + { + "cell_type": "markdown", + "source": [ + "### Define the conversion function\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Model components are PyTorch modules, that can be converted with `ov.convert_model` function directly. We also use `ov.save_model` function to serialize the result of conversion." + ] + }, + { + "cell_type": "code", + "source": [ + "warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)" + ] + }, + { + "cell_type": "code", + "source": [ + "from pathlib import Path\n", + "\n", + "\n", + "def convert(model: torch.nn.Module, xml_path: str, **convert_kwargs) -> Path:\n", + " xml_path = Path(xml_path)\n", + " if not xml_path.exists():\n", + " xml_path.parent.mkdir(parents=True, exist_ok=True)\n", + " with torch.no_grad():\n", + " converted_model = ov.convert_model(model, **convert_kwargs)\n", + " ov.save_model(converted_model, xml_path)\n", + " del converted_model\n", + " gc.collect()\n", + " torch._C._jit_clear_class_registry()\n", + " torch.jit._recursive.concrete_type_store = torch.jit._recursive.ConcreteTypeStore()\n", + " torch.jit._state._clear_class_state()\n", + " return xml_path" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### UNet\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text-to-video generation pipeline main component is a conditional 3D UNet model that takes a noisy sample, conditional state, and a timestep and returns a sample shaped output." + ] + }, + { + "cell_type": "code", + "source": [ + "unet_xml_path = convert(\n", + " unet,\n", + " \"models/unet.xml\",\n", + " example_input={\n", + " \"sample\": torch.randn(2, 4, 2, int(sample_height // 2), int(sample_width // 2)),\n", + " \"timestep\": torch.tensor(1),\n", + " \"encoder_hidden_states\": torch.randn(2, 77, 1024),\n", + " },\n", + " input=[\n", + " (\"sample\", (2, 4, NUM_FRAMES, sample_height, sample_width)),\n", + " (\"timestep\", ()),\n", + " (\"encoder_hidden_states\", (2, 77, 1024)),\n", + " ],\n", + ")\n", + "del unet\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### VAE\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Variational autoencoder (VAE) uses UNet output to decode latents to visual representations. Our VAE model has KL loss for encoding images into latents and decoding latent representations into images. For inference, we need only decoder part." + ] + }, + { + "cell_type": "code", + "source": [ + "class VaeDecoderWrapper(torch.nn.Module):\n", + " def __init__(self, vae):\n", + " super().__init__()\n", + " self.vae = vae\n", + "\n", + " def forward(self, z: torch.FloatTensor):\n", + " return self.vae.decode(z)" + ] + }, + { + "cell_type": "code", + "source": [ + "vae_decoder_xml_path = convert(\n", + " VaeDecoderWrapper(vae),\n", + " \"models/vae.xml\",\n", + " example_input=torch.randn(2, 4, 32, 32),\n", + " input=((NUM_FRAMES, 4, sample_height, sample_width)),\n", + ")\n", + "del vae\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Text encoder\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text encoder is used to encode the input prompt to tensor. Default tensor length is 77." + ] + }, + { + "cell_type": "code", + "source": [ + "text_encoder_xml = convert(\n", + " text_encoder,\n", + " \"models/text_encoder.xml\",\n", + " example_input=torch.ones(1, 77, dtype=torch.int64),\n", + " input=((1, 77), ov.Type.i64),\n", + ")\n", + "del text_encoder\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Build a pipeline\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def tensor2vid(video: torch.Tensor, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) -> List[np.ndarray]:\n", + " # This code is copied from https://github.com/modelscope/modelscope/blob/1509fdb973e5871f37148a4b5e5964cafd43e64d/modelscope/pipelines/multi_modal/text_to_video_synthesis_pipeline.py#L78\n", + " # reshape to ncfhw\n", + " mean = torch.tensor(mean, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " std = torch.tensor(std, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " # unnormalize back to [0,1]\n", + " video = video.mul_(std).add_(mean)\n", + " video.clamp_(0, 1)\n", + " # prepare the final outputs\n", + " i, c, f, h, w = video.shape\n", + " images = video.permute(2, 3, 0, 4, 1).reshape(f, h, i * w, c) # 1st (frames, h, batch_size, w, c) 2nd (frames, h, batch_size * w, c)\n", + " images = images.unbind(dim=0) # prepare a list of indvidual (consecutive frames)\n", + " images = [(image.cpu().numpy() * 255).astype(\"uint8\") for image in images] # f h w c\n", + " return images" + ] + }, + { + "cell_type": "code", + "source": [ + "try:\n", + " from diffusers.utils import randn_tensor\n", + "except ImportError:\n", + " from diffusers.utils.torch_utils import randn_tensor\n", + "\n", + "\n", + "class OVTextToVideoSDPipeline(diffusers.DiffusionPipeline):\n", + " def __init__(\n", + " self,\n", + " vae_decoder: ov.CompiledModel,\n", + " text_encoder: ov.CompiledModel,\n", + " tokenizer: transformers.CLIPTokenizer,\n", + " unet: ov.CompiledModel,\n", + " scheduler: diffusers.schedulers.DDIMScheduler,\n", + " ):\n", + " super().__init__()\n", + "\n", + " self.vae_decoder = vae_decoder\n", + " self.text_encoder = text_encoder\n", + " self.tokenizer = tokenizer\n", + " self.unet = unet\n", + " self.scheduler = scheduler\n", + " self.vae_scale_factor = vae_scale_factor\n", + " self.unet_in_channels = unet_in_channels\n", + " self.width = WIDTH\n", + " self.height = HEIGHT\n", + " self.num_frames = NUM_FRAMES\n", + "\n", + " def __call__(\n", + " self,\n", + " prompt: Union[str, List[str]] = None,\n", + " num_inference_steps: int = 50,\n", + " guidance_scale: float = 9.0,\n", + " negative_prompt: Optional[Union[str, List[str]]] = None,\n", + " eta: float = 0.0,\n", + " generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,\n", + " latents: Optional[torch.FloatTensor] = None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " output_type: Optional[str] = \"np\",\n", + " return_dict: bool = True,\n", + " callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,\n", + " callback_steps: int = 1,\n", + " ):\n", + " r\"\"\"\n", + " Function invoked when calling the pipeline for generation.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts to guide the video generation. If not defined, one has to pass `prompt_embeds`.\n", + " instead.\n", + " num_inference_steps (`int`, *optional*, defaults to 50):\n", + " The number of denoising steps. More denoising steps usually lead to a higher quality videos at the\n", + " expense of slower inference.\n", + " guidance_scale (`float`, *optional*, defaults to 7.5):\n", + " Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).\n", + " `guidance_scale` is defined as `w` of equation 2. of [Imagen\n", + " Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >\n", + " 1`. Higher guidance scale encourages to generate videos that are closely linked to the text `prompt`,\n", + " usually at the expense of lower video quality.\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the video generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " eta (`float`, *optional*, defaults to 0.0):\n", + " Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to\n", + " [`schedulers.DDIMScheduler`], will be ignored for others.\n", + " generator (`torch.Generator` or `List[torch.Generator]`, *optional*):\n", + " One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)\n", + " to make generation deterministic.\n", + " latents (`torch.FloatTensor`, *optional*):\n", + " Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for video\n", + " generation. Can be used to tweak the same generation with different prompts. If not provided, a latents\n", + " tensor will ge generated by sampling using the supplied random `generator`. Latents should be of shape\n", + " `(batch_size, num_channel, num_frames, height, width)`.\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " output_type (`str`, *optional*, defaults to `\"np\"`):\n", + " The output format of the generate video. Choose between `torch.FloatTensor` or `np.array`.\n", + " return_dict (`bool`, *optional*, defaults to `True`):\n", + " Whether or not to return a [`~pipelines.stable_diffusion.TextToVideoSDPipelineOutput`] instead of a\n", + " plain tuple.\n", + " callback (`Callable`, *optional*):\n", + " A function that will be called every `callback_steps` steps during inference. The function will be\n", + " called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`.\n", + " callback_steps (`int`, *optional*, defaults to 1):\n", + " The frequency at which the `callback` function will be called. If not specified, the callback will be\n", + " called at every step.\n", + "\n", + " Returns:\n", + " `List[np.ndarray]`: generated video frames\n", + " \"\"\"\n", + "\n", + " num_images_per_prompt = 1\n", + "\n", + " # 1. Check inputs. Raise error if not correct\n", + " self.check_inputs(\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt,\n", + " prompt_embeds,\n", + " negative_prompt_embeds,\n", + " )\n", + "\n", + " # 2. Define call parameters\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)\n", + " # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`\n", + " # corresponds to doing no classifier free guidance.\n", + " do_classifier_free_guidance = guidance_scale > 1.0\n", + "\n", + " # 3. Encode input prompt\n", + " prompt_embeds = self._encode_prompt(\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt,\n", + " prompt_embeds=prompt_embeds,\n", + " negative_prompt_embeds=negative_prompt_embeds,\n", + " )\n", + "\n", + " # 4. Prepare timesteps\n", + " self.scheduler.set_timesteps(num_inference_steps)\n", + " timesteps = self.scheduler.timesteps\n", + "\n", + " # 5. Prepare latent variables\n", + " num_channels_latents = self.unet_in_channels\n", + " latents = self.prepare_latents(\n", + " batch_size * num_images_per_prompt,\n", + " num_channels_latents,\n", + " prompt_embeds.dtype,\n", + " generator,\n", + " latents,\n", + " )\n", + "\n", + " # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline\n", + " extra_step_kwargs = {\"generator\": generator, \"eta\": eta}\n", + "\n", + " # 7. Denoising loop\n", + " num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order\n", + " with self.progress_bar(total=num_inference_steps) as progress_bar:\n", + " for i, t in enumerate(timesteps):\n", + " # expand the latents if we are doing classifier free guidance\n", + " latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents\n", + " latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)\n", + "\n", + " # predict the noise residual\n", + " noise_pred = self.unet(\n", + " {\n", + " \"sample\": latent_model_input,\n", + " \"timestep\": t,\n", + " \"encoder_hidden_states\": prompt_embeds,\n", + " }\n", + " )[0]\n", + " noise_pred = torch.tensor(noise_pred)\n", + "\n", + " # perform guidance\n", + " if do_classifier_free_guidance:\n", + " noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)\n", + " noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)\n", + "\n", + " # reshape latents\n", + " bsz, channel, frames, width, height = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + " noise_pred = noise_pred.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + "\n", + " # compute the previous noisy sample x_t -> x_t-1\n", + " latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample\n", + "\n", + " # reshape latents back\n", + " latents = latents[None, :].reshape(bsz, frames, channel, width, height).permute(0, 2, 1, 3, 4)\n", + "\n", + " # call the callback, if provided\n", + " if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):\n", + " progress_bar.update()\n", + " if callback is not None and i % callback_steps == 0:\n", + " callback(i, t, latents)\n", + "\n", + " video_tensor = self.decode_latents(latents)\n", + "\n", + " if output_type == \"pt\":\n", + " video = video_tensor\n", + " else:\n", + " video = tensor2vid(video_tensor)\n", + "\n", + " if not return_dict:\n", + " return (video,)\n", + "\n", + " return {\"frames\": video}\n", + "\n", + " # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt\n", + " def _encode_prompt(\n", + " self,\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt=None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " ):\n", + " r\"\"\"\n", + " Encodes the prompt into text encoder hidden states.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " prompt to be encoded\n", + " num_images_per_prompt (`int`):\n", + " number of images that should be generated per prompt\n", + " do_classifier_free_guidance (`bool`):\n", + " whether to use classifier free guidance or not\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the image generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " \"\"\"\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " if prompt_embeds is None:\n", + " text_inputs = self.tokenizer(\n", + " prompt,\n", + " padding=\"max_length\",\n", + " max_length=self.tokenizer.model_max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + " text_input_ids = text_inputs.input_ids\n", + " untruncated_ids = self.tokenizer(prompt, padding=\"longest\", return_tensors=\"pt\").input_ids\n", + "\n", + " if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):\n", + " removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1])\n", + " print(\n", + " \"The following part of your input was truncated because CLIP can only handle sequences up to\"\n", + " f\" {self.tokenizer.model_max_length} tokens: {removed_text}\"\n", + " )\n", + "\n", + " prompt_embeds = self.text_encoder(text_input_ids)\n", + " prompt_embeds = prompt_embeds[0]\n", + " prompt_embeds = torch.tensor(prompt_embeds)\n", + "\n", + " bs_embed, seq_len, _ = prompt_embeds.shape\n", + " # duplicate text embeddings for each generation per prompt, using mps friendly method\n", + " prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # get unconditional embeddings for classifier free guidance\n", + " if do_classifier_free_guidance and negative_prompt_embeds is None:\n", + " uncond_tokens: List[str]\n", + " if negative_prompt is None:\n", + " uncond_tokens = [\"\"] * batch_size\n", + " elif type(prompt) is not type(negative_prompt):\n", + " raise TypeError(f\"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=\" f\" {type(prompt)}.\")\n", + " elif isinstance(negative_prompt, str):\n", + " uncond_tokens = [negative_prompt]\n", + " elif batch_size != len(negative_prompt):\n", + " raise ValueError(\n", + " f\"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:\"\n", + " f\" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches\"\n", + " \" the batch size of `prompt`.\"\n", + " )\n", + " else:\n", + " uncond_tokens = negative_prompt\n", + "\n", + " max_length = prompt_embeds.shape[1]\n", + " uncond_input = self.tokenizer(\n", + " uncond_tokens,\n", + " padding=\"max_length\",\n", + " max_length=max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + "\n", + " negative_prompt_embeds = self.text_encoder(uncond_input.input_ids)\n", + " negative_prompt_embeds = negative_prompt_embeds[0]\n", + " negative_prompt_embeds = torch.tensor(negative_prompt_embeds)\n", + "\n", + " if do_classifier_free_guidance:\n", + " # duplicate unconditional embeddings for each generation per prompt, using mps friendly method\n", + " seq_len = negative_prompt_embeds.shape[1]\n", + "\n", + " negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # For classifier free guidance, we need to do two forward passes.\n", + " # Here we concatenate the unconditional and text embeddings into a single batch\n", + " # to avoid doing two forward passes\n", + " prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])\n", + "\n", + " return prompt_embeds\n", + "\n", + " def prepare_latents(\n", + " self,\n", + " batch_size,\n", + " num_channels_latents,\n", + " dtype,\n", + " generator,\n", + " latents=None,\n", + " ):\n", + " shape = (\n", + " batch_size,\n", + " num_channels_latents,\n", + " self.num_frames,\n", + " self.height // self.vae_scale_factor,\n", + " self.width // self.vae_scale_factor,\n", + " )\n", + " if isinstance(generator, list) and len(generator) != batch_size:\n", + " raise ValueError(\n", + " f\"You have passed a list of generators of length {len(generator)}, but requested an effective batch\"\n", + " f\" size of {batch_size}. Make sure the batch size matches the length of the generators.\"\n", + " )\n", + "\n", + " if latents is None:\n", + " latents = randn_tensor(shape, generator=generator, dtype=dtype)\n", + "\n", + " # scale the initial noise by the standard deviation required by the scheduler\n", + " latents = latents * self.scheduler.init_noise_sigma\n", + " return latents\n", + "\n", + " def check_inputs(\n", + " self,\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt=None,\n", + " prompt_embeds=None,\n", + " negative_prompt_embeds=None,\n", + " ):\n", + " if self.height % 8 != 0 or self.width % 8 != 0:\n", + " raise ValueError(f\"`height` and `width` have to be divisible by 8 but are {self.height} and {self.width}.\")\n", + "\n", + " if (callback_steps is None) or (callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0)):\n", + " raise ValueError(f\"`callback_steps` has to be a positive integer but is {callback_steps} of type\" f\" {type(callback_steps)}.\")\n", + "\n", + " if prompt is not None and prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to\" \" only forward one of the two.\"\n", + " )\n", + " elif prompt is None and prompt_embeds is None:\n", + " raise ValueError(\"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined.\")\n", + " elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):\n", + " raise ValueError(f\"`prompt` has to be of type `str` or `list` but is {type(prompt)}\")\n", + "\n", + " if negative_prompt is not None and negative_prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:\"\n", + " f\" {negative_prompt_embeds}. Please make sure to only forward one of the two.\"\n", + " )\n", + "\n", + " if prompt_embeds is not None and negative_prompt_embeds is not None:\n", + " if prompt_embeds.shape != negative_prompt_embeds.shape:\n", + " raise ValueError(\n", + " \"`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but\"\n", + " f\" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`\"\n", + " f\" {negative_prompt_embeds.shape}.\"\n", + " )\n", + "\n", + " def decode_latents(self, latents):\n", + " scale_factor = 0.18215\n", + " latents = 1 / scale_factor * latents\n", + "\n", + " batch_size, channels, num_frames, height, width = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_frames, channels, height, width)\n", + " image = self.vae_decoder(latents)[0]\n", + " image = torch.tensor(image)\n", + " video = (\n", + " image[None, :]\n", + " .reshape(\n", + " (\n", + " batch_size,\n", + " num_frames,\n", + " -1,\n", + " )\n", + " + image.shape[2:]\n", + " )\n", + " .permute(0, 2, 1, 3, 4)\n", + " )\n", + " # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16\n", + " video = video.float()\n", + " return video" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Inference with OpenVINO\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "from notebook_utils import device_widget\n", + "\n", + "device = device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_unet = core.compile_model(unet_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_vae_decoder = core.compile_model(vae_decoder_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_text_encoder = core.compile_model(text_encoder_xml, device_name=device.value)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Here we replace the pipeline parts with versions converted to OpenVINO IR and compiled to specific device. Note that we use original pipeline tokenizer and scheduler." + ] + }, + { + "cell_type": "code", + "source": [ + "ov_pipe = OVTextToVideoSDPipeline(ov_vae_decoder, ov_text_encoder, tokenizer, ov_unet, scheduler)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Define a prompt\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "prompt = \"A panda eating bamboo on a rock.\"" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Let's generate a video for our prompt. For full list of arguments, see `__call__` function definition of `OVTextToVideoSDPipeline` class in [Build a pipeline](#Build-a-pipeline) section." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Video generation\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "frames = ov_pipe(prompt, num_inference_steps=25)[\"frames\"]" + ] + }, + { + "cell_type": "code", + "source": [ + "images = [PIL.Image.fromarray(frame) for frame in frames]\n", + "images[0].save(\"output.gif\", save_all=True, append_images=images[1:], duration=125, loop=0)\n", + "with open(\"output.gif\", \"rb\") as gif_file:\n", + " b64 = f\"data:image/gif;base64,{base64.b64encode(gif_file.read()).decode()}\"\n", + "IPython.display.HTML(f'')" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Interactive demo\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "metadata": { + "test_replace": { + " demo.queue().launch(debug=True)": " demo.queue.launch()", + " demo.queue().launch(share=True, debug=True)": " demo.queue().launch(share=True)" + } + }, + "source": [ + "def generate(prompt, seed, num_inference_steps, _=gr.Progress(track_tqdm=True)):\n", + " generator = torch.Generator().manual_seed(seed)\n", + " frames = ov_pipe(\n", + " prompt,\n", + " num_inference_steps=num_inference_steps,\n", + " generator=generator,\n", + " )[\"frames\"]\n", + " out_file = tempfile.NamedTemporaryFile(suffix=\".gif\", delete=False)\n", + " images = [PIL.Image.fromarray(frame) for frame in frames]\n", + " images[0].save(out_file, save_all=True, append_images=images[1:], duration=125, loop=0)\n", + " return out_file.name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "if not Path(\"gradio_helper.py\").exists():\n", + " r = requests.get(url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/notebooks/zeroscope-text2video/gradio_helper.py\")\n", + " open(\"gradio_helper.py\", \"w\").write(r.text)\n", + "\n", + "from gradio_helper import make_demo\n", + "\n", + "demo = make_demo(fn=generate)\n", + "\n", + "try:\n", + " demo.queue().launch(debug=True)\n", + "except Exception:\n", + " demo.queue().launch(share=True, debug=True)\n", + "# If you are launching remotely, specify server_name and server_port\n", + "# EXAMPLE: `demo.launch(server_name='your server name', server_port='server port in int')`\n", + "# To learn more please refer to the Gradio docs: https://gradio.app/docs/" + ] + }, + { + "cell_type": "code", + "source": [ + "# please uncomment and run this cell for stopping gradio interface\n", + "# demo.close()" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + { modified: 24, original: 24 }, + { modified: 25, original: 25 }, + { modified: 26, original: 26 }, + { modified: 27, original: 27 }, + { modified: 28, original: 28 }, + { modified: 29, original: 29 }, + { modified: 30, original: 30 }, + { modified: 31, original: 31 }, + { modified: 32, original: 32 }, + { modified: 33, original: 33 }, + { modified: 34, original: 34 }, + { modified: 35, original: 35 }, + { modified: 36, original: 36 }, + { modified: 37, original: 37 }, + { modified: 38, original: 38 }, + { modified: 39, original: 39 }, + { modified: 40, original: 40 }, + { modified: 41, original: 41 }, + { modified: 42, original: 42 }, + { modified: 43, original: 43 }, + { modified: 44, original: 44 }, + { modified: 45, original: 45 }, + { modified: 46, original: 46 }, + { modified: 47, original: 47 }, + { modified: 48, original: 48 }, + { modified: 49, original: 49 }, + { modified: 50, original: -1 }, + { modified: 51, original: -1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 49, originalLength: 1, modifiedStart: 49, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 50, originalLength: 0, modifiedStart: 50, modifiedLength: 2 } satisfies IDiffChange, + ]); + }); + test('Modification of three cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "source": [ + "# Video generation with ZeroScope and OpenVINO\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Install and import required packages](#Install-and-import-required-packages)\n", + "- [Load the model](#Load-the-model)\n", + "- [Convert the model](#Convert-the-model)\n", + " - [Define the conversion function](#Define-the-conversion-function)\n", + " - [UNet](#UNet)\n", + " - [VAE](#VAE)\n", + " - [Text encoder](#Text-encoder)\n", + "- [Build a pipeline](#Build-a-pipeline)\n", + "- [Inference with OpenVINO](#Inference-with-OpenVINO)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Define a prompt](#Define-a-prompt)\n", + " - [Video generation](#Video-generation)\n", + "- [Interactive demo](#Interactive-demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The ZeroScope model is a free and open-source text-to-video model that can generate realistic and engaging videos from text descriptions. It is based on the [Modelscope](https://modelscope.cn/models/damo/text-to-video-synthesis/summary) model, but it has been improved to produce higher-quality videos with a 16:9 aspect ratio and no Shutterstock watermark. The ZeroScope model is available in two versions: ZeroScope_v2 576w, which is optimized for rapid content creation at a resolution of 576x320 pixels, and ZeroScope_v2 XL, which upscales videos to a high-definition resolution of 1024x576.\n", + "\n", + "The ZeroScope model is trained on a dataset of over 9,000 videos and 29,000 tagged frames. It uses a diffusion model to generate videos, which means that it starts with a random noise image and gradually adds detail to it until it matches the text description. The ZeroScope model is still under development, but it has already been used to create some impressive videos. For example, it has been used to create videos of people dancing, playing sports, and even driving cars.\n", + "\n", + "The ZeroScope model is a powerful tool that can be used to create various videos, from simple animations to complex scenes. It is still under development, but it has the potential to revolutionize the way we create and consume video content.\n", + "\n", + "Both versions of the ZeroScope model are available on Hugging Face:\n", + " - [ZeroScope_v2 576w](https://huggingface.co/cerspense/zeroscope_v2_576w)\n", + " - [ZeroScope_v2 XL](https://huggingface.co/cerspense/zeroscope_v2_XL)\n", + "\n", + "We will use the first one." + ] + }, + { + "cell_type": "markdown", + "source": [ + "
\n", + " This tutorial requires at least 24GB of free memory to generate a video with a frame size of 432x240 and 16 frames. Increasing either of these values will require more memory and take more time.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Install and import required packages\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "To work with text-to-video synthesis model, we will use Hugging Face's [Diffusers](https://github.com/huggingface/diffusers) library. It provides already pretrained model from `cerspense`." + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install -q --extra-index-url https://download.pytorch.org/whl/cpu \"diffusers>=0.18.0\" \"torch>=2.1\" transformers \"openvino>=2023.1.0\" numpy \"gradio>=4.19\"" + ] + }, + { + "cell_type": "code", + "source": [ + "import gc\n", + "from typing import Optional, Union, List, Callable\n", + "import base64\n", + "import tempfile\n", + "import warnings\n", + "\n", + "import diffusers\n", + "import transformers\n", + "import numpy as np\n", + "import IPython\n", + "import ipywidgets as widgets\n", + "import torch\n", + "import PIL\n", + "import gradio as gr\n", + "\n", + "import openvino as ov" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Original 576x320 inference requires a lot of RAM (>100GB), so let's run our example on a smaller frame size, keeping the same aspect ratio. Try reducing values below to reduce the memory consumption." + ] + }, + { + "cell_type": "code", + "source": [ + "WIDTH = 432 # must be divisible by 8\n", + "HEIGHT = 240 # must be divisible by 8\n", + "NUM_FRAMES = 16" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The model is loaded from HuggingFace using `.from_pretrained` method of `diffusers.DiffusionPipeline`." + ] + }, + { + "cell_type": "code", + "source": [ + "pipe = diffusers.DiffusionPipeline.from_pretrained(\"cerspense/zeroscope_v2_576w\")" + ] + }, + { + "cell_type": "code", + "source": [ + "unet = pipe.unet\n", + "unet.eval()\n", + "vae = pipe.vae\n", + "vae.eval()\n", + "text_encoder = pipe.text_encoder\n", + "text_encoder.eval()\n", + "tokenizer = pipe.tokenizer\n", + "scheduler = pipe.scheduler\n", + "vae_scale_factor = pipe.vae_scale_factor\n", + "unet_in_channels = pipe.unet.config.in_channels\n", + "sample_width = WIDTH // vae_scale_factor\n", + "sample_height = HEIGHT // vae_scale_factor\n", + "del pipe\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Convert the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The architecture for generating videos from text comprises three distinct sub-networks: one for extracting text features, another for translating text features into the video latent space using a diffusion model, and a final one for mapping the video latent space to the visual space. The collective parameters of the entire model amount to approximately 1.7 billion. It's capable of processing English input. The diffusion model is built upon the Unet3D model and achieves video generation by iteratively denoising a starting point of pure Gaussian noise video." + ] + }, + { + "cell_type": "markdown", + "source": [] + }, + { + "cell_type": "markdown", + "source": [ + "### Define the conversion function\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Model components are PyTorch modules, that can be converted with `ov.convert_model` function directly. We also use `ov.save_model` function to serialize the result of conversion." + ] + }, + { + "cell_type": "code", + "source": [ + "warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)" + ] + }, + { + "cell_type": "code", + "source": [ + "from pathlib import Path\n", + "\n", + "\n", + "def convert(model: torch.nn.Module, xml_path: str, **convert_kwargs) -> Path:\n", + " xml_path = Path(xml_path)\n", + " if not xml_path.exists():\n", + " xml_path.parent.mkdir(parents=True, exist_ok=True)\n", + " with torch.no_grad():\n", + " converted_model = ov.convert_model(model, **convert_kwargs)\n", + " ov.save_model(converted_model, xml_path)\n", + " del converted_model\n", + " gc.collect()\n", + " torch._C._jit_clear_class_registry()\n", + " torch.jit._recursive.concrete_type_store = torch.jit._recursive.ConcreteTypeStore()\n", + " torch.jit._state._clear_class_state()\n", + " return xml_path" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### UNet\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text-to-video generation pipeline main component is a conditional 3D UNet model that takes a noisy sample, conditional state, and a timestep and returns a sample shaped output." + ] + }, + { + "cell_type": "code", + "source": [ + "unet_xml_path = convert(\n", + " unet,\n", + " \"models/unet.xml\",\n", + " example_input={\n", + " \"sample\": torch.randn(2, 4, 2, int(sample_height // 2), int(sample_width // 2)),\n", + " \"timestep\": torch.tensor(1),\n", + " \"encoder_hidden_states\": torch.randn(2, 77, 1024),\n", + " },\n", + " input=[\n", + " (\"sample\", (2, 4, NUM_FRAMES, sample_height, sample_width)),\n", + " (\"timestep\", ()),\n", + " (\"encoder_hidden_states\", (2, 77, 1024)),\n", + " ],\n", + ")\n", + "del unet\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### VAE\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Variational autoencoder (VAE) uses UNet output to decode latents to visual representations. Our VAE model has KL loss for encoding images into latents and decoding latent representations into images. For inference, we need only decoder part." + ] + }, + { + "cell_type": "code", + "source": [ + "class VaeDecoderWrapper(torch.nn.Module):\n", + " def __init__(self, vae):\n", + " super().__init__()\n", + " self.vae = vae\n", + "\n", + " def forward(self, z: torch.FloatTensor):\n", + " return self.vae.decode(z)" + ] + }, + { + "cell_type": "code", + "source": [ + "vae_decoder_xml_path = convert(\n", + " VaeDecoderWrapper(vae),\n", + " \"models/vae.xml\",\n", + " example_input=torch.randn(2, 4, 32, 32),\n", + " input=((NUM_FRAMES, 4, sample_height, sample_width)),\n", + ")\n", + "del vae\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Text encoder\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text encoder is used to encode the input prompt to tensor. Default tensor length is 77." + ] + }, + { + "cell_type": "code", + "source": [ + "text_encoder_xml = convert(\n", + " text_encoder,\n", + " \"models/text_encoder.xml\",\n", + " example_input=torch.ones(1, 77, dtype=torch.int64),\n", + " input=((1, 77), ov.Type.i64),\n", + ")\n", + "del text_encoder\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Build a pipeline\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def tensor2vid(video: torch.Tensor, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) -> List[np.ndarray]:\n", + " # This code is copied from https://github.com/modelscope/modelscope/blob/1509fdb973e5871f37148a4b5e5964cafd43e64d/modelscope/pipelines/multi_modal/text_to_video_synthesis_pipeline.py#L78\n", + " # reshape to ncfhw\n", + " mean = torch.tensor(mean, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " std = torch.tensor(std, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " # unnormalize back to [0,1]\n", + " video = video.mul_(std).add_(mean)\n", + " video.clamp_(0, 1)\n", + " # prepare the final outputs\n", + " i, c, f, h, w = video.shape\n", + " images = video.permute(2, 3, 0, 4, 1).reshape(f, h, i * w, c) # 1st (frames, h, batch_size, w, c) 2nd (frames, h, batch_size * w, c)\n", + " images = images.unbind(dim=0) # prepare a list of indvidual (consecutive frames)\n", + " images = [(image.cpu().numpy() * 255).astype(\"uint8\") for image in images] # f h w c\n", + " return images" + ] + }, + { + "cell_type": "code", + "source": [ + "try:\n", + " from diffusers.utils import randn_tensor\n", + "except ImportError:\n", + " from diffusers.utils.torch_utils import randn_tensor\n", + "\n", + "\n", + "class OVTextToVideoSDPipeline(diffusers.DiffusionPipeline):\n", + " def __init__(\n", + " self,\n", + " vae_decoder: ov.CompiledModel,\n", + " text_encoder: ov.CompiledModel,\n", + " tokenizer: transformers.CLIPTokenizer,\n", + " unet: ov.CompiledModel,\n", + " scheduler: diffusers.schedulers.DDIMScheduler,\n", + " ):\n", + " super().__init__()\n", + "\n", + " self.vae_decoder = vae_decoder\n", + " self.text_encoder = text_encoder\n", + " self.tokenizer = tokenizer\n", + " self.unet = unet\n", + " self.scheduler = scheduler\n", + " self.vae_scale_factor = vae_scale_factor\n", + " self.unet_in_channels = unet_in_channels\n", + " self.width = WIDTH\n", + " self.height = HEIGHT\n", + " self.num_frames = NUM_FRAMES\n", + "\n", + " def __call__(\n", + " self,\n", + " prompt: Union[str, List[str]] = None,\n", + " num_inference_steps: int = 50,\n", + " guidance_scale: float = 9.0,\n", + " negative_prompt: Optional[Union[str, List[str]]] = None,\n", + " eta: float = 0.0,\n", + " generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,\n", + " latents: Optional[torch.FloatTensor] = None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " output_type: Optional[str] = \"np\",\n", + " return_dict: bool = True,\n", + " callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,\n", + " callback_steps: int = 1,\n", + " ):\n", + " r\"\"\"\n", + " Function invoked when calling the pipeline for generation.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts to guide the video generation. If not defined, one has to pass `prompt_embeds`.\n", + " instead.\n", + " num_inference_steps (`int`, *optional*, defaults to 50):\n", + " The number of denoising steps. More denoising steps usually lead to a higher quality videos at the\n", + " expense of slower inference.\n", + " guidance_scale (`float`, *optional*, defaults to 7.5):\n", + " Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).\n", + " `guidance_scale` is defined as `w` of equation 2. of [Imagen\n", + " Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >\n", + " 1`. Higher guidance scale encourages to generate videos that are closely linked to the text `prompt`,\n", + " usually at the expense of lower video quality.\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the video generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " eta (`float`, *optional*, defaults to 0.0):\n", + " Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to\n", + " [`schedulers.DDIMScheduler`], will be ignored for others.\n", + " generator (`torch.Generator` or `List[torch.Generator]`, *optional*):\n", + " One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)\n", + " to make generation deterministic.\n", + " latents (`torch.FloatTensor`, *optional*):\n", + " Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for video\n", + " generation. Can be used to tweak the same generation with different prompts. If not provided, a latents\n", + " tensor will ge generated by sampling using the supplied random `generator`. Latents should be of shape\n", + " `(batch_size, num_channel, num_frames, height, width)`.\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " output_type (`str`, *optional*, defaults to `\"np\"`):\n", + " The output format of the generate video. Choose between `torch.FloatTensor` or `np.array`.\n", + " return_dict (`bool`, *optional*, defaults to `True`):\n", + " Whether or not to return a [`~pipelines.stable_diffusion.TextToVideoSDPipelineOutput`] instead of a\n", + " plain tuple.\n", + " callback (`Callable`, *optional*):\n", + " A function that will be called every `callback_steps` steps during inference. The function will be\n", + " called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`.\n", + " callback_steps (`int`, *optional*, defaults to 1):\n", + " The frequency at which the `callback` function will be called. If not specified, the callback will be\n", + " called at every step.\n", + "\n", + " Returns:\n", + " `List[np.ndarray]`: generated video frames\n", + " \"\"\"\n", + "\n", + " num_images_per_prompt = 1\n", + "\n", + " # 1. Check inputs. Raise error if not correct\n", + " self.check_inputs(\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt,\n", + " prompt_embeds,\n", + " negative_prompt_embeds,\n", + " )\n", + "\n", + " # 2. Define call parameters\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)\n", + " # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`\n", + " # corresponds to doing no classifier free guidance.\n", + " do_classifier_free_guidance = guidance_scale > 1.0\n", + "\n", + " # 3. Encode input prompt\n", + " prompt_embeds = self._encode_prompt(\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt,\n", + " prompt_embeds=prompt_embeds,\n", + " negative_prompt_embeds=negative_prompt_embeds,\n", + " )\n", + "\n", + " # 4. Prepare timesteps\n", + " self.scheduler.set_timesteps(num_inference_steps)\n", + " timesteps = self.scheduler.timesteps\n", + "\n", + " # 5. Prepare latent variables\n", + " num_channels_latents = self.unet_in_channels\n", + " latents = self.prepare_latents(\n", + " batch_size * num_images_per_prompt,\n", + " num_channels_latents,\n", + " prompt_embeds.dtype,\n", + " generator,\n", + " latents,\n", + " )\n", + "\n", + " # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline\n", + " extra_step_kwargs = {\"generator\": generator, \"eta\": eta}\n", + "\n", + " # 7. Denoising loop\n", + " num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order\n", + " with self.progress_bar(total=num_inference_steps) as progress_bar:\n", + " for i, t in enumerate(timesteps):\n", + " # expand the latents if we are doing classifier free guidance\n", + " latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents\n", + " latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)\n", + "\n", + " # predict the noise residual\n", + " noise_pred = self.unet(\n", + " {\n", + " \"sample\": latent_model_input,\n", + " \"timestep\": t,\n", + " \"encoder_hidden_states\": prompt_embeds,\n", + " }\n", + " )[0]\n", + " noise_pred = torch.tensor(noise_pred)\n", + "\n", + " # perform guidance\n", + " if do_classifier_free_guidance:\n", + " noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)\n", + " noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)\n", + "\n", + " # reshape latents\n", + " bsz, channel, frames, width, height = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + " noise_pred = noise_pred.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + "\n", + " # compute the previous noisy sample x_t -> x_t-1\n", + " latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample\n", + "\n", + " # reshape latents back\n", + " latents = latents[None, :].reshape(bsz, frames, channel, width, height).permute(0, 2, 1, 3, 4)\n", + "\n", + " # call the callback, if provided\n", + " if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):\n", + " progress_bar.update()\n", + " if callback is not None and i % callback_steps == 0:\n", + " callback(i, t, latents)\n", + "\n", + " video_tensor = self.decode_latents(latents)\n", + "\n", + " if output_type == \"pt\":\n", + " video = video_tensor\n", + " else:\n", + " video = tensor2vid(video_tensor)\n", + "\n", + " if not return_dict:\n", + " return (video,)\n", + "\n", + " return {\"frames\": video}\n", + "\n", + " # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt\n", + " def _encode_prompt(\n", + " self,\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt=None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " ):\n", + " r\"\"\"\n", + " Encodes the prompt into text encoder hidden states.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " prompt to be encoded\n", + " num_images_per_prompt (`int`):\n", + " number of images that should be generated per prompt\n", + " do_classifier_free_guidance (`bool`):\n", + " whether to use classifier free guidance or not\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the image generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " \"\"\"\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " if prompt_embeds is None:\n", + " text_inputs = self.tokenizer(\n", + " prompt,\n", + " padding=\"max_length\",\n", + " max_length=self.tokenizer.model_max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + " text_input_ids = text_inputs.input_ids\n", + " untruncated_ids = self.tokenizer(prompt, padding=\"longest\", return_tensors=\"pt\").input_ids\n", + "\n", + " if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):\n", + " removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1])\n", + " print(\n", + " \"The following part of your input was truncated because CLIP can only handle sequences up to\"\n", + " f\" {self.tokenizer.model_max_length} tokens: {removed_text}\"\n", + " )\n", + "\n", + " prompt_embeds = self.text_encoder(text_input_ids)\n", + " prompt_embeds = prompt_embeds[0]\n", + " prompt_embeds = torch.tensor(prompt_embeds)\n", + "\n", + " bs_embed, seq_len, _ = prompt_embeds.shape\n", + " # duplicate text embeddings for each generation per prompt, using mps friendly method\n", + " prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # get unconditional embeddings for classifier free guidance\n", + " if do_classifier_free_guidance and negative_prompt_embeds is None:\n", + " uncond_tokens: List[str]\n", + " if negative_prompt is None:\n", + " uncond_tokens = [\"\"] * batch_size\n", + " elif type(prompt) is not type(negative_prompt):\n", + " raise TypeError(f\"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=\" f\" {type(prompt)}.\")\n", + " elif isinstance(negative_prompt, str):\n", + " uncond_tokens = [negative_prompt]\n", + " elif batch_size != len(negative_prompt):\n", + " raise ValueError(\n", + " f\"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:\"\n", + " f\" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches\"\n", + " \" the batch size of `prompt`.\"\n", + " )\n", + " else:\n", + " uncond_tokens = negative_prompt\n", + "\n", + " max_length = prompt_embeds.shape[1]\n", + " uncond_input = self.tokenizer(\n", + " uncond_tokens,\n", + " padding=\"max_length\",\n", + " max_length=max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + "\n", + " negative_prompt_embeds = self.text_encoder(uncond_input.input_ids)\n", + " negative_prompt_embeds = negative_prompt_embeds[0]\n", + " negative_prompt_embeds = torch.tensor(negative_prompt_embeds)\n", + "\n", + " if do_classifier_free_guidance:\n", + " # duplicate unconditional embeddings for each generation per prompt, using mps friendly method\n", + " seq_len = negative_prompt_embeds.shape[1]\n", + "\n", + " negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # For classifier free guidance, we need to do two forward passes.\n", + " # Here we concatenate the unconditional and text embeddings into a single batch\n", + " # to avoid doing two forward passes\n", + " prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])\n", + "\n", + " return prompt_embeds\n", + "\n", + " def prepare_latents(\n", + " self,\n", + " batch_size,\n", + " num_channels_latents,\n", + " dtype,\n", + " generator,\n", + " latents=None,\n", + " ):\n", + " shape = (\n", + " batch_size,\n", + " num_channels_latents,\n", + " self.num_frames,\n", + " self.height // self.vae_scale_factor,\n", + " self.width // self.vae_scale_factor,\n", + " )\n", + " if isinstance(generator, list) and len(generator) != batch_size:\n", + " raise ValueError(\n", + " f\"You have passed a list of generators of length {len(generator)}, but requested an effective batch\"\n", + " f\" size of {batch_size}. Make sure the batch size matches the length of the generators.\"\n", + " )\n", + "\n", + " if latents is None:\n", + " latents = randn_tensor(shape, generator=generator, dtype=dtype)\n", + "\n", + " # scale the initial noise by the standard deviation required by the scheduler\n", + " latents = latents * self.scheduler.init_noise_sigma\n", + " return latents\n", + "\n", + " def check_inputs(\n", + " self,\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt=None,\n", + " prompt_embeds=None,\n", + " negative_prompt_embeds=None,\n", + " ):\n", + " if self.height % 8 != 0 or self.width % 8 != 0:\n", + " raise ValueError(f\"`height` and `width` have to be divisible by 8 but are {self.height} and {self.width}.\")\n", + "\n", + " if (callback_steps is None) or (callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0)):\n", + " raise ValueError(f\"`callback_steps` has to be a positive integer but is {callback_steps} of type\" f\" {type(callback_steps)}.\")\n", + "\n", + " if prompt is not None and prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to\" \" only forward one of the two.\"\n", + " )\n", + " elif prompt is None and prompt_embeds is None:\n", + " raise ValueError(\"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined.\")\n", + " elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):\n", + " raise ValueError(f\"`prompt` has to be of type `str` or `list` but is {type(prompt)}\")\n", + "\n", + " if negative_prompt is not None and negative_prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:\"\n", + " f\" {negative_prompt_embeds}. Please make sure to only forward one of the two.\"\n", + " )\n", + "\n", + " if prompt_embeds is not None and negative_prompt_embeds is not None:\n", + " if prompt_embeds.shape != negative_prompt_embeds.shape:\n", + " raise ValueError(\n", + " \"`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but\"\n", + " f\" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`\"\n", + " f\" {negative_prompt_embeds.shape}.\"\n", + " )\n", + "\n", + " def decode_latents(self, latents):\n", + " scale_factor = 0.18215\n", + " latents = 1 / scale_factor * latents\n", + "\n", + " batch_size, channels, num_frames, height, width = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_frames, channels, height, width)\n", + " image = self.vae_decoder(latents)[0]\n", + " image = torch.tensor(image)\n", + " video = (\n", + " image[None, :]\n", + " .reshape(\n", + " (\n", + " batch_size,\n", + " num_frames,\n", + " -1,\n", + " )\n", + " + image.shape[2:]\n", + " )\n", + " .permute(0, 2, 1, 3, 4)\n", + " )\n", + " # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16\n", + " video = video.float()\n", + " return video" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Inference with OpenVINO\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "device = widgets.Dropdown(\n", + " options=core.available_devices + [\"AUTO\"],\n", + " value=\"AUTO\",\n", + " description=\"Device:\",\n", + " disabled=False,\n", + ")\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_unet = core.compile_model(unet_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_vae_decoder = core.compile_model(vae_decoder_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_text_encoder = core.compile_model(text_encoder_xml, device_name=device.value)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Here we replace the pipeline parts with versions converted to OpenVINO IR and compiled to specific device. Note that we use original pipeline tokenizer and scheduler." + ] + }, + { + "cell_type": "code", + "source": [ + "ov_pipe = OVTextToVideoSDPipeline(ov_vae_decoder, ov_text_encoder, tokenizer, ov_unet, scheduler)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Define a prompt\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "prompt = \"A panda eating bamboo on a rock.\"" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Let's generate a video for our prompt. For full list of arguments, see `__call__` function definition of `OVTextToVideoSDPipeline` class in [Build a pipeline](#Build-a-pipeline) section." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Video generation\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "frames = ov_pipe(prompt, num_inference_steps=25)[\"frames\"]" + ] + }, + { + "cell_type": "code", + "source": [ + "images = [PIL.Image.fromarray(frame) for frame in frames]\n", + "images[0].save(\"output.gif\", save_all=True, append_images=images[1:], duration=125, loop=0)\n", + "with open(\"output.gif\", \"rb\") as gif_file:\n", + " b64 = f\"data:image/gif;base64,{base64.b64encode(gif_file.read()).decode()}\"\n", + "IPython.display.HTML(f'')" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Interactive demo\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def generate(prompt, seed, num_inference_steps, _=gr.Progress(track_tqdm=True)):\n", + " generator = torch.Generator().manual_seed(seed)\n", + " frames = ov_pipe(\n", + " prompt,\n", + " num_inference_steps=num_inference_steps,\n", + " generator=generator,\n", + " )[\"frames\"]\n", + " out_file = tempfile.NamedTemporaryFile(suffix=\".gif\", delete=False)\n", + " images = [PIL.Image.fromarray(frame) for frame in frames]\n", + " images[0].save(out_file, save_all=True, append_images=images[1:], duration=125, loop=0)\n", + " return out_file.name\n", + "\n", + "\n", + "demo = gr.Interface(\n", + " generate,\n", + " [\n", + " gr.Textbox(label=\"Prompt\"),\n", + " gr.Slider(0, 1000000, value=42, label=\"Seed\", step=1),\n", + " gr.Slider(10, 50, value=25, label=\"Number of inference steps\", step=1),\n", + " ],\n", + " gr.Image(label=\"Result\"),\n", + " examples=[\n", + " [\"An astronaut riding a horse.\", 0, 25],\n", + " [\"A panda eating bamboo on a rock.\", 0, 25],\n", + " [\"Spiderman is surfing.\", 0, 25],\n", + " ],\n", + " allow_flagging=\"never\",\n", + ")\n", + "\n", + "try:\n", + " demo.queue().launch(debug=True)\n", + "except Exception:\n", + " demo.queue().launch(share=True, debug=True)\n", + "# if you are launching remotely, specify server_name and server_port\n", + "# demo.launch(server_name='your server name', server_port='server port in int')\n", + "# Read more in the docs: https://gradio.app/docs/" + ] + } + ] + .map(fromJupyterCell) + , + [ + { + "cell_type": "markdown", + "source": [ + "# Video generation with ZeroScope and OpenVINO\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Install and import required packages](#Install-and-import-required-packages)\n", + "- [Load the model](#Load-the-model)\n", + "- [Convert the model](#Convert-the-model)\n", + " - [Define the conversion function](#Define-the-conversion-function)\n", + " - [UNet](#UNet)\n", + " - [VAE](#VAE)\n", + " - [Text encoder](#Text-encoder)\n", + "- [Build a pipeline](#Build-a-pipeline)\n", + "- [Inference with OpenVINO](#Inference-with-OpenVINO)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Define a prompt](#Define-a-prompt)\n", + " - [Video generation](#Video-generation)\n", + "- [Interactive demo](#Interactive-demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The ZeroScope model is a free and open-source text-to-video model that can generate realistic and engaging videos from text descriptions. It is based on the [Modelscope](https://modelscope.cn/models/damo/text-to-video-synthesis/summary) model, but it has been improved to produce higher-quality videos with a 16:9 aspect ratio and no Shutterstock watermark. The ZeroScope model is available in two versions: ZeroScope_v2 576w, which is optimized for rapid content creation at a resolution of 576x320 pixels, and ZeroScope_v2 XL, which upscales videos to a high-definition resolution of 1024x576.\n", + "\n", + "The ZeroScope model is trained on a dataset of over 9,000 videos and 29,000 tagged frames. It uses a diffusion model to generate videos, which means that it starts with a random noise image and gradually adds detail to it until it matches the text description. The ZeroScope model is still under development, but it has already been used to create some impressive videos. For example, it has been used to create videos of people dancing, playing sports, and even driving cars.\n", + "\n", + "The ZeroScope model is a powerful tool that can be used to create various videos, from simple animations to complex scenes. It is still under development, but it has the potential to revolutionize the way we create and consume video content.\n", + "\n", + "Both versions of the ZeroScope model are available on Hugging Face:\n", + " - [ZeroScope_v2 576w](https://huggingface.co/cerspense/zeroscope_v2_576w)\n", + " - [ZeroScope_v2 XL](https://huggingface.co/cerspense/zeroscope_v2_XL)\n", + "\n", + "We will use the first one." + ] + }, + { + "cell_type": "markdown", + "source": [ + "
\n", + " This tutorial requires at least 24GB of free memory to generate a video with a frame size of 432x240 and 16 frames. Increasing either of these values will require more memory and take more time.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Install and import required packages\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "To work with text-to-video synthesis model, we will use Hugging Face's [Diffusers](https://github.com/huggingface/diffusers) library. It provides already pretrained model from `cerspense`." + ] + }, + { + "cell_type": "code", + "source": [ + "%pip install -q --extra-index-url https://download.pytorch.org/whl/cpu \"diffusers>=0.18.0\" \"torch>=2.1\" transformers \"openvino>=2023.1.0\" numpy \"gradio>=4.19\"" + ] + }, + { + "cell_type": "code", + "source": [ + "import gc\n", + "from typing import Optional, Union, List, Callable\n", + "import base64\n", + "import tempfile\n", + "import warnings\n", + "\n", + "import diffusers\n", + "import transformers\n", + "import numpy as np\n", + "import IPython\n", + "import torch\n", + "import PIL\n", + "import gradio as gr\n", + "\n", + "import openvino as ov" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Original 576x320 inference requires a lot of RAM (>100GB), so let's run our example on a smaller frame size, keeping the same aspect ratio. Try reducing values below to reduce the memory consumption." + ] + }, + { + "cell_type": "code", + "source": [ + "WIDTH = 432 # must be divisible by 8\n", + "HEIGHT = 240 # must be divisible by 8\n", + "NUM_FRAMES = 16" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Load the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The model is loaded from HuggingFace using `.from_pretrained` method of `diffusers.DiffusionPipeline`." + ] + }, + { + "cell_type": "code", + "source": [ + "pipe = diffusers.DiffusionPipeline.from_pretrained(\"cerspense/zeroscope_v2_576w\")" + ] + }, + { + "cell_type": "code", + "source": [ + "unet = pipe.unet\n", + "unet.eval()\n", + "vae = pipe.vae\n", + "vae.eval()\n", + "text_encoder = pipe.text_encoder\n", + "text_encoder.eval()\n", + "tokenizer = pipe.tokenizer\n", + "scheduler = pipe.scheduler\n", + "vae_scale_factor = pipe.vae_scale_factor\n", + "unet_in_channels = pipe.unet.config.in_channels\n", + "sample_width = WIDTH // vae_scale_factor\n", + "sample_height = HEIGHT // vae_scale_factor\n", + "del pipe\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Convert the model\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "The architecture for generating videos from text comprises three distinct sub-networks: one for extracting text features, another for translating text features into the video latent space using a diffusion model, and a final one for mapping the video latent space to the visual space. The collective parameters of the entire model amount to approximately 1.7 billion. It's capable of processing English input. The diffusion model is built upon the Unet3D model and achieves video generation by iteratively denoising a starting point of pure Gaussian noise video." + ] + }, + { + "cell_type": "markdown", + "source": [ + "![](data:image/png;base64,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6Vv1VWglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqtKCVdk7osejTpV6m273NOyd0WPRp0q9Tbd7mqrSglXZO6LHo06Veptu9zTsndFj0adKvU23e5qq0oJV2Tuix6NOlXqbbvc07J3RY9GnSr1Nt3uaqt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)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Define the conversion function\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Model components are PyTorch modules, that can be converted with `ov.convert_model` function directly. We also use `ov.save_model` function to serialize the result of conversion." + ] + }, + { + "cell_type": "code", + "source": [ + "warnings.filterwarnings(\"ignore\", category=torch.jit.TracerWarning)" + ] + }, + { + "cell_type": "code", + "source": [ + "from pathlib import Path\n", + "\n", + "\n", + "def convert(model: torch.nn.Module, xml_path: str, **convert_kwargs) -> Path:\n", + " xml_path = Path(xml_path)\n", + " if not xml_path.exists():\n", + " xml_path.parent.mkdir(parents=True, exist_ok=True)\n", + " with torch.no_grad():\n", + " converted_model = ov.convert_model(model, **convert_kwargs)\n", + " ov.save_model(converted_model, xml_path)\n", + " del converted_model\n", + " gc.collect()\n", + " torch._C._jit_clear_class_registry()\n", + " torch.jit._recursive.concrete_type_store = torch.jit._recursive.ConcreteTypeStore()\n", + " torch.jit._state._clear_class_state()\n", + " return xml_path" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### UNet\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text-to-video generation pipeline main component is a conditional 3D UNet model that takes a noisy sample, conditional state, and a timestep and returns a sample shaped output." + ] + }, + { + "cell_type": "code", + "source": [ + "unet_xml_path = convert(\n", + " unet,\n", + " \"models/unet.xml\",\n", + " example_input={\n", + " \"sample\": torch.randn(2, 4, 2, int(sample_height // 2), int(sample_width // 2)),\n", + " \"timestep\": torch.tensor(1),\n", + " \"encoder_hidden_states\": torch.randn(2, 77, 1024),\n", + " },\n", + " input=[\n", + " (\"sample\", (2, 4, NUM_FRAMES, sample_height, sample_width)),\n", + " (\"timestep\", ()),\n", + " (\"encoder_hidden_states\", (2, 77, 1024)),\n", + " ],\n", + ")\n", + "del unet\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### VAE\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Variational autoencoder (VAE) uses UNet output to decode latents to visual representations. Our VAE model has KL loss for encoding images into latents and decoding latent representations into images. For inference, we need only decoder part." + ] + }, + { + "cell_type": "code", + "source": [ + "class VaeDecoderWrapper(torch.nn.Module):\n", + " def __init__(self, vae):\n", + " super().__init__()\n", + " self.vae = vae\n", + "\n", + " def forward(self, z: torch.FloatTensor):\n", + " return self.vae.decode(z)" + ] + }, + { + "cell_type": "code", + "source": [ + "vae_decoder_xml_path = convert(\n", + " VaeDecoderWrapper(vae),\n", + " \"models/vae.xml\",\n", + " example_input=torch.randn(2, 4, 32, 32),\n", + " input=((NUM_FRAMES, 4, sample_height, sample_width)),\n", + ")\n", + "del vae\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Text encoder\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Text encoder is used to encode the input prompt to tensor. Default tensor length is 77." + ] + }, + { + "cell_type": "code", + "source": [ + "text_encoder_xml = convert(\n", + " text_encoder,\n", + " \"models/text_encoder.xml\",\n", + " example_input=torch.ones(1, 77, dtype=torch.int64),\n", + " input=((1, 77), ov.Type.i64),\n", + ")\n", + "del text_encoder\n", + "gc.collect();" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Build a pipeline\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def tensor2vid(video: torch.Tensor, mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) -> List[np.ndarray]:\n", + " # This code is copied from https://github.com/modelscope/modelscope/blob/1509fdb973e5871f37148a4b5e5964cafd43e64d/modelscope/pipelines/multi_modal/text_to_video_synthesis_pipeline.py#L78\n", + " # reshape to ncfhw\n", + " mean = torch.tensor(mean, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " std = torch.tensor(std, device=video.device).reshape(1, -1, 1, 1, 1)\n", + " # unnormalize back to [0,1]\n", + " video = video.mul_(std).add_(mean)\n", + " video.clamp_(0, 1)\n", + " # prepare the final outputs\n", + " i, c, f, h, w = video.shape\n", + " images = video.permute(2, 3, 0, 4, 1).reshape(f, h, i * w, c) # 1st (frames, h, batch_size, w, c) 2nd (frames, h, batch_size * w, c)\n", + " images = images.unbind(dim=0) # prepare a list of indvidual (consecutive frames)\n", + " images = [(image.cpu().numpy() * 255).astype(\"uint8\") for image in images] # f h w c\n", + " return images" + ] + }, + { + "cell_type": "code", + "source": [ + "try:\n", + " from diffusers.utils import randn_tensor\n", + "except ImportError:\n", + " from diffusers.utils.torch_utils import randn_tensor\n", + "\n", + "\n", + "class OVTextToVideoSDPipeline(diffusers.DiffusionPipeline):\n", + " def __init__(\n", + " self,\n", + " vae_decoder: ov.CompiledModel,\n", + " text_encoder: ov.CompiledModel,\n", + " tokenizer: transformers.CLIPTokenizer,\n", + " unet: ov.CompiledModel,\n", + " scheduler: diffusers.schedulers.DDIMScheduler,\n", + " ):\n", + " super().__init__()\n", + "\n", + " self.vae_decoder = vae_decoder\n", + " self.text_encoder = text_encoder\n", + " self.tokenizer = tokenizer\n", + " self.unet = unet\n", + " self.scheduler = scheduler\n", + " self.vae_scale_factor = vae_scale_factor\n", + " self.unet_in_channels = unet_in_channels\n", + " self.width = WIDTH\n", + " self.height = HEIGHT\n", + " self.num_frames = NUM_FRAMES\n", + "\n", + " def __call__(\n", + " self,\n", + " prompt: Union[str, List[str]] = None,\n", + " num_inference_steps: int = 50,\n", + " guidance_scale: float = 9.0,\n", + " negative_prompt: Optional[Union[str, List[str]]] = None,\n", + " eta: float = 0.0,\n", + " generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,\n", + " latents: Optional[torch.FloatTensor] = None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " output_type: Optional[str] = \"np\",\n", + " return_dict: bool = True,\n", + " callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,\n", + " callback_steps: int = 1,\n", + " ):\n", + " r\"\"\"\n", + " Function invoked when calling the pipeline for generation.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts to guide the video generation. If not defined, one has to pass `prompt_embeds`.\n", + " instead.\n", + " num_inference_steps (`int`, *optional*, defaults to 50):\n", + " The number of denoising steps. More denoising steps usually lead to a higher quality videos at the\n", + " expense of slower inference.\n", + " guidance_scale (`float`, *optional*, defaults to 7.5):\n", + " Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).\n", + " `guidance_scale` is defined as `w` of equation 2. of [Imagen\n", + " Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >\n", + " 1`. Higher guidance scale encourages to generate videos that are closely linked to the text `prompt`,\n", + " usually at the expense of lower video quality.\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the video generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " eta (`float`, *optional*, defaults to 0.0):\n", + " Corresponds to parameter eta (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to\n", + " [`schedulers.DDIMScheduler`], will be ignored for others.\n", + " generator (`torch.Generator` or `List[torch.Generator]`, *optional*):\n", + " One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)\n", + " to make generation deterministic.\n", + " latents (`torch.FloatTensor`, *optional*):\n", + " Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for video\n", + " generation. Can be used to tweak the same generation with different prompts. If not provided, a latents\n", + " tensor will ge generated by sampling using the supplied random `generator`. Latents should be of shape\n", + " `(batch_size, num_channel, num_frames, height, width)`.\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " output_type (`str`, *optional*, defaults to `\"np\"`):\n", + " The output format of the generate video. Choose between `torch.FloatTensor` or `np.array`.\n", + " return_dict (`bool`, *optional*, defaults to `True`):\n", + " Whether or not to return a [`~pipelines.stable_diffusion.TextToVideoSDPipelineOutput`] instead of a\n", + " plain tuple.\n", + " callback (`Callable`, *optional*):\n", + " A function that will be called every `callback_steps` steps during inference. The function will be\n", + " called with the following arguments: `callback(step: int, timestep: int, latents: torch.FloatTensor)`.\n", + " callback_steps (`int`, *optional*, defaults to 1):\n", + " The frequency at which the `callback` function will be called. If not specified, the callback will be\n", + " called at every step.\n", + "\n", + " Returns:\n", + " `List[np.ndarray]`: generated video frames\n", + " \"\"\"\n", + "\n", + " num_images_per_prompt = 1\n", + "\n", + " # 1. Check inputs. Raise error if not correct\n", + " self.check_inputs(\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt,\n", + " prompt_embeds,\n", + " negative_prompt_embeds,\n", + " )\n", + "\n", + " # 2. Define call parameters\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)\n", + " # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`\n", + " # corresponds to doing no classifier free guidance.\n", + " do_classifier_free_guidance = guidance_scale > 1.0\n", + "\n", + " # 3. Encode input prompt\n", + " prompt_embeds = self._encode_prompt(\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt,\n", + " prompt_embeds=prompt_embeds,\n", + " negative_prompt_embeds=negative_prompt_embeds,\n", + " )\n", + "\n", + " # 4. Prepare timesteps\n", + " self.scheduler.set_timesteps(num_inference_steps)\n", + " timesteps = self.scheduler.timesteps\n", + "\n", + " # 5. Prepare latent variables\n", + " num_channels_latents = self.unet_in_channels\n", + " latents = self.prepare_latents(\n", + " batch_size * num_images_per_prompt,\n", + " num_channels_latents,\n", + " prompt_embeds.dtype,\n", + " generator,\n", + " latents,\n", + " )\n", + "\n", + " # 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline\n", + " extra_step_kwargs = {\"generator\": generator, \"eta\": eta}\n", + "\n", + " # 7. Denoising loop\n", + " num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order\n", + " with self.progress_bar(total=num_inference_steps) as progress_bar:\n", + " for i, t in enumerate(timesteps):\n", + " # expand the latents if we are doing classifier free guidance\n", + " latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents\n", + " latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)\n", + "\n", + " # predict the noise residual\n", + " noise_pred = self.unet(\n", + " {\n", + " \"sample\": latent_model_input,\n", + " \"timestep\": t,\n", + " \"encoder_hidden_states\": prompt_embeds,\n", + " }\n", + " )[0]\n", + " noise_pred = torch.tensor(noise_pred)\n", + "\n", + " # perform guidance\n", + " if do_classifier_free_guidance:\n", + " noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)\n", + " noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)\n", + "\n", + " # reshape latents\n", + " bsz, channel, frames, width, height = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + " noise_pred = noise_pred.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channel, width, height)\n", + "\n", + " # compute the previous noisy sample x_t -> x_t-1\n", + " latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs).prev_sample\n", + "\n", + " # reshape latents back\n", + " latents = latents[None, :].reshape(bsz, frames, channel, width, height).permute(0, 2, 1, 3, 4)\n", + "\n", + " # call the callback, if provided\n", + " if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):\n", + " progress_bar.update()\n", + " if callback is not None and i % callback_steps == 0:\n", + " callback(i, t, latents)\n", + "\n", + " video_tensor = self.decode_latents(latents)\n", + "\n", + " if output_type == \"pt\":\n", + " video = video_tensor\n", + " else:\n", + " video = tensor2vid(video_tensor)\n", + "\n", + " if not return_dict:\n", + " return (video,)\n", + "\n", + " return {\"frames\": video}\n", + "\n", + " # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._encode_prompt\n", + " def _encode_prompt(\n", + " self,\n", + " prompt,\n", + " num_images_per_prompt,\n", + " do_classifier_free_guidance,\n", + " negative_prompt=None,\n", + " prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " negative_prompt_embeds: Optional[torch.FloatTensor] = None,\n", + " ):\n", + " r\"\"\"\n", + " Encodes the prompt into text encoder hidden states.\n", + "\n", + " Args:\n", + " prompt (`str` or `List[str]`, *optional*):\n", + " prompt to be encoded\n", + " num_images_per_prompt (`int`):\n", + " number of images that should be generated per prompt\n", + " do_classifier_free_guidance (`bool`):\n", + " whether to use classifier free guidance or not\n", + " negative_prompt (`str` or `List[str]`, *optional*):\n", + " The prompt or prompts not to guide the image generation. If not defined, one has to pass\n", + " `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is\n", + " less than `1`).\n", + " prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not\n", + " provided, text embeddings will be generated from `prompt` input argument.\n", + " negative_prompt_embeds (`torch.FloatTensor`, *optional*):\n", + " Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt\n", + " weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input\n", + " argument.\n", + " \"\"\"\n", + " if prompt is not None and isinstance(prompt, str):\n", + " batch_size = 1\n", + " elif prompt is not None and isinstance(prompt, list):\n", + " batch_size = len(prompt)\n", + " else:\n", + " batch_size = prompt_embeds.shape[0]\n", + "\n", + " if prompt_embeds is None:\n", + " text_inputs = self.tokenizer(\n", + " prompt,\n", + " padding=\"max_length\",\n", + " max_length=self.tokenizer.model_max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + " text_input_ids = text_inputs.input_ids\n", + " untruncated_ids = self.tokenizer(prompt, padding=\"longest\", return_tensors=\"pt\").input_ids\n", + "\n", + " if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):\n", + " removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1])\n", + " print(\n", + " \"The following part of your input was truncated because CLIP can only handle sequences up to\"\n", + " f\" {self.tokenizer.model_max_length} tokens: {removed_text}\"\n", + " )\n", + "\n", + " prompt_embeds = self.text_encoder(text_input_ids)\n", + " prompt_embeds = prompt_embeds[0]\n", + " prompt_embeds = torch.tensor(prompt_embeds)\n", + "\n", + " bs_embed, seq_len, _ = prompt_embeds.shape\n", + " # duplicate text embeddings for each generation per prompt, using mps friendly method\n", + " prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # get unconditional embeddings for classifier free guidance\n", + " if do_classifier_free_guidance and negative_prompt_embeds is None:\n", + " uncond_tokens: List[str]\n", + " if negative_prompt is None:\n", + " uncond_tokens = [\"\"] * batch_size\n", + " elif type(prompt) is not type(negative_prompt):\n", + " raise TypeError(f\"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !=\" f\" {type(prompt)}.\")\n", + " elif isinstance(negative_prompt, str):\n", + " uncond_tokens = [negative_prompt]\n", + " elif batch_size != len(negative_prompt):\n", + " raise ValueError(\n", + " f\"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:\"\n", + " f\" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches\"\n", + " \" the batch size of `prompt`.\"\n", + " )\n", + " else:\n", + " uncond_tokens = negative_prompt\n", + "\n", + " max_length = prompt_embeds.shape[1]\n", + " uncond_input = self.tokenizer(\n", + " uncond_tokens,\n", + " padding=\"max_length\",\n", + " max_length=max_length,\n", + " truncation=True,\n", + " return_tensors=\"pt\",\n", + " )\n", + "\n", + " negative_prompt_embeds = self.text_encoder(uncond_input.input_ids)\n", + " negative_prompt_embeds = negative_prompt_embeds[0]\n", + " negative_prompt_embeds = torch.tensor(negative_prompt_embeds)\n", + "\n", + " if do_classifier_free_guidance:\n", + " # duplicate unconditional embeddings for each generation per prompt, using mps friendly method\n", + " seq_len = negative_prompt_embeds.shape[1]\n", + "\n", + " negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1)\n", + " negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)\n", + "\n", + " # For classifier free guidance, we need to do two forward passes.\n", + " # Here we concatenate the unconditional and text embeddings into a single batch\n", + " # to avoid doing two forward passes\n", + " prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])\n", + "\n", + " return prompt_embeds\n", + "\n", + " def prepare_latents(\n", + " self,\n", + " batch_size,\n", + " num_channels_latents,\n", + " dtype,\n", + " generator,\n", + " latents=None,\n", + " ):\n", + " shape = (\n", + " batch_size,\n", + " num_channels_latents,\n", + " self.num_frames,\n", + " self.height // self.vae_scale_factor,\n", + " self.width // self.vae_scale_factor,\n", + " )\n", + " if isinstance(generator, list) and len(generator) != batch_size:\n", + " raise ValueError(\n", + " f\"You have passed a list of generators of length {len(generator)}, but requested an effective batch\"\n", + " f\" size of {batch_size}. Make sure the batch size matches the length of the generators.\"\n", + " )\n", + "\n", + " if latents is None:\n", + " latents = randn_tensor(shape, generator=generator, dtype=dtype)\n", + "\n", + " # scale the initial noise by the standard deviation required by the scheduler\n", + " latents = latents * self.scheduler.init_noise_sigma\n", + " return latents\n", + "\n", + " def check_inputs(\n", + " self,\n", + " prompt,\n", + " callback_steps,\n", + " negative_prompt=None,\n", + " prompt_embeds=None,\n", + " negative_prompt_embeds=None,\n", + " ):\n", + " if self.height % 8 != 0 or self.width % 8 != 0:\n", + " raise ValueError(f\"`height` and `width` have to be divisible by 8 but are {self.height} and {self.width}.\")\n", + "\n", + " if (callback_steps is None) or (callback_steps is not None and (not isinstance(callback_steps, int) or callback_steps <= 0)):\n", + " raise ValueError(f\"`callback_steps` has to be a positive integer but is {callback_steps} of type\" f\" {type(callback_steps)}.\")\n", + "\n", + " if prompt is not None and prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to\" \" only forward one of the two.\"\n", + " )\n", + " elif prompt is None and prompt_embeds is None:\n", + " raise ValueError(\"Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined.\")\n", + " elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):\n", + " raise ValueError(f\"`prompt` has to be of type `str` or `list` but is {type(prompt)}\")\n", + "\n", + " if negative_prompt is not None and negative_prompt_embeds is not None:\n", + " raise ValueError(\n", + " f\"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:\"\n", + " f\" {negative_prompt_embeds}. Please make sure to only forward one of the two.\"\n", + " )\n", + "\n", + " if prompt_embeds is not None and negative_prompt_embeds is not None:\n", + " if prompt_embeds.shape != negative_prompt_embeds.shape:\n", + " raise ValueError(\n", + " \"`prompt_embeds` and `negative_prompt_embeds` must have the same shape when passed directly, but\"\n", + " f\" got: `prompt_embeds` {prompt_embeds.shape} != `negative_prompt_embeds`\"\n", + " f\" {negative_prompt_embeds.shape}.\"\n", + " )\n", + "\n", + " def decode_latents(self, latents):\n", + " scale_factor = 0.18215\n", + " latents = 1 / scale_factor * latents\n", + "\n", + " batch_size, channels, num_frames, height, width = latents.shape\n", + " latents = latents.permute(0, 2, 1, 3, 4).reshape(batch_size * num_frames, channels, height, width)\n", + " image = self.vae_decoder(latents)[0]\n", + " image = torch.tensor(image)\n", + " video = (\n", + " image[None, :]\n", + " .reshape(\n", + " (\n", + " batch_size,\n", + " num_frames,\n", + " -1,\n", + " )\n", + " + image.shape[2:]\n", + " )\n", + " .permute(0, 2, 1, 3, 4)\n", + " )\n", + " # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16\n", + " video = video.float()\n", + " return video" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Inference with OpenVINO\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "core = ov.Core()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "source": [ + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "from notebook_utils import device_widget\n", + "\n", + "device = device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_unet = core.compile_model(unet_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_vae_decoder = core.compile_model(vae_decoder_xml_path, device_name=device.value)" + ] + }, + { + "cell_type": "code", + "source": [ + "%%time\n", + "ov_text_encoder = core.compile_model(text_encoder_xml, device_name=device.value)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Here we replace the pipeline parts with versions converted to OpenVINO IR and compiled to specific device. Note that we use original pipeline tokenizer and scheduler." + ] + }, + { + "cell_type": "code", + "source": [ + "ov_pipe = OVTextToVideoSDPipeline(ov_vae_decoder, ov_text_encoder, tokenizer, ov_unet, scheduler)" + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Define a prompt\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "prompt = \"A panda eating bamboo on a rock.\"" + ] + }, + { + "cell_type": "markdown", + "source": [ + "Let's generate a video for our prompt. For full list of arguments, see `__call__` function definition of `OVTextToVideoSDPipeline` class in [Build a pipeline](#Build-a-pipeline) section." + ] + }, + { + "cell_type": "markdown", + "source": [ + "### Video generation\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "frames = ov_pipe(prompt, num_inference_steps=25)[\"frames\"]" + ] + }, + { + "cell_type": "code", + "source": [ + "images = [PIL.Image.fromarray(frame) for frame in frames]\n", + "images[0].save(\"output.gif\", save_all=True, append_images=images[1:], duration=125, loop=0)\n", + "with open(\"output.gif\", \"rb\") as gif_file:\n", + " b64 = f\"data:image/gif;base64,{base64.b64encode(gif_file.read()).decode()}\"\n", + "IPython.display.HTML(f'')" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## Interactive demo\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "code", + "source": [ + "def generate(prompt, seed, num_inference_steps, _=gr.Progress(track_tqdm=True)):\n", + " generator = torch.Generator().manual_seed(seed)\n", + " frames = ov_pipe(\n", + " prompt,\n", + " num_inference_steps=num_inference_steps,\n", + " generator=generator,\n", + " )[\"frames\"]\n", + " out_file = tempfile.NamedTemporaryFile(suffix=\".gif\", delete=False)\n", + " images = [PIL.Image.fromarray(frame) for frame in frames]\n", + " images[0].save(out_file, save_all=True, append_images=images[1:], duration=125, loop=0)\n", + " return out_file.name\n", + "\n", + "\n", + "demo = gr.Interface(\n", + " generate,\n", + " [\n", + " gr.Textbox(label=\"Prompt\"),\n", + " gr.Slider(0, 1000000, value=42, label=\"Seed\", step=1),\n", + " gr.Slider(10, 50, value=25, label=\"Number of inference steps\", step=1),\n", + " ],\n", + " gr.Image(label=\"Result\"),\n", + " examples=[\n", + " [\"An astronaut riding a horse.\", 0, 25],\n", + " [\"A panda eating bamboo on a rock.\", 0, 25],\n", + " [\"Spiderman is surfing.\", 0, 25],\n", + " ],\n", + " allow_flagging=\"never\",\n", + ")\n", + "\n", + "try:\n", + " demo.queue().launch(debug=True)\n", + "except Exception:\n", + " demo.queue().launch(share=True, debug=True)\n", + "# if you are launching remotely, specify server_name and server_port\n", + "# demo.launch(server_name='your server name', server_port='server port in int')\n", + "# Read more in the docs: https://gradio.app/docs/" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + { modified: 24, original: 24 }, + { modified: 25, original: 25 }, + { modified: 26, original: 26 }, + { modified: 27, original: 27 }, + { modified: 28, original: 28 }, + { modified: 29, original: 29 }, + { modified: 30, original: 30 }, + { modified: 31, original: 31 }, + { modified: 32, original: 32 }, + { modified: 33, original: 33 }, + { modified: 34, original: 34 }, + { modified: 35, original: 35 }, + { modified: 36, original: 36 }, + { modified: 37, original: 37 }, + { modified: 38, original: 38 }, + { modified: 39, original: 39 }, + { modified: 40, original: 40 }, + { modified: 41, original: 41 }, + { modified: 42, original: 42 }, + { modified: 43, original: 43 }, + { modified: 44, original: 44 }, + { modified: 45, original: 45 }, + { modified: 46, original: 46 }, + { modified: 47, original: 47 }, + { modified: 48, original: 48 }, + { modified: 49, original: 49 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 6, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 15, originalLength: 1, modifiedStart: 15, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 36, originalLength: 1, modifiedStart: 36, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 50, originalLength: 0, modifiedStart: 50, modifiedLength: 2 } satisfies IDiffChange, + ]); + + }); + test('Modification of multiple cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "id": "a2e7d62b-5779-4211-822c-457c77321f8b", + "source": [ + "# Convert and Optimize YOLOv11 instance segmentation model with OpenVINO™\n", + "\n", + "Instance segmentation goes a step further than object detection and involves identifying individual objects in an image and segmenting them from the rest of the image. Instance segmentation as an object detection are often used as key components in computer vision systems. \n", + "Applications that use real-time instance segmentation models include video analytics, robotics, autonomous vehicles, multi-object tracking and object counting, medical image analysis, and many others.\n", + "\n", + "\n", + "This tutorial demonstrates step-by-step instructions on how to run and optimize PyTorch YOLOv11 with OpenVINO. We consider the steps required for instance segmentation scenario. You can find more details about model on [model page](https://docs.ultralytics.com/models/yolo11/) in Ultralytics documentation.\n", + "\n", + "The tutorial consists of the following steps:\n", + "- Prepare the PyTorch model.\n", + "- Download and prepare a dataset.\n", + "- Validate the original model.\n", + "- Convert the PyTorch model to OpenVINO IR.\n", + "- Validate the converted model.\n", + "- Prepare and run optimization pipeline.\n", + "- Compare performance of the FP32 and quantized models.\n", + "- Compare accuracy of the FP32 and quantized models.\n", + "- Live demo\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Get PyTorch model](#Get-PyTorch-model)\n", + " - [Prerequisites](#Prerequisites)\n", + "- [Instantiate model](#Instantiate-model)\n", + " - [Convert model to OpenVINO IR](#Convert-model-to-OpenVINO-IR)\n", + " - [Verify model inference](#Verify-model-inference)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Test on single image](#Test-on-single-image)\n", + "- [Optimize model using NNCF Post-training Quantization API](#Optimize-model-using-NNCF-Post-training-Quantization-API)\n", + " - [Validate Quantized model inference](#Validate-Quantized-model-inference)\n", + "- [Compare the Original and Quantized Models](#Compare-the-Original-and-Quantized-Models)\n", + " - [Compare performance of the Original and Quantized Models](#Compare-performance-of-the-Original-and-Quantized-Models)\n", + "- [Other ways to optimize model](#Other-ways-to-optimize-model)\n", + "- [Live demo](#Live-demo)\n", + " - [Run Live Object Detection and Segmentation](#Run-Live-Object-Detection-and-Segmentation)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "d7a12678-b12f-48d1-9735-398855733e46", + "source": [ + "## Get PyTorch model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Generally, PyTorch models represent an instance of the [`torch.nn.Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html) class, initialized by a state dictionary with model weights.\n", + "We will use the YOLOv11 nano model (also known as `yolo11n-seg`) pre-trained on a COCO dataset, which is available in this [repo](https://github.com/ultralytics/ultralytics). Similar steps are also applicable to other YOLOv11 models.\n", + "Typical steps to obtain a pre-trained model:\n", + "1. Create an instance of a model class.\n", + "2. Load a checkpoint state dict, which contains the pre-trained model weights.\n", + "3. Turn the model to evaluation for switching some operations to inference mode.\n", + "\n", + "In this case, the creators of the model provide an API that enables converting the YOLOv11 model to OpenVINO IR. Therefore, we do not need to do these steps manually." + ] + }, + { + "cell_type": "markdown", + "id": "e2267760-cbfe-41c6-958d-cad9f845d5bb", + "source": [ + "#### Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Install necessary packages." + ] + }, + { + "cell_type": "code", + "id": "30d04872-6916-454c-9211-6c644b50dc04", + "source": [ + "%pip install -q \"openvino>=2024.0.0\" \"nncf>=2.9.0\"\n", + "%pip install -q \"torch>=2.1\" \"torchvision>=0.16\" \"ultralytics==8.3.0\" opencv-python tqdm --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "markdown", + "id": "1bbe319c", + "source": [ + "Import required utility functions.\n", + "The lower cell will download the `notebook_utils` Python module from GitHub." + ] + }, + { + "cell_type": "code", + "id": "a2f6cd89", + "source": [ + "from pathlib import Path\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "from notebook_utils import download_file, VideoPlayer, device_widget" + ] + }, + { + "cell_type": "code", + "id": "373658bd-7e64-4479-914e-f2742d330afd", + "source": [ + "# Download a test sample\n", + "IMAGE_PATH = Path(\"./data/coco_bike.jpg\")\n", + "download_file(\n", + " url=\"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/image/coco_bike.jpg\",\n", + " filename=IMAGE_PATH.name,\n", + " directory=IMAGE_PATH.parent,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ee32fd08-650c-4751-bb41-d8afccb2495e", + "source": [ + "## Instantiate model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "For loading the model, required to specify a path to the model checkpoint. It can be some local path or name available on models hub (in this case model checkpoint will be downloaded automatically). \n", + "You can select model using widget bellow:" + ] + }, + { + "cell_type": "code", + "id": "7c3963a5-b50b-4ffe-b710-ccd1f4a65380", + "source": [ + "import ipywidgets as widgets\n", + "\n", + "model_id = [\n", + " \"yolo11n-seg\",\n", + " \"yolo11s-seg\",\n", + " \"yolo11m-seg\",\n", + " \"yolo11l-seg\",\n", + " \"yolo11x-seg\",\n", + " \"yolov8n-seg\",\n", + " \"yolov8s-seg\",\n", + " \"yolov8m-seg\",\n", + " \"yolov8l-seg\",\n", + " \"yolov8x-seg\",\n", + "]\n", + "\n", + "model_name = widgets.Dropdown(options=model_id, value=model_id[0], description=\"Model\")\n", + "\n", + "model_name" + ] + }, + { + "cell_type": "markdown", + "id": "60105d86-d7e0-47a9-ad88-4fff014ba3d8", + "source": [ + "Making prediction, the model accepts a path to input image and returns list with Results class object. Results contains boxes for object detection model and boxes and masks for segmentation model. Also it contains utilities for processing results, for example, `plot()` method for drawing.\n", + "\n", + "Let us consider the examples:" + ] + }, + { + "cell_type": "code", + "id": "d4994e1e-a3ee-4620-bc8b-237a55c47742", + "source": [ + "from PIL import Image\n", + "from ultralytics import YOLO\n", + "\n", + "SEG_MODEL_NAME = model_name.value\n", + "\n", + "seg_model = YOLO(f\"{SEG_MODEL_NAME}.pt\")\n", + "label_map = seg_model.model.names\n", + "\n", + "res = seg_model(IMAGE_PATH)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "e345ffcc-c4b8-44ba-8b03-f37e63a060da", + "source": [ + "### Convert model to OpenVINO IR\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Ultralytics provides API for convenient model exporting to different formats including OpenVINO IR. `model.export` is responsible for model conversion. We need to specify the format, and additionally, we can preserve dynamic shapes in the model." + ] + }, + { + "cell_type": "code", + "id": "5c3ef363-f6a1-4fe5-b7ff-eb5a9c0789f1", + "source": [ + "# instance segmentation model\n", + "seg_model_path = Path(f\"{SEG_MODEL_NAME}_openvino_model/{SEG_MODEL_NAME}.xml\")\n", + "if not seg_model_path.exists():\n", + " seg_model.export(format=\"openvino\", dynamic=True, half=True)" + ] + }, + { + "cell_type": "markdown", + "id": "713cd45f-2b19-4a1e-bc9d-5e69bd95d896", + "source": [ + "### Verify model inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We can reuse the base model pipeline for pre- and postprocessing just replacing the inference method where we will use the IR model for inference." + ] + }, + { + "cell_type": "markdown", + "id": "f9b9c472", + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "id": "4e0652b2", + "source": [ + "device = device_widget()\n", + "\n", + "device" + ] + }, + { + "cell_type": "markdown", + "id": "cd163eab-e803-4ae8-96da-22c3bf303630", + "source": [ + "### Test on single image\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "34a65afa-4201-428b-af11-b35e0d206e93", + "source": [ + "import openvino as ov\n", + "\n", + "core = ov.Core()\n", + "seg_ov_model = core.read_model(seg_model_path)\n", + "\n", + "ov_config = {}\n", + "if device.value != \"CPU\":\n", + " seg_ov_model.reshape({0: [1, 3, 640, 640]})\n", + "if \"GPU\" in device.value or (\"AUTO\" in device.value and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "seg_compiled_model = core.compile_model(seg_ov_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "id": "389a4698-b7fc-4aa2-9833-9681f575d88b", + "source": [ + "seg_model = YOLO(seg_model_path.parent, task=\"segment\")\n", + "\n", + "if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + "seg_model.predictor.model.ov_compiled_model = seg_compiled_model" + ] + }, + { + "cell_type": "code", + "id": "12d2522f-e939-4512-b74a-3e20baf1edc8", + "source": [ + "res = seg_model(IMAGE_PATH)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "7e918cf6-cdce-4e90-a6d7-c435a3c08d93", + "source": [ + "Great! The result is the same, as produced by original models." + ] + }, + { + "cell_type": "markdown", + "id": "13b69e12-2e22-44c1-bfed-fdc88f6c424d", + "source": [ + "## Optimize model using NNCF Post-training Quantization API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "[NNCF](https://github.com/openvinotoolkit/nncf) provides a suite of advanced algorithms for Neural Networks inference optimization in OpenVINO with minimal accuracy drop.\n", + "We will use 8-bit quantization in post-training mode (without the fine-tuning pipeline) to optimize YOLOv11.\n", + "\n", + "The optimization process contains the following steps:\n", + "\n", + "1. Create a Dataset for quantization.\n", + "2. Run `nncf.quantize` for getting an optimized model.\n", + "3. Serialize OpenVINO IR model, using the `openvino.runtime.serialize` function." + ] + }, + { + "cell_type": "markdown", + "id": "7d7eeae8-f3f4-4e95-b1a6-a23085f03ebc", + "source": [ + "Please select below whether you would like to run quantization to improve model inference speed." + ] + }, + { + "cell_type": "code", + "id": "4690b9fc-c49f-4882-b8a5-c82f842a9126", + "source": [ + "import ipywidgets as widgets\n", + "\n", + "int8_model_seg_path = Path(f\"{SEG_MODEL_NAME}_openvino_int8_model/{SEG_MODEL_NAME}.xml\")\n", + "quantized_seg_model = None\n", + "\n", + "to_quantize = widgets.Checkbox(\n", + " value=True,\n", + " description=\"Quantization\",\n", + " disabled=False,\n", + ")\n", + "\n", + "to_quantize" + ] + }, + { + "cell_type": "markdown", + "id": "73cfdbd4-3b65-49e0-b8ab-ceb1d0362481", + "source": [ + "Let's load `skip magic` extension to skip quantization if `to_quantize` is not selected" + ] + }, + { + "cell_type": "code", + "id": "bc6121aa-3a07-4066-a123-fe0ed0e72478", + "source": [ + "# Fetch skip_kernel_extension module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/skip_kernel_extension.py\",\n", + ")\n", + "open(\"skip_kernel_extension.py\", \"w\").write(r.text)\n", + "\n", + "%load_ext skip_kernel_extension" + ] + }, + { + "cell_type": "markdown", + "id": "94fb2515-3645-4798-bc7c-082129ac86c0", + "source": [ + "Reuse validation dataloader in accuracy testing for quantization. \n", + "For that, it should be wrapped into the `nncf.Dataset` object and define a transformation function for getting only input tensors." + ] + }, + { + "cell_type": "code", + "id": "fd994958-6988-4a1d-ac7f-3efbd97135cc", + "source": [ + "# %%skip not $to_quantize.value\n", + "\n", + "\n", + "import nncf\n", + "from typing import Dict\n", + "\n", + "from zipfile import ZipFile\n", + "\n", + "from ultralytics.data.utils import DATASETS_DIR\n", + "from ultralytics.utils import DEFAULT_CFG\n", + "from ultralytics.cfg import get_cfg\n", + "from ultralytics.data.converter import coco80_to_coco91_class\n", + "from ultralytics.data.utils import check_det_dataset\n", + "from ultralytics.utils import ops\n", + "\n", + "\n", + "if not int8_model_seg_path.exists():\n", + " DATA_URL = \"http://images.cocodataset.org/zips/val2017.zip\"\n", + " LABELS_URL = \"https://github.com/ultralytics/yolov5/releases/download/v1.0/coco2017labels-segments.zip\"\n", + " CFG_URL = \"https://raw.githubusercontent.com/ultralytics/ultralytics/v8.1.0/ultralytics/cfg/datasets/coco.yaml\"\n", + "\n", + " OUT_DIR = DATASETS_DIR\n", + "\n", + " DATA_PATH = OUT_DIR / \"val2017.zip\"\n", + " LABELS_PATH = OUT_DIR / \"coco2017labels-segments.zip\"\n", + " CFG_PATH = OUT_DIR / \"coco.yaml\"\n", + "\n", + " download_file(DATA_URL, DATA_PATH.name, DATA_PATH.parent)\n", + " download_file(LABELS_URL, LABELS_PATH.name, LABELS_PATH.parent)\n", + " download_file(CFG_URL, CFG_PATH.name, CFG_PATH.parent)\n", + "\n", + " if not (OUT_DIR / \"coco/labels\").exists():\n", + " with ZipFile(LABELS_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR)\n", + " with ZipFile(DATA_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR / \"coco/images\")\n", + "\n", + " args = get_cfg(cfg=DEFAULT_CFG)\n", + " args.data = str(CFG_PATH)\n", + " seg_validator = seg_model.task_map[seg_model.task][\"validator\"](args=args)\n", + " seg_validator.data = check_det_dataset(args.data)\n", + " seg_validator.stride = 32\n", + " seg_data_loader = seg_validator.get_dataloader(OUT_DIR / \"coco/\", 1)\n", + "\n", + " seg_validator.is_coco = True\n", + " seg_validator.class_map = coco80_to_coco91_class()\n", + " seg_validator.names = label_map\n", + " seg_validator.metrics.names = seg_validator.names\n", + " seg_validator.nc = 80\n", + " seg_validator.nm = 32\n", + " seg_validator.process = ops.process_mask\n", + " seg_validator.plot_masks = []\n", + "\n", + " def transform_fn(data_item: Dict):\n", + " \"\"\"\n", + " Quantization transform function. Extracts and preprocess input data from dataloader item for quantization.\n", + " Parameters:\n", + " data_item: Dict with data item produced by DataLoader during iteration\n", + " Returns:\n", + " input_tensor: Input data for quantization\n", + " \"\"\"\n", + " input_tensor = seg_validator.preprocess(data_item)[\"img\"].numpy()\n", + " return input_tensor\n", + "\n", + " quantization_dataset = nncf.Dataset(seg_data_loader, transform_fn)" + ] + }, + { + "cell_type": "markdown", + "id": "91629284-e261-494d-9464-146ed7190084", + "source": [ + "The `nncf.quantize` function provides an interface for model quantization. It requires an instance of the OpenVINO Model and quantization dataset. \n", + "Optionally, some additional parameters for the configuration quantization process (number of samples for quantization, preset, ignored scope, etc.) can be provided. Ultralytics models contain non-ReLU activation functions, which require asymmetric quantization of activations. To achieve a better result, we will use a `mixed` quantization preset. It provides symmetric quantization of weights and asymmetric quantization of activations. For more accurate results, we should keep the operation in the postprocessing subgraph in floating point precision, using the `ignored_scope` parameter.\n", + "\n", + ">**Note**: Model post-training quantization is time-consuming process. Be patient, it can take several minutes depending on your hardware." + ] + }, + { + "cell_type": "code", + "id": "b2e8cec4-d0b3-4da0-b54c-7964e7bcbfe2", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "if not int8_model_seg_path.exists():\n", + " ignored_scope = nncf.IgnoredScope( # post-processing\n", + " subgraphs=[\n", + " nncf.Subgraph(inputs=[f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_1\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_2\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_7\"],\n", + " outputs=[f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_8\"])\n", + " ]\n", + " )\n", + "\n", + " # Segmentation model\n", + " quantized_seg_model = nncf.quantize(\n", + " seg_ov_model,\n", + " quantization_dataset,\n", + " preset=nncf.QuantizationPreset.MIXED,\n", + " ignored_scope=ignored_scope\n", + " )\n", + "\n", + " print(f\"Quantized segmentation model will be saved to {int8_model_seg_path}\")\n", + " ov.save_model(quantized_seg_model, str(int8_model_seg_path))" + ] + }, + { + "cell_type": "markdown", + "id": "c8c92a68-3eb8-4ea5-9e35-d496f45b3cc3", + "source": [ + "### Validate Quantized model inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "`nncf.quantize` returns the OpenVINO Model class instance, which is suitable for loading on a device for making predictions. `INT8` model input data and output result formats have no difference from the floating point model representation. Therefore, we can reuse the same `detect` function defined above for getting the `INT8` model result on the image." + ] + }, + { + "cell_type": "code", + "id": "4003a3ae", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "711200ec-2f26-47cc-b62d-3802961d5711", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "if quantized_seg_model is None:\n", + " quantized_seg_model = core.read_model(int8_model_seg_path)\n", + "\n", + "ov_config = {}\n", + "if device.value != \"CPU\":\n", + " quantized_seg_model.reshape({0: [1, 3, 640, 640]})\n", + "if \"GPU\" in device.value or (\"AUTO\" in device.value and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "\n", + "quantized_seg_compiled_model = core.compile_model(quantized_seg_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "id": "b2e3ea3b-5935-4474-9f08-f51fc688b9c0", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "\n", + "if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + "seg_model.predictor.model.ov_compiled_model = quantized_seg_compiled_model" + ] + }, + { + "cell_type": "code", + "id": "bef2ed5d-5762-41a6-af2e-ee4f9fd56557", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "res = seg_model(IMAGE_PATH)\n", + "display(Image.fromarray(res[0].plot()[:, :, ::-1]))" + ] + }, + { + "cell_type": "markdown", + "id": "b289f057", + "source": [ + "## Compare the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "cell_type": "markdown", + "id": "59081cae-9c92-46f0-8117-ed8c6fa6ff19", + "source": [ + "### Compare performance of the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "Finally, use the OpenVINO [Benchmark Tool](https://docs.openvino.ai/2024/learn-openvino/openvino-samples/benchmark-tool.html) to measure the inference performance of the `FP32` and `INT8` models.\n", + "\n", + "> **Note**: For more accurate performance, it is recommended to run `benchmark_app` in a terminal/command prompt after closing other applications. Run `benchmark_app -m -d CPU -shape \"\"` to benchmark async inference on CPU on specific input data shape for one minute. Change `CPU` to `GPU` to benchmark on GPU. Run `benchmark_app --help` to see an overview of all command-line options." + ] + }, + { + "cell_type": "code", + "id": "b8677df4", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "546828d2", + "source": [ + "if int8_model_seg_path.exists():\n", + " !benchmark_app -m $seg_model_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "code", + "id": "1d43554b", + "source": [ + "if int8_model_seg_path.exists():\n", + " !benchmark_app -m $int8_model_seg_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "markdown", + "id": "184bcebd-588d-4b4d-a60a-0fa753a07ef9", + "source": [ + "## Other ways to optimize model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The performance could be also improved by another OpenVINO method such as async inference pipeline or preprocessing API.\n", + "\n", + "Async Inference pipeline help to utilize the device more optimal. The key advantage of the Async API is that when a device is busy with inference, the application can perform other tasks in parallel (for example, populating inputs or scheduling other requests) rather than wait for the current inference to complete first. To understand how to perform async inference using openvino, refer to [Async API tutorial](../async-api/async-api.ipynb)\n", + "\n", + "Preprocessing API enables making preprocessing a part of the model reducing application code and dependency on additional image processing libraries. \n", + "The main advantage of Preprocessing API is that preprocessing steps will be integrated into the execution graph and will be performed on a selected device (CPU/GPU etc.) rather than always being executed on CPU as part of an application. This will also improve selected device utilization. For more information, refer to the overview of [Preprocessing API tutorial](../optimize-preprocessing/optimize-preprocessing.ipynb). To see, how it could be used with YOLOV8 object detection model, please, see [Convert and Optimize YOLOv8 real-time object detection with OpenVINO tutorial](./yolov8-object-detection.ipynb)" + ] + }, + { + "cell_type": "markdown", + "id": "2e3b4862-c182-4ce4-a473-9f38d98deab8", + "source": [ + "## Live demo\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The following code runs model inference on a video:" + ] + }, + { + "cell_type": "code", + "id": "4c7bb92e-e301-45a9-b5ff-f7953fad298f", + "source": [ + "import collections\n", + "import time\n", + "import cv2\n", + "from IPython import display\n", + "import numpy as np\n", + "\n", + "\n", + "def run_instance_segmentation(\n", + " source=0,\n", + " flip=False,\n", + " use_popup=False,\n", + " skip_first_frames=0,\n", + " model=seg_model,\n", + " device=device.value,\n", + "):\n", + " player = None\n", + "\n", + " ov_config = {}\n", + " if device != \"CPU\":\n", + " model.reshape({0: [1, 3, 640, 640]})\n", + " if \"GPU\" in device or (\"AUTO\" in device and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + " compiled_model = core.compile_model(model, device, ov_config)\n", + "\n", + " if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + " seg_model.predictor.model.ov_compiled_model = compiled_model\n", + "\n", + " try:\n", + " # Create a video player to play with target fps.\n", + " player = VideoPlayer(source=source, flip=flip, fps=30, skip_first_frames=skip_first_frames)\n", + " # Start capturing.\n", + " player.start()\n", + " if use_popup:\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(winname=title, flags=cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_AUTOSIZE)\n", + "\n", + " processing_times = collections.deque()\n", + " while True:\n", + " # Grab the frame.\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + " # If the frame is larger than full HD, reduce size to improve the performance.\n", + " scale = 1280 / max(frame.shape)\n", + " if scale < 1:\n", + " frame = cv2.resize(\n", + " src=frame,\n", + " dsize=None,\n", + " fx=scale,\n", + " fy=scale,\n", + " interpolation=cv2.INTER_AREA,\n", + " )\n", + " # Get the results.\n", + " input_image = np.array(frame)\n", + "\n", + " start_time = time.time()\n", + " detections = seg_model(input_image)\n", + " stop_time = time.time()\n", + " frame = detections[0].plot()\n", + "\n", + " processing_times.append(stop_time - start_time)\n", + " # Use processing times from last 200 frames.\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " _, f_width = frame.shape[:2]\n", + " # Mean processing time [ms].\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + " cv2.putText(\n", + " img=frame,\n", + " text=f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " org=(20, 40),\n", + " fontFace=cv2.FONT_HERSHEY_COMPLEX,\n", + " fontScale=f_width / 1000,\n", + " color=(0, 0, 255),\n", + " thickness=1,\n", + " lineType=cv2.LINE_AA,\n", + " )\n", + " # Use this workaround if there is flickering.\n", + " if use_popup:\n", + " cv2.imshow(winname=title, mat=frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # Encode numpy array to jpg.\n", + " _, encoded_img = cv2.imencode(ext=\".jpg\", img=frame, params=[cv2.IMWRITE_JPEG_QUALITY, 100])\n", + " # Create an IPython image.\n", + " i = display.Image(data=encoded_img)\n", + " # Display the image in this notebook.\n", + " display.clear_output(wait=True)\n", + " display.display(i)\n", + " # ctrl-c\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " # any different error\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " if player is not None:\n", + " # Stop capturing.\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()" + ] + }, + { + "cell_type": "markdown", + "id": "cc5b4ba6-f478-4417-b09d-93fee5adca41", + "source": [ + "### Run Live Object Detection and Segmentation\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Use a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + ">**NOTE**: To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a remote server (for example, in Binder or Google Colab service), the webcam will not work. By default, the lower cell will run model inference on a video file. If you want to try live inference on your webcam set `WEBCAM_INFERENCE = True`" + ] + }, + { + "cell_type": "code", + "id": "90708017", + "source": [ + "WEBCAM_INFERENCE = False\n", + "\n", + "if WEBCAM_INFERENCE:\n", + " VIDEO_SOURCE = 0 # Webcam\n", + "else:\n", + " VIDEO_SOURCE = \"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/video/people.mp4\"" + ] + }, + { + "cell_type": "code", + "id": "e8f1239c", + "source": [ + "device" + ] + }, + { + "cell_type": "code", + "id": "440f2d0e", + "source": [ + "run_instance_segmentation(\n", + " source=VIDEO_SOURCE,\n", + " flip=True,\n", + " use_popup=False,\n", + " model=seg_ov_model,\n", + " device=device.value,\n", + ")" + ] + } + ] + .map(fromJupyterCell) + , + [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "a2e7d62b-5779-4211-822c-457c77321f8b", + "metadata": {}, + "source": [ + "# Convert and Optimize YOLOv11 instance segmentation model with OpenVINO™\n", + "\n", + "Instance segmentation goes a step further than object detection and involves identifying individual objects in an image and segmenting them from the rest of the image. Instance segmentation as an object detection are often used as key components in computer vision systems. \n", + "Applications that use real-time instance segmentation models include video analytics, robotics, autonomous vehicles, multi-object tracking and object counting, medical image analysis, and many others.\n", + "\n", + "\n", + "This tutorial demonstrates step-by-step instructions on how to run and optimize PyTorch YOLOv11 with OpenVINO. We consider the steps required for instance segmentation scenario. You can find more details about model on [model page](https://docs.ultralytics.com/models/yolo11/) in Ultralytics documentation.\n", + "\n", + "The tutorial consists of the following steps:\n", + "- Prepare the PyTorch model.\n", + "- Download and prepare a dataset.\n", + "- Validate the original model.\n", + "- Convert the PyTorch model to OpenVINO IR.\n", + "- Validate the converted model.\n", + "- Prepare and run optimization pipeline.\n", + "- Compare performance of the FP32 and quantized models.\n", + "- Compare accuracy of the FP32 and quantized models.\n", + "- Live demo\n", + "\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Get PyTorch model](#Get-PyTorch-model)\n", + " - [Prerequisites](#Prerequisites)\n", + "- [Instantiate model](#Instantiate-model)\n", + " - [Convert model to OpenVINO IR](#Convert-model-to-OpenVINO-IR)\n", + " - [Verify model inference](#Verify-model-inference)\n", + " - [Select inference device](#Select-inference-device)\n", + " - [Test on single image](#Test-on-single-image)\n", + "- [Optimize model using NNCF Post-training Quantization API](#Optimize-model-using-NNCF-Post-training-Quantization-API)\n", + " - [Validate Quantized model inference](#Validate-Quantized-model-inference)\n", + "- [Compare the Original and Quantized Models](#Compare-the-Original-and-Quantized-Models)\n", + " - [Compare performance of the Original and Quantized Models](#Compare-performance-of-the-Original-and-Quantized-Models)\n", + "- [Other ways to optimize model](#Other-ways-to-optimize-model)\n", + "- [Live demo](#Live-demo)\n", + " - [Run Live Object Detection and Segmentation](#Run-Live-Object-Detection-and-Segmentation)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "d7a12678-b12f-48d1-9735-398855733e46", + "metadata": {}, + "source": [ + "## Get PyTorch model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Generally, PyTorch models represent an instance of the [`torch.nn.Module`](https://pytorch.org/docs/stable/generated/torch.nn.Module.html) class, initialized by a state dictionary with model weights.\n", + "We will use the YOLOv11 nano model (also known as `yolo11n-seg`) pre-trained on a COCO dataset, which is available in this [repo](https://github.com/ultralytics/ultralytics). Similar steps are also applicable to other YOLOv11 models.\n", + "Typical steps to obtain a pre-trained model:\n", + "1. Create an instance of a model class.\n", + "2. Load a checkpoint state dict, which contains the pre-trained model weights.\n", + "3. Turn the model to evaluation for switching some operations to inference mode.\n", + "\n", + "In this case, the creators of the model provide an API that enables converting the YOLOv11 model to OpenVINO IR. Therefore, we do not need to do these steps manually." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e2267760-cbfe-41c6-958d-cad9f845d5bb", + "metadata": {}, + "source": [ + "#### Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Install necessary packages." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30d04872-6916-454c-9211-6c644b50dc04", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install -q \"openvino>=2024.0.0\" \"nncf>=2.9.0\"\n", + "%pip install -q \"torch>=2.1\" \"torchvision>=0.16\" \"ultralytics==8.3.0\" opencv-python tqdm --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "1bbe319c", + "metadata": {}, + "source": [ + "Import required utility functions.\n", + "The lower cell will download the `notebook_utils` Python module from GitHub." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2f6cd89", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import requests\n", + "\n", + "if not Path(\"notebook_utils.py\").exists():\n", + " r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + " )\n", + "\n", + " open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "\n", + "from notebook_utils import download_file, VideoPlayer, device_widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "373658bd-7e64-4479-914e-f2742d330afd", + "metadata": {}, + "outputs": [], + "source": [ + "# Download a test sample\n", + "IMAGE_PATH = Path(\"./data/coco_bike.jpg\")\n", + "\n", + "if not IMAGE_PATH.exists():\n", + " download_file(\n", + " url=\"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/image/coco_bike.jpg\",\n", + " filename=IMAGE_PATH.name,\n", + " directory=IMAGE_PATH.parent,\n", + " )" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "ee32fd08-650c-4751-bb41-d8afccb2495e", + "metadata": {}, + "source": [ + "## Instantiate model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "For loading the model, required to specify a path to the model checkpoint. It can be some local path or name available on models hub (in this case model checkpoint will be downloaded automatically). \n", + "You can select model using widget bellow:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c3963a5-b50b-4ffe-b710-ccd1f4a65380", + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "\n", + "model_id = [\n", + " \"yolo11n-seg\",\n", + " \"yolo11s-seg\",\n", + " \"yolo11m-seg\",\n", + " \"yolo11l-seg\",\n", + " \"yolo11x-seg\",\n", + " \"yolov8n-seg\",\n", + " \"yolov8s-seg\",\n", + " \"yolov8m-seg\",\n", + " \"yolov8l-seg\",\n", + " \"yolov8x-seg\",\n", + "]\n", + "\n", + "model_name = widgets.Dropdown(options=model_id, value=model_id[0], description=\"Model\")\n", + "\n", + "model_name" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "60105d86-d7e0-47a9-ad88-4fff014ba3d8", + "metadata": {}, + "source": [ + "Making prediction, the model accepts a path to input image and returns list with Results class object. Results contains boxes for object detection model and boxes and masks for segmentation model. Also it contains utilities for processing results, for example, `plot()` method for drawing.\n", + "\n", + "Let us consider the examples:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4994e1e-a3ee-4620-bc8b-237a55c47742", + "metadata": {}, + "outputs": [], + "source": [ + "from PIL import Image\n", + "from ultralytics import YOLO\n", + "\n", + "SEG_MODEL_NAME = model_name.value\n", + "\n", + "seg_model = YOLO(f\"{SEG_MODEL_NAME}.pt\")\n", + "label_map = seg_model.model.names\n", + "\n", + "res = seg_model(IMAGE_PATH)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e345ffcc-c4b8-44ba-8b03-f37e63a060da", + "metadata": {}, + "source": [ + "### Convert model to OpenVINO IR\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Ultralytics provides API for convenient model exporting to different formats including OpenVINO IR. `model.export` is responsible for model conversion. We need to specify the format, and additionally, we can preserve dynamic shapes in the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c3ef363-f6a1-4fe5-b7ff-eb5a9c0789f1", + "metadata": {}, + "outputs": [], + "source": [ + "# instance segmentation model\n", + "seg_model_path = Path(f\"{SEG_MODEL_NAME}_openvino_model/{SEG_MODEL_NAME}.xml\")\n", + "if not seg_model_path.exists():\n", + " seg_model.export(format=\"openvino\", dynamic=True, half=True)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "713cd45f-2b19-4a1e-bc9d-5e69bd95d896", + "metadata": {}, + "source": [ + "### Verify model inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "We can reuse the base model pipeline for pre- and postprocessing just replacing the inference method where we will use the IR model for inference." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f9b9c472", + "metadata": {}, + "source": [ + "### Select inference device\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Select device from dropdown list for running inference using OpenVINO" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e0652b2", + "metadata": {}, + "outputs": [], + "source": [ + "device = device_widget()\n", + "\n", + "device" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "cd163eab-e803-4ae8-96da-22c3bf303630", + "metadata": {}, + "source": [ + "### Test on single image\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34a65afa-4201-428b-af11-b35e0d206e93", + "metadata": {}, + "outputs": [], + "source": [ + "import openvino as ov\n", + "\n", + "core = ov.Core()\n", + "seg_ov_model = core.read_model(seg_model_path)\n", + "\n", + "ov_config = {}\n", + "if device.value != \"CPU\":\n", + " seg_ov_model.reshape({0: [1, 3, 640, 640]})\n", + "if \"GPU\" in device.value or (\"AUTO\" in device.value and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "seg_compiled_model = core.compile_model(seg_ov_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "389a4698-b7fc-4aa2-9833-9681f575d88b", + "metadata": {}, + "outputs": [], + "source": [ + "seg_model = YOLO(seg_model_path.parent, task=\"segment\")\n", + "\n", + "if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + "seg_model.predictor.model.ov_compiled_model = seg_compiled_model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12d2522f-e939-4512-b74a-3e20baf1edc8", + "metadata": {}, + "outputs": [], + "source": [ + "res = seg_model(IMAGE_PATH)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7e918cf6-cdce-4e90-a6d7-c435a3c08d93", + "metadata": {}, + "source": [ + "Great! The result is the same, as produced by original models." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "13b69e12-2e22-44c1-bfed-fdc88f6c424d", + "metadata": {}, + "source": [ + "## Optimize model using NNCF Post-training Quantization API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "[NNCF](https://github.com/openvinotoolkit/nncf) provides a suite of advanced algorithms for Neural Networks inference optimization in OpenVINO with minimal accuracy drop.\n", + "We will use 8-bit quantization in post-training mode (without the fine-tuning pipeline) to optimize YOLOv11.\n", + "\n", + "The optimization process contains the following steps:\n", + "\n", + "1. Create a Dataset for quantization.\n", + "2. Run `nncf.quantize` for getting an optimized model.\n", + "3. Serialize OpenVINO IR model, using the `openvino.runtime.serialize` function." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "7d7eeae8-f3f4-4e95-b1a6-a23085f03ebc", + "metadata": {}, + "source": [ + "Please select below whether you would like to run quantization to improve model inference speed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4690b9fc-c49f-4882-b8a5-c82f842a9126", + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "\n", + "int8_model_seg_path = Path(f\"{SEG_MODEL_NAME}_openvino_int8_model/{SEG_MODEL_NAME}.xml\")\n", + "quantized_seg_model = None\n", + "\n", + "to_quantize = widgets.Checkbox(\n", + " value=True,\n", + " description=\"Quantization\",\n", + " disabled=False,\n", + ")\n", + "\n", + "to_quantize" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "73cfdbd4-3b65-49e0-b8ab-ceb1d0362481", + "metadata": {}, + "source": [ + "Let's load `skip magic` extension to skip quantization if `to_quantize` is not selected" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc6121aa-3a07-4066-a123-fe0ed0e72478", + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch skip_kernel_extension module\n", + "import requests\n", + "\n", + "\n", + "if not Path(\"skip_kernel_extension.py\").exists():\n", + " r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/skip_kernel_extension.py\",\n", + " )\n", + " open(\"skip_kernel_extension.py\", \"w\").write(r.text)\n", + "\n", + "%load_ext skip_kernel_extension" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "94fb2515-3645-4798-bc7c-082129ac86c0", + "metadata": {}, + "source": [ + "Reuse validation dataloader in accuracy testing for quantization. \n", + "For that, it should be wrapped into the `nncf.Dataset` object and define a transformation function for getting only input tensors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd994958-6988-4a1d-ac7f-3efbd97135cc", + "metadata": {}, + "outputs": [], + "source": [ + "# %%skip not $to_quantize.value\n", + "\n", + "\n", + "import nncf\n", + "from typing import Dict\n", + "\n", + "from zipfile import ZipFile\n", + "\n", + "from ultralytics.data.utils import DATASETS_DIR\n", + "from ultralytics.utils import DEFAULT_CFG\n", + "from ultralytics.cfg import get_cfg\n", + "from ultralytics.data.converter import coco80_to_coco91_class\n", + "from ultralytics.data.utils import check_det_dataset\n", + "from ultralytics.utils import ops\n", + "\n", + "\n", + "if not int8_model_seg_path.exists():\n", + " DATA_URL = \"http://images.cocodataset.org/zips/val2017.zip\"\n", + " LABELS_URL = \"https://github.com/ultralytics/yolov5/releases/download/v1.0/coco2017labels-segments.zip\"\n", + " CFG_URL = \"https://raw.githubusercontent.com/ultralytics/ultralytics/v8.1.0/ultralytics/cfg/datasets/coco.yaml\"\n", + "\n", + " OUT_DIR = DATASETS_DIR\n", + "\n", + " DATA_PATH = OUT_DIR / \"val2017.zip\"\n", + " LABELS_PATH = OUT_DIR / \"coco2017labels-segments.zip\"\n", + " CFG_PATH = OUT_DIR / \"coco.yaml\"\n", + "\n", + " download_file(DATA_URL, DATA_PATH.name, DATA_PATH.parent)\n", + " download_file(LABELS_URL, LABELS_PATH.name, LABELS_PATH.parent)\n", + " download_file(CFG_URL, CFG_PATH.name, CFG_PATH.parent)\n", + "\n", + " if not (OUT_DIR / \"coco/labels\").exists():\n", + " with ZipFile(LABELS_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR)\n", + " with ZipFile(DATA_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR / \"coco/images\")\n", + "\n", + " args = get_cfg(cfg=DEFAULT_CFG)\n", + " args.data = str(CFG_PATH)\n", + " seg_validator = seg_model.task_map[seg_model.task][\"validator\"](args=args)\n", + " seg_validator.data = check_det_dataset(args.data)\n", + " seg_validator.stride = 32\n", + " seg_data_loader = seg_validator.get_dataloader(OUT_DIR / \"coco/\", 1)\n", + "\n", + " seg_validator.is_coco = True\n", + " seg_validator.class_map = coco80_to_coco91_class()\n", + " seg_validator.names = label_map\n", + " seg_validator.metrics.names = seg_validator.names\n", + " seg_validator.nc = 80\n", + " seg_validator.nm = 32\n", + " seg_validator.process = ops.process_mask\n", + " seg_validator.plot_masks = []\n", + "\n", + " def transform_fn(data_item: Dict):\n", + " \"\"\"\n", + " Quantization transform function. Extracts and preprocess input data from dataloader item for quantization.\n", + " Parameters:\n", + " data_item: Dict with data item produced by DataLoader during iteration\n", + " Returns:\n", + " input_tensor: Input data for quantization\n", + " \"\"\"\n", + " input_tensor = seg_validator.preprocess(data_item)[\"img\"].numpy()\n", + " return input_tensor\n", + "\n", + " quantization_dataset = nncf.Dataset(seg_data_loader, transform_fn)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "91629284-e261-494d-9464-146ed7190084", + "metadata": {}, + "source": [ + "The `nncf.quantize` function provides an interface for model quantization. It requires an instance of the OpenVINO Model and quantization dataset. \n", + "Optionally, some additional parameters for the configuration quantization process (number of samples for quantization, preset, ignored scope, etc.) can be provided. Ultralytics models contain non-ReLU activation functions, which require asymmetric quantization of activations. To achieve a better result, we will use a `mixed` quantization preset. It provides symmetric quantization of weights and asymmetric quantization of activations. For more accurate results, we should keep the operation in the postprocessing subgraph in floating point precision, using the `ignored_scope` parameter.\n", + "\n", + ">**Note**: Model post-training quantization is time-consuming process. Be patient, it can take several minutes depending on your hardware." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2e8cec4-d0b3-4da0-b54c-7964e7bcbfe2", + "metadata": { + "test_replace": { + "ignored_scope=ignored_scope\n": "ignored_scope=ignored_scope, subset_size=10\n" + } + }, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "if not int8_model_seg_path.exists():\n", + " ignored_scope = nncf.IgnoredScope( # post-processing\n", + " subgraphs=[\n", + " nncf.Subgraph(inputs=[f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_1\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_2\",\n", + " f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_7\"],\n", + " outputs=[f\"__module.model.{22 if 'v8' in SEG_MODEL_NAME else 23}/aten::cat/Concat_8\"])\n", + " ]\n", + " )\n", + "\n", + " # Segmentation model\n", + " quantized_seg_model = nncf.quantize(\n", + " seg_ov_model,\n", + " quantization_dataset,\n", + " preset=nncf.QuantizationPreset.MIXED,\n", + " ignored_scope=ignored_scope\n", + " )\n", + "\n", + " print(f\"Quantized segmentation model will be saved to {int8_model_seg_path}\")\n", + " ov.save_model(quantized_seg_model, str(int8_model_seg_path))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c8c92a68-3eb8-4ea5-9e35-d496f45b3cc3", + "metadata": {}, + "source": [ + "### Validate Quantized model inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "`nncf.quantize` returns the OpenVINO Model class instance, which is suitable for loading on a device for making predictions. `INT8` model input data and output result formats have no difference from the floating point model representation. Therefore, we can reuse the same `detect` function defined above for getting the `INT8` model result on the image." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4003a3ae", + "metadata": {}, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "711200ec-2f26-47cc-b62d-3802961d5711", + "metadata": {}, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "if quantized_seg_model is None:\n", + " quantized_seg_model = core.read_model(int8_model_seg_path)\n", + "\n", + "ov_config = {}\n", + "if device.value != \"CPU\":\n", + " quantized_seg_model.reshape({0: [1, 3, 640, 640]})\n", + "if \"GPU\" in device.value or (\"AUTO\" in device.value and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "\n", + "quantized_seg_compiled_model = core.compile_model(quantized_seg_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2e3ea3b-5935-4474-9f08-f51fc688b9c0", + "metadata": {}, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "\n", + "if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + "seg_model.predictor.model.ov_compiled_model = quantized_seg_compiled_model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bef2ed5d-5762-41a6-af2e-ee4f9fd56557", + "metadata": {}, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "res = seg_model(IMAGE_PATH)\n", + "display(Image.fromarray(res[0].plot()[:, :, ::-1]))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "b289f057", + "metadata": {}, + "source": [ + "## Compare the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "59081cae-9c92-46f0-8117-ed8c6fa6ff19", + "metadata": {}, + "source": [ + "### Compare performance of the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "Finally, use the OpenVINO [Benchmark Tool](https://docs.openvino.ai/2024/learn-openvino/openvino-samples/benchmark-tool.html) to measure the inference performance of the `FP32` and `INT8` models.\n", + "\n", + "> **Note**: For more accurate performance, it is recommended to run `benchmark_app` in a terminal/command prompt after closing other applications. Run `benchmark_app -m -d CPU -shape \"\"` to benchmark async inference on CPU on specific input data shape for one minute. Change `CPU` to `GPU` to benchmark on GPU. Run `benchmark_app --help` to see an overview of all command-line options." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8677df4", + "metadata": {}, + "outputs": [], + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "546828d2", + "metadata": {}, + "outputs": [], + "source": [ + "if int8_model_seg_path.exists():\n", + " !benchmark_app -m $seg_model_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d43554b", + "metadata": {}, + "outputs": [], + "source": [ + "if int8_model_seg_path.exists():\n", + " !benchmark_app -m $int8_model_seg_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "184bcebd-588d-4b4d-a60a-0fa753a07ef9", + "metadata": {}, + "source": [ + "## Other ways to optimize model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The performance could be also improved by another OpenVINO method such as async inference pipeline or preprocessing API.\n", + "\n", + "Async Inference pipeline help to utilize the device more optimal. The key advantage of the Async API is that when a device is busy with inference, the application can perform other tasks in parallel (for example, populating inputs or scheduling other requests) rather than wait for the current inference to complete first. To understand how to perform async inference using openvino, refer to [Async API tutorial](../async-api/async-api.ipynb)\n", + "\n", + "Preprocessing API enables making preprocessing a part of the model reducing application code and dependency on additional image processing libraries. \n", + "The main advantage of Preprocessing API is that preprocessing steps will be integrated into the execution graph and will be performed on a selected device (CPU/GPU etc.) rather than always being executed on CPU as part of an application. This will also improve selected device utilization. For more information, refer to the overview of [Preprocessing API tutorial](../optimize-preprocessing/optimize-preprocessing.ipynb). To see, how it could be used with YOLOV8 object detection model, please, see [Convert and Optimize YOLOv8 real-time object detection with OpenVINO tutorial](./yolov8-object-detection.ipynb)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "2e3b4862-c182-4ce4-a473-9f38d98deab8", + "metadata": { + "tags": [] + }, + "source": [ + "## Live demo\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "The following code runs model inference on a video:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c7bb92e-e301-45a9-b5ff-f7953fad298f", + "metadata": {}, + "outputs": [], + "source": [ + "import collections\n", + "import time\n", + "import cv2\n", + "from IPython import display\n", + "import numpy as np\n", + "\n", + "\n", + "def run_instance_segmentation(\n", + " source=0,\n", + " flip=False,\n", + " use_popup=False,\n", + " skip_first_frames=0,\n", + " model=seg_model,\n", + " device=device.value,\n", + " video_width=1280,\n", + "):\n", + " player = None\n", + "\n", + " ov_config = {}\n", + " if device != \"CPU\":\n", + " model.reshape({0: [1, 3, 640, 640]})\n", + " if \"GPU\" in device or (\"AUTO\" in device and \"GPU\" in core.available_devices):\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + " compiled_model = core.compile_model(model, device, ov_config)\n", + "\n", + " if seg_model.predictor is None:\n", + " custom = {\"conf\": 0.25, \"batch\": 1, \"save\": False, \"mode\": \"predict\"} # method defaults\n", + " args = {**seg_model.overrides, **custom}\n", + " seg_model.predictor = seg_model._smart_load(\"predictor\")(overrides=args, _callbacks=seg_model.callbacks)\n", + " seg_model.predictor.setup_model(model=seg_model.model)\n", + "\n", + " seg_model.predictor.model.ov_compiled_model = compiled_model\n", + "\n", + " try:\n", + " # Create a video player to play with target fps.\n", + " player = VideoPlayer(source=source, flip=flip, fps=30, skip_first_frames=skip_first_frames, width=video_width)\n", + " # Start capturing.\n", + " player.start()\n", + " if use_popup:\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(winname=title, flags=cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_AUTOSIZE)\n", + "\n", + " processing_times = collections.deque()\n", + " while True:\n", + " # Grab the frame.\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + " # If the frame is larger than video_width, reduce size to improve the performance.\n", + " # If more, increase size for better demo expirience.\n", + " scale = video_width / max(frame.shape)\n", + " frame = cv2.resize(\n", + " src=frame,\n", + " dsize=None,\n", + " fx=scale,\n", + " fy=scale,\n", + " interpolation=cv2.INTER_AREA,\n", + " )\n", + " # Get the results.\n", + " input_image = np.array(frame)\n", + "\n", + " start_time = time.time()\n", + " detections = seg_model(input_image)\n", + " stop_time = time.time()\n", + " frame = detections[0].plot()\n", + "\n", + " processing_times.append(stop_time - start_time)\n", + " # Use processing times from last 200 frames.\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " _, f_width = frame.shape[:2]\n", + " # Mean processing time [ms].\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + " cv2.putText(\n", + " img=frame,\n", + " text=f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " org=(20, 40),\n", + " fontFace=cv2.FONT_HERSHEY_COMPLEX,\n", + " fontScale=f_width / 1000,\n", + " color=(0, 0, 255),\n", + " thickness=1,\n", + " lineType=cv2.LINE_AA,\n", + " )\n", + " # Use this workaround if there is flickering.\n", + " if use_popup:\n", + " cv2.imshow(winname=title, mat=frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # Encode numpy array to jpg.\n", + " _, encoded_img = cv2.imencode(ext=\".jpg\", img=frame, params=[cv2.IMWRITE_JPEG_QUALITY, 100])\n", + " # Create an IPython image.\n", + " i = display.Image(data=encoded_img)\n", + " # Display the image in this notebook.\n", + " display.clear_output(wait=True)\n", + " display.display(i)\n", + " # ctrl-c\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " # any different error\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " if player is not None:\n", + " # Stop capturing.\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "cc5b4ba6-f478-4417-b09d-93fee5adca41", + "metadata": {}, + "source": [ + "### Run Live Object Detection and Segmentation\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "Use a webcam as the video input. By default, the primary webcam is set with `source=0`. If you have multiple webcams, each one will be assigned a consecutive number starting at 0. Set `flip=True` when using a front-facing camera. Some web browsers, especially Mozilla Firefox, may cause flickering. If you experience flickering, set `use_popup=True`.\n", + "\n", + ">**NOTE**: To use this notebook with a webcam, you need to run the notebook on a computer with a webcam. If you run the notebook on a remote server (for example, in Binder or Google Colab service), the webcam will not work. By default, the lower cell will run model inference on a video file. If you want to try live inference on your webcam set `WEBCAM_INFERENCE = True`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90708017", + "metadata": {}, + "outputs": [], + "source": [ + "WEBCAM_INFERENCE = False\n", + "\n", + "if WEBCAM_INFERENCE:\n", + " VIDEO_SOURCE = 0 # Webcam\n", + "else:\n", + " if not Path(\"people.mp4\").exists():\n", + " download_file(\n", + " \"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/video/people.mp4\",\n", + " \"people.mp4\",\n", + " )\n", + " VIDEO_SOURCE = \"people.mp4\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8f1239c", + "metadata": {}, + "outputs": [], + "source": [ + "device" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "440f2d0e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "run_instance_segmentation(\n", + " source=VIDEO_SOURCE,\n", + " flip=True,\n", + " use_popup=False,\n", + " model=seg_ov_model,\n", + " device=device.value,\n", + " video_width=1280,\n", + ")" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + { modified: 24, original: 24 }, + { modified: 25, original: 25 }, + { modified: 26, original: 26 }, + { modified: 27, original: 27 }, + { modified: 28, original: 28 }, + { modified: 29, original: 29 }, + { modified: 30, original: 30 }, + { modified: 31, original: 31 }, + { modified: 32, original: 32 }, + { modified: 33, original: 33 }, + { modified: 34, original: 34 }, + { modified: 35, original: 35 }, + { modified: 36, original: 36 }, + { modified: 37, original: 37 }, + { modified: 38, original: 38 }, + { modified: 39, original: 39 }, + { modified: 40, original: 40 }, + { modified: 41, original: 41 }, + { modified: 42, original: 42 }, + { modified: 43, original: 43 }, + { modified: 44, original: 44 }, + { modified: 45, original: 45 }, + { modified: 46, original: 46 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 5, originalLength: 1, modifiedStart: 5, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 6, originalLength: 1, modifiedStart: 6, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 25, originalLength: 1, modifiedStart: 25, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 42, originalLength: 1, modifiedStart: 42, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 44, originalLength: 1, modifiedStart: 44, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 46, originalLength: 1, modifiedStart: 46, modifiedLength: 1 } satisfies IDiffChange, + ]); + + }); + test('Modification of a large cell and insert a new cell', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "id": "e9d010ff-85ba-4e69-b439-ba89e4a3a715", + "source": [ + "# Convert and Optimize YOLOv10 with OpenVINO\n", + "\n", + "Real-time object detection aims to accurately predict object categories and positions in images with low latency. The YOLO series has been at the forefront of this research due to its balance between performance and efficiency. However, reliance on NMS and architectural inefficiencies have hindered optimal performance. YOLOv10 addresses these issues by introducing consistent dual assignments for NMS-free training and a holistic efficiency-accuracy driven model design strategy.\n", + "\n", + "YOLOv10, built on the [Ultralytics Python package](https://pypi.org/project/ultralytics/) by researchers at [Tsinghua University](https://www.tsinghua.edu.cn/en/), introduces a new approach to real-time object detection, addressing both the post-processing and model architecture deficiencies found in previous YOLO versions. By eliminating non-maximum suppression (NMS) and optimizing various model components, YOLOv10 achieves state-of-the-art performance with significantly reduced computational overhead. Extensive experiments demonstrate its superior accuracy-latency trade-offs across multiple model scales.\n", + "\n", + "![yolov10-approach.png](https://github.com/ultralytics/ultralytics/assets/26833433/f9b1bec0-928e-41ce-a205-e12db3c4929a)\n", + "\n", + "More details about model architecture you can find in original [repo](https://github.com/THU-MIG/yolov10), [paper](https://arxiv.org/abs/2405.14458) and [Ultralytics documentation](https://docs.ultralytics.com/models/yolov10/).\n", + "\n", + "This tutorial demonstrates step-by-step instructions on how to run and optimize PyTorch YOLO V10 with OpenVINO.\n", + "\n", + "The tutorial consists of the following steps:\n", + "\n", + "- Prepare PyTorch model\n", + "- Convert PyTorch model to OpenVINO IR\n", + "- Run model inference with OpenVINO\n", + "- Prepare and run optimization pipeline using NNCF\n", + "- Compare performance of the FP16 and quantized models.\n", + "- Run optimized model inference on video\n", + "- Launch interactive Gradio demo\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Download PyTorch model](#Download-PyTorch-model)\n", + "- [Export PyTorch model to OpenVINO IR Format](#Export-PyTorch-model-to-OpenVINO-IR-Format)\n", + "- [Run OpenVINO Inference on AUTO device using Ultralytics API](#Run-OpenVINO-Inference-on-AUTO-device-using-Ultralytics-API)\n", + "- [Run OpenVINO Inference on selected device using Ultralytics API](#Run-OpenVINO-Inference-on-selected-device-using-Ultralytics-API)\n", + "- [Optimize model using NNCF Post-training Quantization API](#Optimize-model-using-NNCF-Post-training-Quantization-API)\n", + " - [Prepare Quantization Dataset](#Prepare-Quantization-Dataset)\n", + " - [Quantize and Save INT8 model](#Quantize-and-Save-INT8-model)\n", + "- [Run Optimized Model Inference](#Run-Optimized-Model-Inference)\n", + " - [Run Optimized Model on AUTO device](#Run-Optimized-Model-on-AUTO-device)\n", + " - [Run Optimized Model Inference on selected device](#Run-Optimized-Model-Inference-on-selected-device)\n", + "- [Compare the Original and Quantized Models](#Compare-the-Original-and-Quantized-Models)\n", + " - [Model size](#Model-size)\n", + " - [Performance](#Performance)\n", + " - [FP16 model performance](#FP16-model-performance)\n", + " - [Int8 model performance](#Int8-model-performance)\n", + "- [Live demo](#Live-demo)\n", + " - [Gradio Interactive Demo](#Gradio-Interactive-Demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "144e86a1-c220-4e3b-a8b2-8eda443faf2d", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "bf76580b-860a-4dcd-8b01-f494cdffe37e", + "source": [ + "import os\n", + "\n", + "os.environ[\"GIT_CLONE_PROTECTION_ACTIVE\"] = \"false\"\n", + "\n", + "%pip install -q \"nncf>=2.11.0\"\n", + "%pip install -Uq \"openvino>=2024.3.0\"\n", + "%pip install -q \"git+https://github.com/THU-MIG/yolov10.git\" --extra-index-url https://download.pytorch.org/whl/cpu\n", + "%pip install -q \"torch>=2.1\" \"torchvision>=0.16\" tqdm opencv-python \"gradio>=4.19\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "code", + "id": "75772eac-565b-4234-82bf-f305e0c1ceda", + "source": [ + "from pathlib import Path\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "from notebook_utils import download_file, VideoPlayer, device_widget" + ] + }, + { + "cell_type": "markdown", + "id": "2888d8b4-44f6-4418-bae4-f433ff28010b", + "source": [ + "## Download PyTorch model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "There are several version of [YOLO V10](https://github.com/THU-MIG/yolov10/tree/main?tab=readme-ov-file#performance) models provided by model authors. Each of them has different characteristics depends on number of training parameters, performance and accuracy. For demonstration purposes we will use `yolov10n`, but the same steps are also applicable to other models in YOLO V10 series." + ] + }, + { + "cell_type": "code", + "id": "aea6315b-6a29-4f2a-96a9-eb3c1646a3a4", + "source": [ + "models_dir = Path(\"./models\")\n", + "models_dir.mkdir(exist_ok=True)" + ] + }, + { + "cell_type": "code", + "id": "d828401e-b6bd-4796-a7b8-681957a159d4", + "source": [ + "model_weights_url = \"https://github.com/jameslahm/yolov10/releases/download/v1.0/yolov10n.pt\"\n", + "file_name = model_weights_url.split(\"/\")[-1]\n", + "model_name = file_name.replace(\".pt\", \"\")\n", + "\n", + "download_file(model_weights_url, directory=models_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "dc21487d-dd79-4b56-b8e2-5b28c8d92ac8", + "source": [ + "## Export PyTorch model to OpenVINO IR Format\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "markdown", + "id": "8cfec0d6-fb55-4611-861b-326e892fcf09", + "source": [ + "As it was discussed before, YOLO V10 code is designed on top of [Ultralytics](https://docs.ultralytics.com/) library and has similar interface with YOLO V8 (You can check [YOLO V8 notebooks](https://github.com/openvinotoolkit/openvino_notebooks/tree/latest/notebooks/yolov8-optimization) for more detailed instruction how to work with Ultralytics API). Ultralytics support OpenVINO model export using [export](https://docs.ultralytics.com/modes/export/) method of model class. Additionally, we can specify parameters responsible for target input size, static or dynamic input shapes and model precision (FP32/FP16/INT8). INT8 quantization can be additionally performed on export stage, but for making approach more flexible, we consider how to perform quantization using [NNCF](https://github.com/openvinotoolkit/nncf)." + ] + }, + { + "cell_type": "code", + "id": "d488f564-c1d5-4c6a-9b4a-353a3ab749e6", + "source": [ + "import types\n", + "from ultralytics.utils import ops, yaml_load, yaml_save\n", + "from ultralytics import YOLOv10\n", + "import torch\n", + "\n", + "detection_labels = {\n", + " 0: \"person\",\n", + " 1: \"bicycle\",\n", + " 2: \"car\",\n", + " 3: \"motorcycle\",\n", + " 4: \"airplane\",\n", + " 5: \"bus\",\n", + " 6: \"train\",\n", + " 7: \"truck\",\n", + " 8: \"boat\",\n", + " 9: \"traffic light\",\n", + " 10: \"fire hydrant\",\n", + " 11: \"stop sign\",\n", + " 12: \"parking meter\",\n", + " 13: \"bench\",\n", + " 14: \"bird\",\n", + " 15: \"cat\",\n", + " 16: \"dog\",\n", + " 17: \"horse\",\n", + " 18: \"sheep\",\n", + " 19: \"cow\",\n", + " 20: \"elephant\",\n", + " 21: \"bear\",\n", + " 22: \"zebra\",\n", + " 23: \"giraffe\",\n", + " 24: \"backpack\",\n", + " 25: \"umbrella\",\n", + " 26: \"handbag\",\n", + " 27: \"tie\",\n", + " 28: \"suitcase\",\n", + " 29: \"frisbee\",\n", + " 30: \"skis\",\n", + " 31: \"snowboard\",\n", + " 32: \"sports ball\",\n", + " 33: \"kite\",\n", + " 34: \"baseball bat\",\n", + " 35: \"baseball glove\",\n", + " 36: \"skateboard\",\n", + " 37: \"surfboard\",\n", + " 38: \"tennis racket\",\n", + " 39: \"bottle\",\n", + " 40: \"wine glass\",\n", + " 41: \"cup\",\n", + " 42: \"fork\",\n", + " 43: \"knife\",\n", + " 44: \"spoon\",\n", + " 45: \"bowl\",\n", + " 46: \"banana\",\n", + " 47: \"apple\",\n", + " 48: \"sandwich\",\n", + " 49: \"orange\",\n", + " 50: \"broccoli\",\n", + " 51: \"carrot\",\n", + " 52: \"hot dog\",\n", + " 53: \"pizza\",\n", + " 54: \"donut\",\n", + " 55: \"cake\",\n", + " 56: \"chair\",\n", + " 57: \"couch\",\n", + " 58: \"potted plant\",\n", + " 59: \"bed\",\n", + " 60: \"dining table\",\n", + " 61: \"toilet\",\n", + " 62: \"tv\",\n", + " 63: \"laptop\",\n", + " 64: \"mouse\",\n", + " 65: \"remote\",\n", + " 66: \"keyboard\",\n", + " 67: \"cell phone\",\n", + " 68: \"microwave\",\n", + " 69: \"oven\",\n", + " 70: \"toaster\",\n", + " 71: \"sink\",\n", + " 72: \"refrigerator\",\n", + " 73: \"book\",\n", + " 74: \"clock\",\n", + " 75: \"vase\",\n", + " 76: \"scissors\",\n", + " 77: \"teddy bear\",\n", + " 78: \"hair drier\",\n", + " 79: \"toothbrush\",\n", + "}\n", + "\n", + "\n", + "def v10_det_head_forward(self, x):\n", + " one2one = self.forward_feat([xi.detach() for xi in x], self.one2one_cv2, self.one2one_cv3)\n", + " if not self.export:\n", + " one2many = super().forward(x)\n", + "\n", + " if not self.training:\n", + " one2one = self.inference(one2one)\n", + " if not self.export:\n", + " return {\"one2many\": one2many, \"one2one\": one2one}\n", + " else:\n", + " assert self.max_det != -1\n", + " boxes, scores, labels = ops.v10postprocess(one2one.permute(0, 2, 1), self.max_det, self.nc)\n", + " return torch.cat(\n", + " [boxes, scores.unsqueeze(-1), labels.unsqueeze(-1).to(boxes.dtype)],\n", + " dim=-1,\n", + " )\n", + " else:\n", + " return {\"one2many\": one2many, \"one2one\": one2one}\n", + "\n", + "\n", + "ov_model_path = models_dir / f\"{model_name}_openvino_model/{model_name}.xml\"\n", + "if not ov_model_path.exists():\n", + " model = YOLOv10(models_dir / file_name)\n", + " model.model.model[-1].forward = types.MethodType(v10_det_head_forward, model.model.model[-1])\n", + " model.export(format=\"openvino\", dynamic=True, half=True)\n", + " config = yaml_load(ov_model_path.parent / \"metadata.yaml\")\n", + " config[\"names\"] = detection_labels\n", + " yaml_save(ov_model_path.parent / \"metadata.yaml\", config)" + ] + }, + { + "cell_type": "markdown", + "id": "8a23f621-f6cd-4bfd-8b98-24b8c0392761", + "source": [ + "## Run OpenVINO Inference on AUTO device using Ultralytics API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "Now, when we exported model to OpenVINO, we can load it directly into YOLOv10 class, where automatic inference backend will provide easy-to-use user experience to run OpenVINO YOLOv10 model on the similar level like for original PyTorch model. The code bellow demonstrates how to run inference OpenVINO exported model with Ultralytics API on single image. [AUTO device](https://github.com/openvinotoolkit/openvino_notebooks/tree/latest/notebooks/auto-device) will be used for launching model." + ] + }, + { + "cell_type": "code", + "id": "7e8ed361-bf09-4398-ba15-e6c5dda73cd3", + "source": [ + "ov_yolo_model = YOLOv10(ov_model_path.parent, task=\"detect\")" + ] + }, + { + "cell_type": "code", + "id": "8a473286-59b8-498f-8541-df84f041e119", + "source": [ + "from PIL import Image\n", + "\n", + "IMAGE_PATH = Path(\"./data/coco_bike.jpg\")\n", + "download_file(\n", + " url=\"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/image/coco_bike.jpg\",\n", + " filename=IMAGE_PATH.name,\n", + " directory=IMAGE_PATH.parent,\n", + ")" + ] + }, + { + "cell_type": "code", + "id": "7e116e81-0ad4-4b9e-9f5c-58680fd8f0c6", + "source": [ + "res = ov_yolo_model(IMAGE_PATH, iou=0.45, conf=0.2)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "943b9b04-4b97-4395-99d1-695465de1140", + "source": [ + "## Run OpenVINO Inference on selected device using Ultralytics API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "In this part of notebook you can select inference device for running model inference to compare results with AUTO." + ] + }, + { + "cell_type": "code", + "id": "41655bec-b006-4376-abbb-d6c8c09e89fb", + "source": [ + "device = device_widget(\"CPU\")\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "f284d5fd-12ba-4c08-afce-d8131ef7ad4d", + "source": [ + "import openvino as ov\n", + "\n", + "core = ov.Core()\n", + "\n", + "ov_model = core.read_model(ov_model_path)\n", + "\n", + "# load model on selected device\n", + "if \"GPU\" in device.value or \"NPU\" in device.value:\n", + " ov_model.reshape({0: [1, 3, 640, 640]})\n", + "ov_config = {}\n", + "if \"GPU\" in device.value:\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "det_compiled_model = core.compile_model(ov_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "id": "12164d21-948a-4ef7-9e1c-b0d41c53cf91", + "source": [ + "ov_yolo_model.predictor.model.ov_compiled_model = det_compiled_model" + ] + }, + { + "cell_type": "code", + "id": "ecb9d396-ab1e-4398-a110-18c08b00ab14", + "source": [ + "res = ov_yolo_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "a4abf758-3a72-4ee0-afde-38076602f1c7", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "992b6ac3-4248-4848-8fd6-f9c1b9fc2051", + "source": [ + "## Optimize model using NNCF Post-training Quantization API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "[NNCF](https://github.com/openvinotoolkit/nncf) provides a suite of advanced algorithms for Neural Networks inference optimization in OpenVINO with minimal accuracy drop.\n", + "We will use 8-bit quantization in post-training mode (without the fine-tuning pipeline) to optimize YOLOv10.\n", + "\n", + "The optimization process contains the following steps:\n", + "\n", + "1. Create a Dataset for quantization.\n", + "2. Run `nncf.quantize` for getting an optimized model.\n", + "3. Serialize OpenVINO IR model, using the `openvino.save_model` function.\n", + "\n", + "Quantization is time and memory consuming process, you can skip this step using checkbox bellow:" + ] + }, + { + "cell_type": "code", + "id": "ed9d9ce5-8df9-4803-ae85-bf0744de914c", + "source": [ + "import ipywidgets as widgets\n", + "\n", + "int8_model_det_path = models_dir / \"int8\" / f\"{model_name}_openvino_model/{model_name}.xml\"\n", + "ov_yolo_int8_model = None\n", + "\n", + "to_quantize = widgets.Checkbox(\n", + " value=True,\n", + " description=\"Quantization\",\n", + " disabled=False,\n", + ")\n", + "\n", + "to_quantize" + ] + }, + { + "cell_type": "code", + "id": "52287a23-b5ef-4c34-873a-32b398d26d1a", + "source": [ + "# Fetch skip_kernel_extension module\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/skip_kernel_extension.py\",\n", + ")\n", + "open(\"skip_kernel_extension.py\", \"w\").write(r.text)\n", + "\n", + "%load_ext skip_kernel_extension" + ] + }, + { + "cell_type": "markdown", + "id": "89ba64af-aa73-4ed9-a2f9-527bd9ae8221", + "source": [ + "### Prepare Quantization Dataset\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "For starting quantization, we need to prepare dataset. We will use validation subset from [MS COCO dataset](https://cocodataset.org/) for model quantization and Ultralytics validation data loader for preparing input data." + ] + }, + { + "cell_type": "code", + "id": "5e10435e-9cd6-4f29-a865-721d6209314d", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "from zipfile import ZipFile\n", + "\n", + "from ultralytics.data.utils import DATASETS_DIR\n", + "\n", + "if not int8_model_det_path.exists():\n", + "\n", + " DATA_URL = \"http://images.cocodataset.org/zips/val2017.zip\"\n", + " LABELS_URL = \"https://github.com/ultralytics/yolov5/releases/download/v1.0/coco2017labels-segments.zip\"\n", + " CFG_URL = \"https://raw.githubusercontent.com/ultralytics/ultralytics/v8.1.0/ultralytics/cfg/datasets/coco.yaml\"\n", + "\n", + " OUT_DIR = DATASETS_DIR\n", + "\n", + " DATA_PATH = OUT_DIR / \"val2017.zip\"\n", + " LABELS_PATH = OUT_DIR / \"coco2017labels-segments.zip\"\n", + " CFG_PATH = OUT_DIR / \"coco.yaml\"\n", + "\n", + " download_file(DATA_URL, DATA_PATH.name, DATA_PATH.parent)\n", + " download_file(LABELS_URL, LABELS_PATH.name, LABELS_PATH.parent)\n", + " download_file(CFG_URL, CFG_PATH.name, CFG_PATH.parent)\n", + "\n", + " if not (OUT_DIR / \"coco/labels\").exists():\n", + " with ZipFile(LABELS_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR)\n", + " with ZipFile(DATA_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR / \"coco/images\")" + ] + }, + { + "cell_type": "code", + "id": "b0a14818-21fd-4c71-8e58-2297dfafaadd", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "from ultralytics.utils import DEFAULT_CFG\n", + "from ultralytics.cfg import get_cfg\n", + "from ultralytics.data.converter import coco80_to_coco91_class\n", + "from ultralytics.data.utils import check_det_dataset\n", + "\n", + "if not int8_model_det_path.exists():\n", + " args = get_cfg(cfg=DEFAULT_CFG)\n", + " args.data = str(CFG_PATH)\n", + " det_validator = ov_yolo_model.task_map[ov_yolo_model.task][\"validator\"](args=args)\n", + "\n", + " det_validator.data = check_det_dataset(args.data)\n", + " det_validator.stride = 32\n", + " det_data_loader = det_validator.get_dataloader(OUT_DIR / \"coco\", 1)" + ] + }, + { + "cell_type": "markdown", + "id": "83fa49f4-e40c-48c7-82a6-8ef56e20b343", + "source": [ + "NNCF provides `nncf.Dataset` wrapper for using native framework dataloaders in quantization pipeline. Additionally, we specify transform function that will be responsible for preparing input data in model expected format." + ] + }, + { + "cell_type": "code", + "id": "253f0e0f-131e-43b9-b1be-639d4b3d1fe5", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "import nncf\n", + "from typing import Dict\n", + "\n", + "\n", + "def transform_fn(data_item:Dict):\n", + " \"\"\"\n", + " Quantization transform function. Extracts and preprocess input data from dataloader item for quantization.\n", + " Parameters:\n", + " data_item: Dict with data item produced by DataLoader during iteration\n", + " Returns:\n", + " input_tensor: Input data for quantization\n", + " \"\"\"\n", + " input_tensor = det_validator.preprocess(data_item)['img'].numpy()\n", + " return input_tensor\n", + "\n", + "if not int8_model_det_path.exists():\n", + " quantization_dataset = nncf.Dataset(det_data_loader, transform_fn)" + ] + }, + { + "cell_type": "markdown", + "id": "ff8a6372-2097-4308-a4e5-e4651242a8df", + "source": [ + "### Quantize and Save INT8 model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The `nncf.quantize` function provides an interface for model quantization. It requires an instance of the OpenVINO Model and quantization dataset. \n", + "Optionally, some additional parameters for the configuration quantization process (number of samples for quantization, preset, ignored scope, etc.) can be provided. YOLOv10 model contains non-ReLU activation functions, which require asymmetric quantization of activations. To achieve a better result, we will use a `mixed` quantization preset. It provides symmetric quantization of weights and asymmetric quantization of activations.\n", + "\n", + ">**Note**: Model post-training quantization is time-consuming process. Be patient, it can take several minutes depending on your hardware." + ] + }, + { + "cell_type": "code", + "id": "4c12ae38-239e-4fe1-8296-47567f98538c", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "import shutil\n", + "\n", + "if not int8_model_det_path.exists():\n", + " quantized_det_model = nncf.quantize(\n", + " ov_model,\n", + " quantization_dataset,\n", + " preset=nncf.QuantizationPreset.MIXED,\n", + " )\n", + "\n", + " ov.save_model(quantized_det_model, int8_model_det_path)\n", + " shutil.copy(ov_model_path.parent / \"metadata.yaml\", int8_model_det_path.parent / \"metadata.yaml\")" + ] + }, + { + "cell_type": "markdown", + "id": "41b8a3eb-adb8-40cf-8602-311ff8d75a76", + "source": [ + "## Run Optimized Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The way of usage INT8 quantized model is the same like for model before quantization. Let's check inference result of quantized model on single image" + ] + }, + { + "cell_type": "markdown", + "id": "f438df41-d614-4cca-a746-a173d92dddf5", + "source": [ + "### Run Optimized Model on AUTO device\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "af61303b-16bd-4206-b926-2c28b3a859a4", + "source": [ + "%%skip not $to_quantize.value\n", + "ov_yolo_int8_model = YOLOv10(int8_model_det_path.parent, task=\"detect\")" + ] + }, + { + "cell_type": "code", + "id": "db13b8dd-49a1-4046-8a1a-491eee095a1b", + "source": [ + "%%skip not $to_quantize.value\n", + "res = ov_yolo_int8_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "0423ba31-f0ee-4bfc-a18a-f7b870a9683d", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "4b4f7118-e046-4f87-b403-87053f2b0ac3", + "source": [ + "### Run Optimized Model Inference on selected device\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "142f22ae-6a8d-4578-b97f-4d9c77123418", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "2f43767a-1994-4202-a411-4738c92223da", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "ov_config = {}\n", + "if \"GPU\" in device.value or \"NPU\" in device.value:\n", + " ov_model.reshape({0: [1, 3, 640, 640]})\n", + "ov_config = {}\n", + "if \"GPU\" in device.value:\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "\n", + "quantized_det_model = core.read_model(int8_model_det_path)\n", + "quantized_det_compiled_model = core.compile_model(quantized_det_model, device.value, ov_config)\n", + "\n", + "ov_yolo_int8_model.predictor.model.ov_compiled_model = quantized_det_compiled_model\n", + "\n", + "res = ov_yolo_int8_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "51d64c7b-7595-41ca-87dd-4c85308f84d6", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "c7b70f75-a706-4f24-a487-88a3ad645dd5", + "source": [ + "## Compare the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "markdown", + "id": "d6561246-a9e0-4cdf-8207-7e2f919d8582", + "source": [ + "### Model size\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "43d84d15-75be-4430-a58b-61cfd8e08dd0", + "source": [ + "ov_model_weights = ov_model_path.with_suffix(\".bin\")\n", + "print(f\"Size of FP16 model is {ov_model_weights.stat().st_size / 1024 / 1024:.2f} MB\")\n", + "if int8_model_det_path.exists():\n", + " ov_int8_weights = int8_model_det_path.with_suffix(\".bin\")\n", + " print(f\"Size of model with INT8 compressed weights is {ov_int8_weights.stat().st_size / 1024 / 1024:.2f} MB\")\n", + " print(f\"Compression rate for INT8 model: {ov_model_weights.stat().st_size / ov_int8_weights.stat().st_size:.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f72b3d84-91f3-493c-8f4e-46598ffbd6d1", + "source": [ + "### Performance\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "### FP16 model performance\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "909ea508-e007-4313-ab0f-bfa0e3906176", + "source": [ + "!benchmark_app -m $ov_model_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "markdown", + "id": "9852a8ef-4c45-43dd-a9ef-53ca6f13c99e", + "source": [ + "### Int8 model performance\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "8254626d-6c4f-4ca3-936f-a7c47e402e03", + "source": [ + "if int8_model_det_path.exists():\n", + " !benchmark_app -m $int8_model_det_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "markdown", + "id": "12d35bc1-8101-45ea-8bd7-53720b98f5e5", + "source": [ + "## Live demo\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The following code runs model inference on a video:" + ] + }, + { + "cell_type": "code", + "id": "246bd379-b1d5-4eb2-928f-97c7c2455a92", + "source": [ + "import collections\n", + "import time\n", + "from IPython import display\n", + "import cv2\n", + "import numpy as np\n", + "\n", + "\n", + "# Main processing function to run object detection.\n", + "def run_object_detection(\n", + " source=0,\n", + " flip=False,\n", + " use_popup=False,\n", + " skip_first_frames=0,\n", + " det_model=ov_yolo_int8_model,\n", + " device=device.value,\n", + "):\n", + " player = None\n", + " try:\n", + " # Create a video player to play with target fps.\n", + " player = VideoPlayer(source=source, flip=flip, fps=30, skip_first_frames=skip_first_frames)\n", + " # Start capturing.\n", + " player.start()\n", + " if use_popup:\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(winname=title, flags=cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_AUTOSIZE)\n", + "\n", + " processing_times = collections.deque()\n", + " while True:\n", + " # Grab the frame.\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + " # If the frame is larger than full HD, reduce size to improve the performance.\n", + " scale = 1280 / max(frame.shape)\n", + " if scale < 1:\n", + " frame = cv2.resize(\n", + " src=frame,\n", + " dsize=None,\n", + " fx=scale,\n", + " fy=scale,\n", + " interpolation=cv2.INTER_AREA,\n", + " )\n", + " # Get the results.\n", + " input_image = np.array(frame)\n", + "\n", + " start_time = time.time()\n", + " detections = det_model(input_image, iou=0.45, conf=0.2, verbose=False)\n", + " stop_time = time.time()\n", + " frame = detections[0].plot()\n", + "\n", + " processing_times.append(stop_time - start_time)\n", + " # Use processing times from last 200 frames.\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " _, f_width = frame.shape[:2]\n", + " # Mean processing time [ms].\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + " cv2.putText(\n", + " img=frame,\n", + " text=f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " org=(20, 40),\n", + " fontFace=cv2.FONT_HERSHEY_COMPLEX,\n", + " fontScale=f_width / 1000,\n", + " color=(0, 0, 255),\n", + " thickness=1,\n", + " lineType=cv2.LINE_AA,\n", + " )\n", + " # Use this workaround if there is flickering.\n", + " if use_popup:\n", + " cv2.imshow(winname=title, mat=frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # Encode numpy array to jpg.\n", + " _, encoded_img = cv2.imencode(ext=\".jpg\", img=frame, params=[cv2.IMWRITE_JPEG_QUALITY, 100])\n", + " # Create an IPython image.\n", + " i = display.Image(data=encoded_img)\n", + " # Display the image in this notebook.\n", + " display.clear_output(wait=True)\n", + " display.display(i)\n", + " # ctrl-c\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " # any different error\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " if player is not None:\n", + " # Stop capturing.\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()" + ] + }, + { + "cell_type": "code", + "id": "8db930f4-fc41-4635-9248-deab2b6cfcb8", + "source": [ + "use_int8 = widgets.Checkbox(\n", + " value=ov_yolo_int8_model is not None,\n", + " description=\"Use int8 model\",\n", + " disabled=ov_yolo_int8_model is None,\n", + ")\n", + "\n", + "use_int8" + ] + }, + { + "cell_type": "code", + "id": "59e37135-1bdc-40ff-8789-b5b4491cab84", + "source": [ + "WEBCAM_INFERENCE = False\n", + "\n", + "if WEBCAM_INFERENCE:\n", + " VIDEO_SOURCE = 0 # Webcam\n", + "else:\n", + " download_file(\n", + " \"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/video/people.mp4\",\n", + " directory=\"data\",\n", + " )\n", + " VIDEO_SOURCE = \"data/people.mp4\"" + ] + }, + { + "cell_type": "code", + "id": "88c6f47c-1c47-451d-89ba-0a29760f5ec4", + "source": [ + "run_object_detection(\n", + " det_model=ov_yolo_model if not use_int8.value else ov_yolo_int8_model,\n", + " source=VIDEO_SOURCE,\n", + " flip=True,\n", + " use_popup=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9f612d80-68e2-45f8-aa62-9f3a05ca9de8", + "source": [ + "### Gradio Interactive Demo\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "ffdbe248-7086-4804-b4b9-c3dd454bf80c", + "source": [ + "import gradio as gr\n", + "\n", + "\n", + "def yolov10_inference(image, int8, conf_threshold, iou_threshold):\n", + " model = ov_yolo_model if not int8 else ov_yolo_int8_model\n", + " results = model(source=image, iou=iou_threshold, conf=conf_threshold, verbose=False)[0]\n", + " annotated_image = Image.fromarray(results.plot())\n", + "\n", + " return annotated_image\n", + "\n", + "\n", + "with gr.Blocks() as demo:\n", + " gr.HTML(\n", + " \"\"\"\n", + "

\n", + " YOLOv10: Real-Time End-to-End Object Detection using OpenVINO\n", + "

\n", + " \"\"\"\n", + " )\n", + " with gr.Row():\n", + " with gr.Column():\n", + " image = gr.Image(type=\"numpy\", label=\"Image\")\n", + " conf_threshold = gr.Slider(\n", + " label=\"Confidence Threshold\",\n", + " minimum=0.1,\n", + " maximum=1.0,\n", + " step=0.1,\n", + " value=0.2,\n", + " )\n", + " iou_threshold = gr.Slider(\n", + " label=\"IoU Threshold\",\n", + " minimum=0.1,\n", + " maximum=1.0,\n", + " step=0.1,\n", + " value=0.45,\n", + " )\n", + " use_int8 = gr.Checkbox(\n", + " value=ov_yolo_int8_model is not None,\n", + " visible=ov_yolo_int8_model is not None,\n", + " label=\"Use INT8 model\",\n", + " )\n", + " yolov10_infer = gr.Button(value=\"Detect Objects\")\n", + "\n", + " with gr.Column():\n", + " output_image = gr.Image(type=\"pil\", label=\"Annotated Image\")\n", + "\n", + " yolov10_infer.click(\n", + " fn=yolov10_inference,\n", + " inputs=[\n", + " image,\n", + " use_int8,\n", + " conf_threshold,\n", + " iou_threshold,\n", + " ],\n", + " outputs=[output_image],\n", + " )\n", + " examples = gr.Examples(\n", + " [\n", + " \"data/coco_bike.jpg\",\n", + " ],\n", + " inputs=[\n", + " image,\n", + " ],\n", + " )\n", + "\n", + "\n", + "try:\n", + " demo.launch(debug=True)\n", + "except Exception:\n", + " demo.launch(debug=True, share=True)" + ] + } + ] + .map(fromJupyterCell) + , + [ + { + "cell_type": "markdown", + "id": "e9d010ff-85ba-4e69-b439-ba89e4a3a715", + "source": [ + "# Convert and Optimize YOLOv10 with OpenVINO\n", + "\n", + "Real-time object detection aims to accurately predict object categories and positions in images with low latency. The YOLO series has been at the forefront of this research due to its balance between performance and efficiency. However, reliance on NMS and architectural inefficiencies have hindered optimal performance. YOLOv10 addresses these issues by introducing consistent dual assignments for NMS-free training and a holistic efficiency-accuracy driven model design strategy.\n", + "\n", + "YOLOv10, built on the [Ultralytics Python package](https://pypi.org/project/ultralytics/) by researchers at [Tsinghua University](https://www.tsinghua.edu.cn/en/), introduces a new approach to real-time object detection, addressing both the post-processing and model architecture deficiencies found in previous YOLO versions. By eliminating non-maximum suppression (NMS) and optimizing various model components, YOLOv10 achieves state-of-the-art performance with significantly reduced computational overhead. Extensive experiments demonstrate its superior accuracy-latency trade-offs across multiple model scales.\n", + "\n", + "![yolov10-approach.png](https://github.com/ultralytics/ultralytics/assets/26833433/f9b1bec0-928e-41ce-a205-e12db3c4929a)\n", + "\n", + "More details about model architecture you can find in original [repo](https://github.com/THU-MIG/yolov10), [paper](https://arxiv.org/abs/2405.14458) and [Ultralytics documentation](https://docs.ultralytics.com/models/yolov10/).\n", + "\n", + "This tutorial demonstrates step-by-step instructions on how to run and optimize PyTorch YOLO V10 with OpenVINO.\n", + "\n", + "The tutorial consists of the following steps:\n", + "\n", + "- Prepare PyTorch model\n", + "- Convert PyTorch model to OpenVINO IR\n", + "- Run model inference with OpenVINO\n", + "- Prepare and run optimization pipeline using NNCF\n", + "- Compare performance of the FP16 and quantized models.\n", + "- Run optimized model inference on video\n", + "- Launch interactive Gradio demo\n", + "\n", + "#### Table of contents:\n", + "\n", + "- [Prerequisites](#Prerequisites)\n", + "- [Download PyTorch model](#Download-PyTorch-model)\n", + "- [Export PyTorch model to OpenVINO IR Format](#Export-PyTorch-model-to-OpenVINO-IR-Format)\n", + "- [Run OpenVINO Inference on AUTO device using Ultralytics API](#Run-OpenVINO-Inference-on-AUTO-device-using-Ultralytics-API)\n", + "- [Run OpenVINO Inference on selected device using Ultralytics API](#Run-OpenVINO-Inference-on-selected-device-using-Ultralytics-API)\n", + "- [Optimize model using NNCF Post-training Quantization API](#Optimize-model-using-NNCF-Post-training-Quantization-API)\n", + " - [Prepare Quantization Dataset](#Prepare-Quantization-Dataset)\n", + " - [Quantize and Save INT8 model](#Quantize-and-Save-INT8-model)\n", + "- [Run Optimized Model Inference](#Run-Optimized-Model-Inference)\n", + " - [Run Optimized Model on AUTO device](#Run-Optimized-Model-on-AUTO-device)\n", + " - [Run Optimized Model Inference on selected device](#Run-Optimized-Model-Inference-on-selected-device)\n", + "- [Compare the Original and Quantized Models](#Compare-the-Original-and-Quantized-Models)\n", + " - [Model size](#Model-size)\n", + " - [Performance](#Performance)\n", + " - [FP16 model performance](#FP16-model-performance)\n", + " - [Int8 model performance](#Int8-model-performance)\n", + "- [Live demo](#Live-demo)\n", + " - [Gradio Interactive Demo](#Gradio-Interactive-Demo)\n", + "\n", + "\n", + "### Installation Instructions\n", + "\n", + "This is a self-contained example that relies solely on its own code.\n", + "\n", + "We recommend running the notebook in a virtual environment. You only need a Jupyter server to start.\n", + "For details, please refer to [Installation Guide](https://github.com/openvinotoolkit/openvino_notebooks/blob/latest/README.md#-installation-guide).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "144e86a1-c220-4e3b-a8b2-8eda443faf2d", + "source": [ + "## Prerequisites\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "bf76580b-860a-4dcd-8b01-f494cdffe37e", + "source": [ + "import os\n", + "\n", + "os.environ[\"GIT_CLONE_PROTECTION_ACTIVE\"] = \"false\"\n", + "\n", + "%pip install -q \"nncf>=2.11.0\"\n", + "%pip install -Uq \"openvino>=2024.3.0\"\n", + "%pip install -q \"git+https://github.com/THU-MIG/yolov10.git\" --extra-index-url https://download.pytorch.org/whl/cpu\n", + "%pip install -q \"torch>=2.1\" \"torchvision>=0.16\" tqdm opencv-python \"gradio>=4.19\" --extra-index-url https://download.pytorch.org/whl/cpu" + ] + }, + { + "cell_type": "code", + "id": "75772eac-565b-4234-82bf-f305e0c1ceda", + "source": [ + "from pathlib import Path\n", + "\n", + "# Fetch `notebook_utils` module\n", + "import requests\n", + "\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/notebook_utils.py\",\n", + ")\n", + "\n", + "open(\"notebook_utils.py\", \"w\").write(r.text)\n", + "\n", + "from notebook_utils import download_file, VideoPlayer, device_widget" + ] + }, + { + "cell_type": "markdown", + "id": "2888d8b4-44f6-4418-bae4-f433ff28010b", + "source": [ + "## Download PyTorch model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "There are several version of [YOLO V10](https://github.com/THU-MIG/yolov10/tree/main?tab=readme-ov-file#performance) models provided by model authors. Each of them has different characteristics depends on number of training parameters, performance and accuracy. For demonstration purposes we will use `yolov10n`, but the same steps are also applicable to other models in YOLO V10 series." + ] + }, + { + "cell_type": "code", + "id": "aea6315b-6a29-4f2a-96a9-eb3c1646a3a4", + "source": [ + "models_dir = Path(\"./models\")\n", + "models_dir.mkdir(exist_ok=True)" + ] + }, + { + "cell_type": "code", + "id": "d828401e-b6bd-4796-a7b8-681957a159d4", + "source": [ + "model_weights_url = \"https://github.com/jameslahm/yolov10/releases/download/v1.0/yolov10n.pt\"\n", + "file_name = model_weights_url.split(\"/\")[-1]\n", + "model_name = file_name.replace(\".pt\", \"\")\n", + "\n", + "download_file(model_weights_url, directory=models_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "dc21487d-dd79-4b56-b8e2-5b28c8d92ac8", + "source": [ + "## Export PyTorch model to OpenVINO IR Format\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "markdown", + "id": "8cfec0d6-fb55-4611-861b-326e892fcf09", + "source": [ + "As it was discussed before, YOLO V10 code is designed on top of [Ultralytics](https://docs.ultralytics.com/) library and has similar interface with YOLO V8 (You can check [YOLO V8 notebooks](https://github.com/openvinotoolkit/openvino_notebooks/tree/latest/notebooks/yolov8-optimization) for more detailed instruction how to work with Ultralytics API). Ultralytics support OpenVINO model export using [export](https://docs.ultralytics.com/modes/export/) method of model class. Additionally, we can specify parameters responsible for target input size, static or dynamic input shapes and model precision (FP32/FP16/INT8). INT8 quantization can be additionally performed on export stage, but for making approach more flexible, we consider how to perform quantization using [NNCF](https://github.com/openvinotoolkit/nncf)." + ] + }, + { + "cell_type": "code", + "id": "d488f564-c1d5-4c6a-9b4a-353a3ab749e6", + "source": [ + "import types\n", + "from ultralytics.utils import ops, yaml_load, yaml_save\n", + "from ultralytics import YOLOv10\n", + "import torch\n", + "\n", + "detection_labels = {\n", + " 0: \"person\",\n", + " 1: \"bicycle\",\n", + " 2: \"car\",\n", + " 3: \"motorcycle\",\n", + " 4: \"airplane\",\n", + " 5: \"bus\",\n", + " 6: \"train\",\n", + " 7: \"truck\",\n", + " 8: \"boat\",\n", + " 9: \"traffic light\",\n", + " 10: \"fire hydrant\",\n", + " 11: \"stop sign\",\n", + " 12: \"parking meter\",\n", + " 13: \"bench\",\n", + " 14: \"bird\",\n", + " 15: \"cat\",\n", + " 16: \"dog\",\n", + " 17: \"horse\",\n", + " 18: \"sheep\",\n", + " 19: \"cow\",\n", + " 20: \"elephant\",\n", + " 21: \"bear\",\n", + " 22: \"zebra\",\n", + " 23: \"giraffe\",\n", + " 24: \"backpack\",\n", + " 25: \"umbrella\",\n", + " 26: \"handbag\",\n", + " 27: \"tie\",\n", + " 28: \"suitcase\",\n", + " 29: \"frisbee\",\n", + " 30: \"skis\",\n", + " 31: \"snowboard\",\n", + " 32: \"sports ball\",\n", + " 33: \"kite\",\n", + " 34: \"baseball bat\",\n", + " 35: \"baseball glove\",\n", + " 36: \"skateboard\",\n", + " 37: \"surfboard\",\n", + " 38: \"tennis racket\",\n", + " 39: \"bottle\",\n", + " 40: \"wine glass\",\n", + " 41: \"cup\",\n", + " 42: \"fork\",\n", + " 43: \"knife\",\n", + " 44: \"spoon\",\n", + " 45: \"bowl\",\n", + " 46: \"banana\",\n", + " 47: \"apple\",\n", + " 48: \"sandwich\",\n", + " 49: \"orange\",\n", + " 50: \"broccoli\",\n", + " 51: \"carrot\",\n", + " 52: \"hot dog\",\n", + " 53: \"pizza\",\n", + " 54: \"donut\",\n", + " 55: \"cake\",\n", + " 56: \"chair\",\n", + " 57: \"couch\",\n", + " 58: \"potted plant\",\n", + " 59: \"bed\",\n", + " 60: \"dining table\",\n", + " 61: \"toilet\",\n", + " 62: \"tv\",\n", + " 63: \"laptop\",\n", + " 64: \"mouse\",\n", + " 65: \"remote\",\n", + " 66: \"keyboard\",\n", + " 67: \"cell phone\",\n", + " 68: \"microwave\",\n", + " 69: \"oven\",\n", + " 70: \"toaster\",\n", + " 71: \"sink\",\n", + " 72: \"refrigerator\",\n", + " 73: \"book\",\n", + " 74: \"clock\",\n", + " 75: \"vase\",\n", + " 76: \"scissors\",\n", + " 77: \"teddy bear\",\n", + " 78: \"hair drier\",\n", + " 79: \"toothbrush\",\n", + "}\n", + "\n", + "\n", + "def v10_det_head_forward(self, x):\n", + " one2one = self.forward_feat([xi.detach() for xi in x], self.one2one_cv2, self.one2one_cv3)\n", + " if not self.export:\n", + " one2many = super().forward(x)\n", + "\n", + " if not self.training:\n", + " one2one = self.inference(one2one)\n", + " if not self.export:\n", + " return {\"one2many\": one2many, \"one2one\": one2one}\n", + " else:\n", + " assert self.max_det != -1\n", + " boxes, scores, labels = ops.v10postprocess(one2one.permute(0, 2, 1), self.max_det, self.nc)\n", + " return torch.cat(\n", + " [boxes, scores.unsqueeze(-1), labels.unsqueeze(-1).to(boxes.dtype)],\n", + " dim=-1,\n", + " )\n", + " else:\n", + " return {\"one2many\": one2many, \"one2one\": one2one}\n", + "\n", + "\n", + "ov_model_path = models_dir / f\"{model_name}_openvino_model/{model_name}.xml\"\n", + "if not ov_model_path.exists():\n", + " model = YOLOv10(models_dir / file_name)\n", + " model.model.model[-1].forward = types.MethodType(v10_det_head_forward, model.model.model[-1])\n", + " model.export(format=\"openvino\", dynamic=True, half=True)\n", + " config = yaml_load(ov_model_path.parent / \"metadata.yaml\")\n", + " config[\"names\"] = detection_labels\n", + " yaml_save(ov_model_path.parent / \"metadata.yaml\", config)" + ] + }, + { + "cell_type": "markdown", + "id": "8a23f621-f6cd-4bfd-8b98-24b8c0392761", + "source": [ + "## Run OpenVINO Inference on AUTO device using Ultralytics API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "Now, when we exported model to OpenVINO, we can load it directly into YOLOv10 class, where automatic inference backend will provide easy-to-use user experience to run OpenVINO YOLOv10 model on the similar level like for original PyTorch model. The code bellow demonstrates how to run inference OpenVINO exported model with Ultralytics API on single image. [AUTO device](https://github.com/openvinotoolkit/openvino_notebooks/tree/latest/notebooks/auto-device) will be used for launching model." + ] + }, + { + "cell_type": "code", + "id": "7e8ed361-bf09-4398-ba15-e6c5dda73cd3", + "source": [ + "ov_yolo_model = YOLOv10(ov_model_path.parent, task=\"detect\")" + ] + }, + { + "cell_type": "code", + "id": "8a473286-59b8-498f-8541-df84f041e119", + "source": [ + "from PIL import Image\n", + "\n", + "IMAGE_PATH = Path(\"./data/coco_bike.jpg\")\n", + "download_file(\n", + " url=\"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/image/coco_bike.jpg\",\n", + " filename=IMAGE_PATH.name,\n", + " directory=IMAGE_PATH.parent,\n", + ")" + ] + }, + { + "cell_type": "code", + "id": "7e116e81-0ad4-4b9e-9f5c-58680fd8f0c6", + "source": [ + "res = ov_yolo_model(IMAGE_PATH, iou=0.45, conf=0.2)\n", + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "943b9b04-4b97-4395-99d1-695465de1140", + "source": [ + "## Run OpenVINO Inference on selected device using Ultralytics API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "In this part of notebook you can select inference device for running model inference to compare results with AUTO." + ] + }, + { + "cell_type": "code", + "id": "41655bec-b006-4376-abbb-d6c8c09e89fb", + "source": [ + "device = device_widget(\"CPU\")\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "f284d5fd-12ba-4c08-afce-d8131ef7ad4d", + "source": [ + "import openvino as ov\n", + "\n", + "core = ov.Core()\n", + "\n", + "ov_model = core.read_model(ov_model_path)\n", + "\n", + "# load model on selected device\n", + "if \"GPU\" in device.value or \"NPU\" in device.value:\n", + " ov_model.reshape({0: [1, 3, 640, 640]})\n", + "ov_config = {}\n", + "if \"GPU\" in device.value:\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "det_compiled_model = core.compile_model(ov_model, device.value, ov_config)" + ] + }, + { + "cell_type": "code", + "id": "12164d21-948a-4ef7-9e1c-b0d41c53cf91", + "source": [ + "ov_yolo_model.predictor.model.ov_compiled_model = det_compiled_model" + ] + }, + { + "cell_type": "code", + "id": "ecb9d396-ab1e-4398-a110-18c08b00ab14", + "source": [ + "res = ov_yolo_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "a4abf758-3a72-4ee0-afde-38076602f1c7", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "992b6ac3-4248-4848-8fd6-f9c1b9fc2051", + "source": [ + "## Optimize model using NNCF Post-training Quantization API\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "[NNCF](https://github.com/openvinotoolkit/nncf) provides a suite of advanced algorithms for Neural Networks inference optimization in OpenVINO with minimal accuracy drop.\n", + "We will use 8-bit quantization in post-training mode (without the fine-tuning pipeline) to optimize YOLOv10.\n", + "\n", + "The optimization process contains the following steps:\n", + "\n", + "1. Create a Dataset for quantization.\n", + "2. Run `nncf.quantize` for getting an optimized model.\n", + "3. Serialize OpenVINO IR model, using the `openvino.save_model` function.\n", + "\n", + "Quantization is time and memory consuming process, you can skip this step using checkbox bellow:" + ] + }, + { + "cell_type": "code", + "id": "ed9d9ce5-8df9-4803-ae85-bf0744de914c", + "source": [ + "import ipywidgets as widgets\n", + "\n", + "int8_model_det_path = models_dir / \"int8\" / f\"{model_name}_openvino_model/{model_name}.xml\"\n", + "ov_yolo_int8_model = None\n", + "\n", + "to_quantize = widgets.Checkbox(\n", + " value=True,\n", + " description=\"Quantization\",\n", + " disabled=False,\n", + ")\n", + "\n", + "to_quantize" + ] + }, + { + "cell_type": "code", + "id": "52287a23-b5ef-4c34-873a-32b398d26d1a", + "source": [ + "# Fetch skip_kernel_extension module\n", + "r = requests.get(\n", + " url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/utils/skip_kernel_extension.py\",\n", + ")\n", + "open(\"skip_kernel_extension.py\", \"w\").write(r.text)\n", + "\n", + "%load_ext skip_kernel_extension" + ] + }, + { + "cell_type": "markdown", + "id": "89ba64af-aa73-4ed9-a2f9-527bd9ae8221", + "source": [ + "### Prepare Quantization Dataset\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "For starting quantization, we need to prepare dataset. We will use validation subset from [MS COCO dataset](https://cocodataset.org/) for model quantization and Ultralytics validation data loader for preparing input data." + ] + }, + { + "cell_type": "code", + "id": "5e10435e-9cd6-4f29-a865-721d6209314d", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "from zipfile import ZipFile\n", + "\n", + "from ultralytics.data.utils import DATASETS_DIR\n", + "\n", + "if not int8_model_det_path.exists():\n", + "\n", + " DATA_URL = \"http://images.cocodataset.org/zips/val2017.zip\"\n", + " LABELS_URL = \"https://github.com/ultralytics/yolov5/releases/download/v1.0/coco2017labels-segments.zip\"\n", + " CFG_URL = \"https://raw.githubusercontent.com/ultralytics/ultralytics/v8.1.0/ultralytics/cfg/datasets/coco.yaml\"\n", + "\n", + " OUT_DIR = DATASETS_DIR\n", + "\n", + " DATA_PATH = OUT_DIR / \"val2017.zip\"\n", + " LABELS_PATH = OUT_DIR / \"coco2017labels-segments.zip\"\n", + " CFG_PATH = OUT_DIR / \"coco.yaml\"\n", + "\n", + " download_file(DATA_URL, DATA_PATH.name, DATA_PATH.parent)\n", + " download_file(LABELS_URL, LABELS_PATH.name, LABELS_PATH.parent)\n", + " download_file(CFG_URL, CFG_PATH.name, CFG_PATH.parent)\n", + "\n", + " if not (OUT_DIR / \"coco/labels\").exists():\n", + " with ZipFile(LABELS_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR)\n", + " with ZipFile(DATA_PATH, \"r\") as zip_ref:\n", + " zip_ref.extractall(OUT_DIR / \"coco/images\")" + ] + }, + { + "cell_type": "code", + "id": "b0a14818-21fd-4c71-8e58-2297dfafaadd", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "from ultralytics.utils import DEFAULT_CFG\n", + "from ultralytics.cfg import get_cfg\n", + "from ultralytics.data.converter import coco80_to_coco91_class\n", + "from ultralytics.data.utils import check_det_dataset\n", + "\n", + "if not int8_model_det_path.exists():\n", + " args = get_cfg(cfg=DEFAULT_CFG)\n", + " args.data = str(CFG_PATH)\n", + " det_validator = ov_yolo_model.task_map[ov_yolo_model.task][\"validator\"](args=args)\n", + "\n", + " det_validator.data = check_det_dataset(args.data)\n", + " det_validator.stride = 32\n", + " det_data_loader = det_validator.get_dataloader(OUT_DIR / \"coco\", 1)" + ] + }, + { + "cell_type": "markdown", + "id": "83fa49f4-e40c-48c7-82a6-8ef56e20b343", + "source": [ + "NNCF provides `nncf.Dataset` wrapper for using native framework dataloaders in quantization pipeline. Additionally, we specify transform function that will be responsible for preparing input data in model expected format." + ] + }, + { + "cell_type": "code", + "id": "253f0e0f-131e-43b9-b1be-639d4b3d1fe5", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "import nncf\n", + "from typing import Dict\n", + "\n", + "\n", + "def transform_fn(data_item:Dict):\n", + " \"\"\"\n", + " Quantization transform function. Extracts and preprocess input data from dataloader item for quantization.\n", + " Parameters:\n", + " data_item: Dict with data item produced by DataLoader during iteration\n", + " Returns:\n", + " input_tensor: Input data for quantization\n", + " \"\"\"\n", + " input_tensor = det_validator.preprocess(data_item)['img'].numpy()\n", + " return input_tensor\n", + "\n", + "if not int8_model_det_path.exists():\n", + " quantization_dataset = nncf.Dataset(det_data_loader, transform_fn)" + ] + }, + { + "cell_type": "markdown", + "id": "ff8a6372-2097-4308-a4e5-e4651242a8df", + "source": [ + "### Quantize and Save INT8 model\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The `nncf.quantize` function provides an interface for model quantization. It requires an instance of the OpenVINO Model and quantization dataset. \n", + "Optionally, some additional parameters for the configuration quantization process (number of samples for quantization, preset, ignored scope, etc.) can be provided. YOLOv10 model contains non-ReLU activation functions, which require asymmetric quantization of activations. To achieve a better result, we will use a `mixed` quantization preset. It provides symmetric quantization of weights and asymmetric quantization of activations.\n", + "\n", + ">**Note**: Model post-training quantization is time-consuming process. Be patient, it can take several minutes depending on your hardware." + ] + }, + { + "cell_type": "code", + "id": "4c12ae38-239e-4fe1-8296-47567f98538c", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "import shutil\n", + "\n", + "if not int8_model_det_path.exists():\n", + " quantized_det_model = nncf.quantize(\n", + " ov_model,\n", + " quantization_dataset,\n", + " preset=nncf.QuantizationPreset.MIXED,\n", + " )\n", + "\n", + " ov.save_model(quantized_det_model, int8_model_det_path)\n", + " shutil.copy(ov_model_path.parent / \"metadata.yaml\", int8_model_det_path.parent / \"metadata.yaml\")" + ] + }, + { + "cell_type": "markdown", + "id": "41b8a3eb-adb8-40cf-8602-311ff8d75a76", + "source": [ + "## Run Optimized Model Inference\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The way of usage INT8 quantized model is the same like for model before quantization. Let's check inference result of quantized model on single image" + ] + }, + { + "cell_type": "markdown", + "id": "f438df41-d614-4cca-a746-a173d92dddf5", + "source": [ + "### Run Optimized Model on AUTO device\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "af61303b-16bd-4206-b926-2c28b3a859a4", + "source": [ + "%%skip not $to_quantize.value\n", + "ov_yolo_int8_model = YOLOv10(int8_model_det_path.parent, task=\"detect\")" + ] + }, + { + "cell_type": "code", + "id": "db13b8dd-49a1-4046-8a1a-491eee095a1b", + "source": [ + "%%skip not $to_quantize.value\n", + "res = ov_yolo_int8_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "0423ba31-f0ee-4bfc-a18a-f7b870a9683d", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "4b4f7118-e046-4f87-b403-87053f2b0ac3", + "source": [ + "### Run Optimized Model Inference on selected device\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "142f22ae-6a8d-4578-b97f-4d9c77123418", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "device" + ] + }, + { + "cell_type": "code", + "id": "2f43767a-1994-4202-a411-4738c92223da", + "source": [ + "%%skip not $to_quantize.value\n", + "\n", + "ov_config = {}\n", + "if \"GPU\" in device.value or \"NPU\" in device.value:\n", + " ov_model.reshape({0: [1, 3, 640, 640]})\n", + "ov_config = {}\n", + "if \"GPU\" in device.value:\n", + " ov_config = {\"GPU_DISABLE_WINOGRAD_CONVOLUTION\": \"YES\"}\n", + "\n", + "quantized_det_model = core.read_model(int8_model_det_path)\n", + "quantized_det_compiled_model = core.compile_model(quantized_det_model, device.value, ov_config)\n", + "\n", + "ov_yolo_int8_model.predictor.model.ov_compiled_model = quantized_det_compiled_model\n", + "\n", + "res = ov_yolo_int8_model(IMAGE_PATH, iou=0.45, conf=0.2)" + ] + }, + { + "cell_type": "code", + "id": "51d64c7b-7595-41ca-87dd-4c85308f84d6", + "source": [ + "Image.fromarray(res[0].plot()[:, :, ::-1])" + ] + }, + { + "cell_type": "markdown", + "id": "c7b70f75-a706-4f24-a487-88a3ad645dd5", + "source": [ + "## Compare the Original and Quantized Models\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "markdown", + "id": "d6561246-a9e0-4cdf-8207-7e2f919d8582", + "source": [ + "### Model size\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "43d84d15-75be-4430-a58b-61cfd8e08dd0", + "source": [ + "ov_model_weights = ov_model_path.with_suffix(\".bin\")\n", + "print(f\"Size of FP16 model is {ov_model_weights.stat().st_size / 1024 / 1024:.2f} MB\")\n", + "if int8_model_det_path.exists():\n", + " ov_int8_weights = int8_model_det_path.with_suffix(\".bin\")\n", + " print(f\"Size of model with INT8 compressed weights is {ov_int8_weights.stat().st_size / 1024 / 1024:.2f} MB\")\n", + " print(f\"Compression rate for INT8 model: {ov_model_weights.stat().st_size / ov_int8_weights.stat().st_size:.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f72b3d84-91f3-493c-8f4e-46598ffbd6d1", + "source": [ + "### Performance\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "### FP16 model performance\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "909ea508-e007-4313-ab0f-bfa0e3906176", + "source": [ + "!benchmark_app -m $ov_model_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "markdown", + "id": "9852a8ef-4c45-43dd-a9ef-53ca6f13c99e", + "source": [ + "### Int8 model performance\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "8254626d-6c4f-4ca3-936f-a7c47e402e03", + "source": [ + "if int8_model_det_path.exists():\n", + " !benchmark_app -m $int8_model_det_path -d $device.value -api async -shape \"[1,3,640,640]\" -t 15" + ] + }, + { + "cell_type": "markdown", + "id": "12d35bc1-8101-45ea-8bd7-53720b98f5e5", + "source": [ + "## Live demo\n", + "[back to top ⬆️](#Table-of-contents:)\n", + "\n", + "\n", + "The following code runs model inference on a video:" + ] + }, + { + "cell_type": "code", + "id": "246bd379-b1d5-4eb2-928f-97c7c2455a92", + "source": [ + "import collections\n", + "import time\n", + "from IPython import display\n", + "import cv2\n", + "import numpy as np\n", + "\n", + "\n", + "# Main processing function to run object detection.\n", + "def run_object_detection(\n", + " source=0,\n", + " flip=False,\n", + " use_popup=False,\n", + " skip_first_frames=0,\n", + " det_model=ov_yolo_int8_model,\n", + " device=device.value,\n", + "):\n", + " player = None\n", + " try:\n", + " # Create a video player to play with target fps.\n", + " player = VideoPlayer(source=source, flip=flip, fps=30, skip_first_frames=skip_first_frames)\n", + " # Start capturing.\n", + " player.start()\n", + " if use_popup:\n", + " title = \"Press ESC to Exit\"\n", + " cv2.namedWindow(winname=title, flags=cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_AUTOSIZE)\n", + "\n", + " processing_times = collections.deque()\n", + " while True:\n", + " # Grab the frame.\n", + " frame = player.next()\n", + " if frame is None:\n", + " print(\"Source ended\")\n", + " break\n", + " # If the frame is larger than full HD, reduce size to improve the performance.\n", + " scale = 1280 / max(frame.shape)\n", + " if scale < 1:\n", + " frame = cv2.resize(\n", + " src=frame,\n", + " dsize=None,\n", + " fx=scale,\n", + " fy=scale,\n", + " interpolation=cv2.INTER_AREA,\n", + " )\n", + " # Get the results.\n", + " input_image = np.array(frame)\n", + "\n", + " start_time = time.time()\n", + " detections = det_model(input_image, iou=0.45, conf=0.2, verbose=False)\n", + " stop_time = time.time()\n", + " frame = detections[0].plot()\n", + "\n", + " processing_times.append(stop_time - start_time)\n", + " # Use processing times from last 200 frames.\n", + " if len(processing_times) > 200:\n", + " processing_times.popleft()\n", + "\n", + " _, f_width = frame.shape[:2]\n", + " # Mean processing time [ms].\n", + " processing_time = np.mean(processing_times) * 1000\n", + " fps = 1000 / processing_time\n", + " cv2.putText(\n", + " img=frame,\n", + " text=f\"Inference time: {processing_time:.1f}ms ({fps:.1f} FPS)\",\n", + " org=(20, 40),\n", + " fontFace=cv2.FONT_HERSHEY_COMPLEX,\n", + " fontScale=f_width / 1000,\n", + " color=(0, 0, 255),\n", + " thickness=1,\n", + " lineType=cv2.LINE_AA,\n", + " )\n", + " # Use this workaround if there is flickering.\n", + " if use_popup:\n", + " cv2.imshow(winname=title, mat=frame)\n", + " key = cv2.waitKey(1)\n", + " # escape = 27\n", + " if key == 27:\n", + " break\n", + " else:\n", + " # Encode numpy array to jpg.\n", + " _, encoded_img = cv2.imencode(ext=\".jpg\", img=frame, params=[cv2.IMWRITE_JPEG_QUALITY, 100])\n", + " # Create an IPython image.\n", + " i = display.Image(data=encoded_img)\n", + " # Display the image in this notebook.\n", + " display.clear_output(wait=True)\n", + " display.display(i)\n", + " # ctrl-c\n", + " except KeyboardInterrupt:\n", + " print(\"Interrupted\")\n", + " # any different error\n", + " except RuntimeError as e:\n", + " print(e)\n", + " finally:\n", + " if player is not None:\n", + " # Stop capturing.\n", + " player.stop()\n", + " if use_popup:\n", + " cv2.destroyAllWindows()" + ] + }, + { + "cell_type": "code", + "id": "8db930f4-fc41-4635-9248-deab2b6cfcb8", + "source": [ + "use_int8 = widgets.Checkbox(\n", + " value=ov_yolo_int8_model is not None,\n", + " description=\"Use int8 model\",\n", + " disabled=ov_yolo_int8_model is None,\n", + ")\n", + "\n", + "use_int8" + ] + }, + { + "cell_type": "code", + "id": "59e37135-1bdc-40ff-8789-b5b4491cab84", + "source": [ + "WEBCAM_INFERENCE = False\n", + "\n", + "if WEBCAM_INFERENCE:\n", + " VIDEO_SOURCE = 0 # Webcam\n", + "else:\n", + " download_file(\n", + " \"https://storage.openvinotoolkit.org/repositories/openvino_notebooks/data/data/video/people.mp4\",\n", + " directory=\"data\",\n", + " )\n", + " VIDEO_SOURCE = \"data/people.mp4\"" + ] + }, + { + "cell_type": "code", + "id": "88c6f47c-1c47-451d-89ba-0a29760f5ec4", + "source": [ + "run_object_detection(\n", + " det_model=ov_yolo_model if not use_int8.value else ov_yolo_int8_model,\n", + " source=VIDEO_SOURCE,\n", + " flip=True,\n", + " use_popup=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9f612d80-68e2-45f8-aa62-9f3a05ca9de8", + "source": [ + "### Gradio Interactive Demo\n", + "[back to top ⬆️](#Table-of-contents:)" + ] + }, + { + "cell_type": "code", + "id": "2e5a6249", + "source": [ + "def yolov10_inference(image, int8, conf_threshold, iou_threshold):\n", + " model = ov_yolo_model if not int8 else ov_yolo_int8_model\n", + " results = model(source=image, iou=iou_threshold, conf=conf_threshold, verbose=False)[0]\n", + " annotated_image = Image.fromarray(results.plot())\n", + "\n", + " return annotated_image" + ] + }, + { + "cell_type": "code", + "id": "ffdbe248-7086-4804-b4b9-c3dd454bf80c", + "source": [ + "if not Path(\"gradio_helper.py\").exists():\n", + " r = requests.get(url=\"https://raw.githubusercontent.com/openvinotoolkit/openvino_notebooks/latest/notebooks/yolov10-optimization/gradio_helper.py\")\n", + " open(\"gradio_helper.py\", \"w\").write(r.text)\n", + "\n", + "from gradio_helper import make_demo\n", + "\n", + "demo = make_demo(fn=yolov10_inference, quantized=ov_yolo_int8_model is not None)\n", + "\n", + "try:\n", + " demo.launch(debug=True)\n", + "except Exception:\n", + " demo.launch(debug=True, share=True)\n", + "# If you are launching remotely, specify server_name and server_port\n", + "# EXAMPLE: `demo.launch(server_name='your server name', server_port='server port in int')`\n", + "# To learn more please refer to the Gradio docs: https://gradio.app/docs/" + ] + }, + { + "cell_type": "code", + "id": "4b123c5e", + "source": [ + "# please uncomment and run this cell for stopping gradio interface\n", + "# demo.close()" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 18 }, + { modified: 19, original: 19 }, + { modified: 20, original: 20 }, + { modified: 21, original: 21 }, + { modified: 22, original: 22 }, + { modified: 23, original: 23 }, + { modified: 24, original: 24 }, + { modified: 25, original: 25 }, + { modified: 26, original: 26 }, + { modified: 27, original: 27 }, + { modified: 28, original: 28 }, + { modified: 29, original: 29 }, + { modified: 30, original: 30 }, + { modified: 31, original: 31 }, + { modified: 32, original: 32 }, + { modified: 33, original: 33 }, + { modified: 34, original: 34 }, + { modified: 35, original: 35 }, + { modified: 36, original: 36 }, + { modified: 37, original: 37 }, + { modified: 38, original: 38 }, + { modified: 39, original: 39 }, + { modified: 40, original: 40 }, + { modified: 41, original: 41 }, + { modified: 42, original: 42 }, + { modified: 43, original: 43 }, + { modified: 44, original: 44 }, + { modified: 45, original: 45 }, + { modified: 46, original: 46 }, + { modified: 47, original: 47 }, + { modified: 48, original: 48 }, + { modified: 49, original: 49 }, + { modified: 50, original: 50 }, + { modified: 51, original: 51 }, + { modified: 52, original: 52 }, + { modified: 53, original: -1 }, + { modified: 54, original: -1 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 52, originalLength: 1, modifiedStart: 52, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 53, originalLength: 0, modifiedStart: 53, modifiedLength: 2 } satisfies IDiffChange, + ]); + + }); + test('Misalignment due to 2 deleted cells', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "markdown", + "metadata": { + "id": "F0Wut2G5kz2Z" + }, + "source": [ + "[![Roboflow Notebooks](https://ik.imagekit.io/roboflow/notebooks/template/bannertest2-2.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672932710194)](https://github.com/roboflow/notebooks)\n", + "\n", + "# Fast Segment Anything Model (FastSAM)\n", + "\n", + "---\n", + "\n", + "[![GitHub](https://badges.aleen42.com/src/github.svg)](https://github.com/CASIA-IVA-Lab/FastSAM)\n", + "[![arXiv](https://img.shields.io/badge/arXiv-2306.12156-b31b1b.svg)](https://arxiv.org/pdf/2306.12156.pdf)\n", + "[![YouTube](https://badges.aleen42.com/src/youtube.svg)](https://youtu.be/yHNPyqazYYU)\n", + "[![Roboflow](https://raw.githubusercontent.com/roboflow-ai/notebooks/main/assets/badges/roboflow-blogpost.svg)](https://blog.roboflow.com/how-to-use-fastsam)\n", + "\n", + "The Fast Segment Anything Model (FastSAM) is a CNN Segment Anything Model trained by only 2% of the SA-1B dataset published by SAM authors.\n", + "\n", + "![fast-segment-anything-model-figure-1](https://media.roboflow.com/notebooks/examples/fast-sam-figure-1.png)\n", + "\n", + "The FastSAM authors claim it achieves comparable performance to the SAM method at 50 times the speed.\n", + "\n", + "![fast-segment-anything-model-figure-2](https://media.roboflow.com/notebooks/examples/fast-sam-figure-2.png)\n", + "\n", + "## Complementary Materials Covering SAM\n", + "\n", + "---\n", + "\n", + "[![GitHub](https://badges.aleen42.com/src/github.svg)](https://github.com/facebookresearch/segment-anything)\n", + "[![arXiv](https://img.shields.io/badge/arXiv-2304.02643-b31b1b.svg)](https://arxiv.org/abs/2304.02643)\n", + "[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-segment-anything-with-sam.ipynb)\n", + "[![YouTube](https://badges.aleen42.com/src/youtube.svg)](https://youtu.be/D-D6ZmadzPE)\n", + "[![Roboflow](https://raw.githubusercontent.com/roboflow-ai/notebooks/main/assets/badges/roboflow-blogpost.svg)](https://blog.roboflow.com/how-to-use-segment-anything-model-sam)\n", + "\n", + "## Steps in this Tutorial\n", + "\n", + "In this tutorial, we are going to cover:\n", + "\n", + "- Before you start\n", + "- Install FastSAM, SAM, and other dependencies\n", + "- Download FastSAM and SAM weights\n", + "- Download example data\n", + "- Imports\n", + "- Load FastSAM\n", + "- FastSAM inference\n", + "- FastSAM box prompt inference\n", + "- FastSAM point prompt inference\n", + "- FastSAM text prompt inference\n", + "- SAM vs FastSAM\n", + "- Roboflow benchmark dataset\n", + "\n", + "## 🔥 Let's begin!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W4u6KjiVdNZM" + }, + "source": [ + "## Before you start\n", + "\n", + "Let's make sure that we have access to GPU. We can use `nvidia-smi` command to do that. In case of any problems navigate to `Edit` -> `Notebook settings` -> `Hardware accelerator`, set it to `GPU`, and then click `Save`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Frcrk09FhJeV", + "outputId": "683a378e-4a8f-4239-901b-037279e92e2b" + }, + "outputs": [], + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PUACTM2zdmJI" + }, + "source": [ + "**NOTE:** To make it easier for us to manage datasets, images and models we create a `HOME` constant." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "PRtHQpTNdnm7", + "outputId": "2824c70d-dcf9-4006-b32d-adc6797fd708" + }, + "outputs": [], + "source": [ + "import os\n", + "HOME = os.getcwd()\n", + "print(\"HOME:\", HOME)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YN3DPGZSn57p" + }, + "source": [ + "## Install FastSAM, SAM, and other dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Vr3kcq1KfFir", + "outputId": "234e5336-ba28-4114-d9e7-efd7f2752ca8" + }, + "outputs": [], + "source": [ + "%cd {HOME}\n", + "\n", + "# install FastSAM\n", + "!git clone https://github.com/CASIA-IVA-Lab/FastSAM.git\n", + "!pip -q install -r FastSAM/requirements.txt\n", + "# install CLIP\n", + "!pip -q install git+https://github.com/openai/CLIP.git\n", + "# install SAM\n", + "!pip -q install git+https://github.com/facebookresearch/segment-anything.git\n", + "# install other dependencies\n", + "!pip -q install roboflow supervision jupyter_bbox_widget" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BMmgSfDWfFis" + }, + "source": [ + "## Download FastSAM and SAM weights" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "ADpOBElSfFis", + "outputId": "b3d89d4c-e2ad-4f5e-a910-b52695263151" + }, + "outputs": [], + "source": [ + "!mkdir -p {HOME}/weights\n", + "!wget -P {HOME}/weights -q https://huggingface.co/spaces/An-619/FastSAM/resolve/main/weights/FastSAM.pt\n", + "!wget -P {HOME}/weights -q https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth\n", + "!ls -lh {HOME}/weights" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "W6w1ck4z72sn" + }, + "outputs": [], + "source": [ + "FAST_SAM_CHECKPOINT_PATH = f\"{HOME}/weights/FastSAM.pt\"\n", + "SAM_SAM_CHECKPOINT_PATH = f\"{HOME}/weights/sam_vit_h_4b8939.pth\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t1ef2h1R9WMb" + }, + "source": [ + "## Download example data\n", + "\n", + "**NONE:** Let's download few example images. Feel free to use your images or videos." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "CL4gVrnl9dmR", + "outputId": "db8f78d2-eaf4-41de-9d1f-e5fd6b588a87" + }, + "outputs": [], + "source": [ + "!mkdir -p {HOME}/data\n", + "!wget -P {HOME}/data -q https://media.roboflow.com/notebooks/examples/dog.jpeg\n", + "!wget -P {HOME}/data -q https://media.roboflow.com/notebooks/examples/robot.jpeg\n", + "!ls -lh {HOME}/data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J_a_icikYiVN" + }, + "source": [ + "## Imports\n", + "\n", + "**NOTE:** `FastSAM` code is not distributed via `pip` not it is packaged. Make sure to run code below from `{HOME}/FastSAM` directory. ⚠️" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "u_NVTXOwYppS", + "outputId": "b86184de-5827-4b9c-eb5c-959539abe699" + }, + "outputs": [], + "source": [ + "%cd {HOME}/FastSAM\n", + "\n", + "import os\n", + "import cv2\n", + "import torch\n", + "import roboflow\n", + "import base64\n", + "\n", + "import supervision as sv\n", + "import numpy as np\n", + "\n", + "from roboflow import Roboflow\n", + "from fastsam import FastSAM, FastSAMPrompt\n", + "from segment_anything import sam_model_registry, SamAutomaticMaskGenerator, SamPredictor" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a_Iizsy07pb7" + }, + "source": [ + "## Load FastSAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ao8tVDVq7ebG" + }, + "outputs": [], + "source": [ + "DEVICE = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n", + "print(f\"DEVICE = {DEVICE}\")\n", + "fast_sam = FastSAM(FAST_SAM_CHECKPOINT_PATH)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SvsRFtw_--Nn" + }, + "source": [ + "## FastSAM inference\n", + "\n", + "* `retina_masks=True` determines whether the model uses retina masks for generating segmentation masks.\n", + "* `imgsz=1024` sets the input image size to 1024x1024 pixels for processing by the model.\n", + "* `conf=0.4` sets the minimum confidence threshold for object detection.\n", + "* `iou=0.9` sets the minimum intersection over union threshold for non-maximum suppression to filter out duplicate detections." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "L49P5e-0Iaea" + }, + "outputs": [], + "source": [ + "IMAGE_PATH = f\"{HOME}/data/dog.jpeg\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os.makedirs(f\"{HOME}/output\", exist_ok=True)\n", + "output_image_path = f\"{HOME}/output/output-{os.path.basename(IMAGE_PATH)}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "KV4XzCg1_fAS", + "outputId": "1df75d90-cf73-4d78-feed-01911a81f6e4" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.4,\n", + " iou=0.9)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()\n", + "prompt_process.plot(annotations=masks, output=f\"{HOME}/output\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uPrxqbaBOBMw" + }, + "source": [ + "**NOTE:** `prompt_process.everything_prompt` returns `torch.Tensor` when DEVICE = 'cuda:0'. `prompt_process.everything_prompt` returns `numpy.ndarray` when DEVICE = 'cpu'." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Convert masks to boolean (True/False)\n", + "def masks_to_bool(masks):\n", + " if type(masks) == np.ndarray:\n", + " masks = masks.astype(bool)\n", + " else:\n", + " masks = masks.cpu().numpy().astype(bool)\n", + " return masks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PUJkojNiKBQa" + }, + "outputs": [], + "source": [ + "def annotate_image(image_path: str, masks: np.ndarray) -> np.ndarray:\n", + " image = cv2.imread(image_path)\n", + "\n", + " xyxy = sv.mask_to_xyxy(masks=masks)\n", + " detections = sv.Detections(xyxy=xyxy, mask=masks)\n", + "\n", + " mask_annotator = sv.MaskAnnotator(color_lookup = sv.ColorLookup.INDEX)\n", + " return mask_annotator.annotate(scene=image.copy(), detections=detections)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "wwtmiH6pLA8N", + "outputId": "17948850-f051-4a1f-b2b8-d01c2a1f4f20" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jYkdczKoRUVB" + }, + "source": [ + "**NOTE:** The quality of the generated masks is quite poor. Let's see if we can improve it by manipulating the parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "dDFGi6EhO2ul", + "outputId": "f5a03357-eb37-446e-dce7-0659e4c2a368" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "OiBPjQW-O59c", + "outputId": "afb011fc-933e-4d69-b4dd-e5e2713f407b" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GoymBQ16KJbv" + }, + "source": [ + "## FastSAM box prompt inference" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "w1_T5SXpMNku" + }, + "source": [ + "### Draw Box" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "04-1G1WrMJUm" + }, + "outputs": [], + "source": [ + "# helper function that loads an image before adding it to the widget\n", + "\n", + "def encode_image(filepath):\n", + " with open(filepath, 'rb') as f:\n", + " image_bytes = f.read()\n", + " encoded = str(base64.b64encode(image_bytes), 'utf-8')\n", + " return \"data:image/jpg;base64,\"+encoded" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xrxd-Ug9MSWG" + }, + "source": [ + "**NOTE:** Execute cell below and use your mouse to draw bounding box on the image 👇" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "a747e095e21448f29d8697fb6b1a7a74", + "6401ca8c2f574ba5ab62f0f06b9e0d06" + ] + }, + "id": "nLA7xmDIMTfx", + "outputId": "06b51195-d54f-44ed-9236-258e94d624c5" + }, + "outputs": [], + "source": [ + "IS_COLAB = True\n", + "\n", + "if IS_COLAB:\n", + " from google.colab import output\n", + " output.enable_custom_widget_manager()\n", + "\n", + "from jupyter_bbox_widget import BBoxWidget\n", + "\n", + "widget = BBoxWidget()\n", + "widget.image = encode_image(IMAGE_PATH)\n", + "widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "td6tYT4INOyD", + "outputId": "73a4e886-dcb8-447c-d3f7-1b3a6496e44d" + }, + "outputs": [], + "source": [ + "widget.bboxes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XfUvm9bwNUAe" + }, + "source": [ + "### Generate mask with FastSAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bI6kPDSROBI-" + }, + "outputs": [], + "source": [ + "# default_box is going to be used if you will not draw any box on image above\n", + "default_box = {'x': 68, 'y': 247, 'width': 555, 'height': 678, 'label': ''}\n", + "\n", + "box = widget.bboxes[0] if widget.bboxes else default_box\n", + "box = [\n", + " box['x'],\n", + " box['y'],\n", + " box['x'] + box['width'],\n", + " box['y'] + box['height']\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LF879hDqO5iB", + "outputId": "4e7498c4-4007-4614-d46c-0e35608a04a2" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.box_prompt(bbox=box)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yt6i2sNJPEdk" + }, + "source": [ + "**NOTE:** `prompt_process.box_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "EJThTct_PA68", + "outputId": "f6eddd24-f603-4702-8325-6cff7925fe0f" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vR-THJthhWH5" + }, + "source": [ + "## FastSAM point prompt inference" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xyfpOPPwhpMy" + }, + "source": [ + "### Select point" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "OC7d3pZ3hop9" + }, + "outputs": [], + "source": [ + "point = [405, 505]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rE5tV4e3h6nG" + }, + "source": [ + "### Generate mask with FastSAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "31FIby6Zh-Wa", + "outputId": "a12c4d1c-837b-4b1b-9240-664797442413" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.point_prompt(points=[point], pointlabel=[1])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d7tpVo8CiM3s" + }, + "source": [ + "**NOTE:** `prompt_process.point_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "lMVpGxShiPky", + "outputId": "224d84ee-e664-400b-c0df-49507d229f62" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HOC7v6ATHEr6" + }, + "source": [ + "## FastSAM text prompt inference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "4CzLLtXOHeGk", + "outputId": "cecc50d8-e2be-4b33-850a-a2442e3e30b6" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.text_prompt(text='cap')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KYW79zD0OKsv" + }, + "source": [ + "**NOTE:** `prompt_process.text_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "zODY5xU1LMCb", + "outputId": "a01540a7-deb3-4555-916c-2ac59928e0f3" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XFOUWUMZEk7m" + }, + "source": [ + "## SAM vs FastSAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7PKBZ7Hhahhw" + }, + "outputs": [], + "source": [ + "IMAGE_PATH = f\"{HOME}/data/robot.jpeg\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c48hRY7dVJeL" + }, + "source": [ + "### Load SAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "PdKpFCCmaETG" + }, + "outputs": [], + "source": [ + "MODEL_TYPE = \"vit_h\"\n", + "\n", + "sam = sam_model_registry[MODEL_TYPE](checkpoint=SAM_SAM_CHECKPOINT_PATH).to(device=DEVICE)\n", + "mask_generator = SamAutomaticMaskGenerator(sam)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V5bPGvtBaewO" + }, + "source": [ + "### SAM inference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0HgWsk1yalow" + }, + "outputs": [], + "source": [ + "image_bgr = cv2.imread(IMAGE_PATH)\n", + "image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)\n", + "\n", + "sam_result = mask_generator.generate(image_rgb)\n", + "sam_detections = sv.Detections.from_sam(sam_result=sam_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t03RrSzMasN6" + }, + "source": [ + "### FastSAM inference" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "xuFGLw8MavQI", + "outputId": "b0118973-1e29-419d-b7c0-2933b97d7ce5" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()\n", + "masks = masks_to_bool(masks)\n", + "xyxy = sv.mask_to_xyxy(masks=masks)\n", + "fast_sam_detections = sv.Detections(xyxy=xyxy, mask=masks)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IYn8xKySbBnc" + }, + "source": [ + "### FastSAM vs. SAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 321 + }, + "id": "c2Qj7sLjbDvo", + "outputId": "e85e3ed3-8b90-409d-cf95-877a6a9b2034" + }, + "outputs": [], + "source": [ + "mask_annotator = sv.MaskAnnotator(color_lookup = sv.ColorLookup.INDEX)\n", + "\n", + "sam_result = mask_annotator.annotate(scene=image_bgr.copy(), detections=sam_detections)\n", + "fast_sam_result = mask_annotator.annotate(scene=image_bgr.copy(), detections=fast_sam_detections)\n", + "\n", + "sv.plot_images_grid(\n", + " images=[sam_result, fast_sam_result],\n", + " grid_size=(1, 2),\n", + " titles=['SAM', 'FastSAM']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MfU4NQiGWQMC" + }, + "source": [ + "**NOTE:** There is a lot going on in our test image. Let's plot our masks against a blank background." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 321 + }, + "id": "PIrywlF5QRPz", + "outputId": "8e7c84ae-7869-4523-847a-0678fd1aea53" + }, + "outputs": [], + "source": [ + "mask_annotator = sv.MaskAnnotator(color_lookup = sv.ColorLookup.INDEX)\n", + "\n", + "sam_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=sam_detections)\n", + "fast_sam_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=fast_sam_detections)\n", + "\n", + "sv.plot_images_grid(\n", + " images=[sam_result, fast_sam_result],\n", + " grid_size=(1, 2),\n", + " titles=['SAM', 'FastSAM']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "51Fs3sDGQmIt" + }, + "source": [ + "### Mask diff\n", + "\n", + "**NOTE:** Let's try to understand which masks generated by SAM were not generated by FastSAM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "O5bIUAhYQrqZ" + }, + "outputs": [], + "source": [ + "def compute_iou(mask1, mask2):\n", + " intersection = np.logical_and(mask1, mask2)\n", + " union = np.logical_or(mask1, mask2)\n", + " return np.sum(intersection) / np.sum(union)\n", + "\n", + "def filter_masks(mask_array1, mask_array2, threshold):\n", + " return np.array([mask1 for mask1 in mask_array1 if max(compute_iou(mask1, mask2) for mask2 in mask_array2) < threshold])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 420 + }, + "id": "1GM4bL9sRbOm", + "outputId": "421fb4f5-7a42-461b-ba1e-7cd46d2d7bd7" + }, + "outputs": [], + "source": [ + "diff_masks = filter_masks(sam_detections.mask, fast_sam_detections.mask, 0.5)\n", + "diff_xyxy = sv.mask_to_xyxy(masks=diff_masks)\n", + "diff_detections = sv.Detections(xyxy=diff_xyxy, mask=diff_masks)\n", + "\n", + "mask_annotator = sv.MaskAnnotator(color_lookup = sv.ColorLookup.INDEX)\n", + "diff_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=diff_detections)\n", + "sv.plot_image(image=diff_result, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0lPZAZZulhF8" + }, + "source": [ + "## Roboflow Benchmark Dataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LWN6XwPuSZBY", + "outputId": "86b18c0e-f6d6-486b-eee6-d004f13f531e" + }, + "outputs": [], + "source": [ + "%cd {HOME}\n", + "\n", + "roboflow.login()\n", + "rf = Roboflow()\n", + "\n", + "project = rf.workspace(\"roboticfish\").project(\"underwater_object_detection\")\n", + "dataset = project.version(10).download(\"yolov8\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "T6-PaxWXTZ69" + }, + "outputs": [], + "source": [ + "IMAGE_PATH = os.path.join(dataset.location, 'train', 'images', 'IMG_2311_jpeg_jpg.rf.09ae6820eaff21dc838b1f9b6b20342b.jpg')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "p-gY5jf1Tife", + "outputId": "630d35d8-26f4-4342-8d82-c7b61be26fe8" + }, + "outputs": [], + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.text_prompt(text='Penguin')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "3tBIH0yubHsW", + "outputId": "fc4d0a6a-53c1-4a2b-91f0-75c8f78e10fd" + }, + "outputs": [], + "source": [ + "masks = masks_to_bool(masks)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gP9gEw9Tp8GJ" + }, + "source": [ + "## 🏆 Congratulations\n", + "\n", + "### Learning Resources\n", + "\n", + "Roboflow has produced many resources that you may find interesting as you advance your knowledge of computer vision:\n", + "\n", + "- [Roboflow Notebooks](https://github.com/roboflow/notebooks): A repository of over 20 notebooks that walk through how to train custom models with a range of model types, from YOLOv7 to SegFormer.\n", + "- [Roboflow YouTube](https://www.youtube.com/c/Roboflow): Our library of videos featuring deep dives into the latest in computer vision, detailed tutorials that accompany our notebooks, and more.\n", + "- [Roboflow Discuss](https://discuss.roboflow.com/): Have a question about how to do something on Roboflow? Ask your question on our discussion forum.\n", + "- [Roboflow Models](https://roboflow.com): Learn about state-of-the-art models and their performance. Find links and tutorials to guide your learning.\n", + "\n", + "### Convert data formats\n", + "\n", + "Roboflow provides free utilities to convert data between dozens of popular computer vision formats. Check out [Roboflow Formats](https://roboflow.com/formats) to find tutorials on how to convert data between formats in a few clicks.\n", + "\n", + "### Connect computer vision to your project logic\n", + "\n", + "[Roboflow Templates](https://roboflow.com/templates) is a public gallery of code snippets that you can use to connect computer vision to your project logic. Code snippets range from sending emails after inference to measuring object distance between detections." + ] + } + ] + .map(fromJupyterCell) + , + [ + { + "cell_type": "markdown", + "metadata": { + "id": "F0Wut2G5kz2Z" + }, + "source": [ + "[![Roboflow Notebooks](https://ik.imagekit.io/roboflow/notebooks/template/bannertest2-2.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672932710194)](https://github.com/roboflow/notebooks)\n", + "\n", + "# Fast Segment Anything Model (FastSAM)\n", + "\n", + "---\n", + "\n", + "[![GitHub](https://badges.aleen42.com/src/github.svg)](https://github.com/CASIA-IVA-Lab/FastSAM)\n", + "[![arXiv](https://img.shields.io/badge/arXiv-2306.12156-b31b1b.svg)](https://arxiv.org/pdf/2306.12156.pdf)\n", + "[![YouTube](https://badges.aleen42.com/src/youtube.svg)](https://youtu.be/yHNPyqazYYU)\n", + "[![Roboflow](https://raw.githubusercontent.com/roboflow-ai/notebooks/main/assets/badges/roboflow-blogpost.svg)](https://blog.roboflow.com/how-to-use-fastsam)\n", + "\n", + "The Fast Segment Anything Model (FastSAM) is a CNN Segment Anything Model trained by only 2% of the SA-1B dataset published by SAM authors.\n", + "\n", + "![fast-segment-anything-model-figure-1](https://media.roboflow.com/notebooks/examples/fast-sam-figure-1.png)\n", + "\n", + "The FastSAM authors claim it achieves comparable performance to the SAM method at 50 times the speed.\n", + "\n", + "![fast-segment-anything-model-figure-2](https://media.roboflow.com/notebooks/examples/fast-sam-figure-2.png)\n", + "\n", + "## Complementary Materials Covering SAM\n", + "\n", + "---\n", + "\n", + "[![GitHub](https://badges.aleen42.com/src/github.svg)](https://github.com/facebookresearch/segment-anything)\n", + "[![arXiv](https://img.shields.io/badge/arXiv-2304.02643-b31b1b.svg)](https://arxiv.org/abs/2304.02643)\n", + "[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-segment-anything-with-sam.ipynb)\n", + "[![YouTube](https://badges.aleen42.com/src/youtube.svg)](https://youtu.be/D-D6ZmadzPE)\n", + "[![Roboflow](https://raw.githubusercontent.com/roboflow-ai/notebooks/main/assets/badges/roboflow-blogpost.svg)](https://blog.roboflow.com/how-to-use-segment-anything-model-sam)\n", + "\n", + "## Steps in this Tutorial\n", + "\n", + "In this tutorial, we are going to cover:\n", + "\n", + "- Before you start\n", + "- Install FastSAM, SAM, and other dependencies\n", + "- Download FastSAM and SAM weights\n", + "- Download example data\n", + "- Imports\n", + "- Load FastSAM\n", + "- FastSAM inference\n", + "- FastSAM box prompt inference\n", + "- FastSAM point prompt inference\n", + "- FastSAM text prompt inference\n", + "- SAM vs FastSAM\n", + "- Roboflow benchmark dataset\n", + "\n", + "## 🔥 Let's begin!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "W4u6KjiVdNZM" + }, + "source": [ + "## Before you start\n", + "\n", + "Let's make sure that we have access to GPU. We can use `nvidia-smi` command to do that. In case of any problems navigate to `Edit` -> `Notebook settings` -> `Hardware accelerator`, set it to `GPU`, and then click `Save`." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Frcrk09FhJeV", + "outputId": "683a378e-4a8f-4239-901b-037279e92e2b" + }, + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PUACTM2zdmJI" + }, + "source": [ + "**NOTE:** To make it easier for us to manage datasets, images and models we create a `HOME` constant." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "PRtHQpTNdnm7", + "outputId": "2824c70d-dcf9-4006-b32d-adc6797fd708" + }, + "source": [ + "import os\n", + "HOME = os.getcwd()\n", + "print(\"HOME:\", HOME)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YN3DPGZSn57p" + }, + "source": [ + "## Install FastSAM, SAM, and other dependencies" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "Vr3kcq1KfFir", + "outputId": "234e5336-ba28-4114-d9e7-efd7f2752ca8" + }, + "source": [ + "%cd {HOME}\n", + "\n", + "# install FastSAM\n", + "!git clone https://github.com/CASIA-IVA-Lab/FastSAM.git\n", + "!pip -q install -r FastSAM/requirements.txt\n", + "# install CLIP\n", + "!pip -q install git+https://github.com/openai/CLIP.git\n", + "# install SAM\n", + "!pip -q install git+https://github.com/facebookresearch/segment-anything.git\n", + "# install other dependencies\n", + "!pip -q install roboflow supervision jupyter_bbox_widget" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BMmgSfDWfFis" + }, + "source": [ + "## Download FastSAM and SAM weights" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "ADpOBElSfFis", + "outputId": "b3d89d4c-e2ad-4f5e-a910-b52695263151" + }, + "source": [ + "!mkdir -p {HOME}/weights\n", + "!wget -P {HOME}/weights -q https://huggingface.co/spaces/An-619/FastSAM/resolve/main/weights/FastSAM.pt\n", + "!wget -P {HOME}/weights -q https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth\n", + "!ls -lh {HOME}/weights" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "W6w1ck4z72sn" + }, + "source": [ + "FAST_SAM_CHECKPOINT_PATH = f\"{HOME}/weights/FastSAM.pt\"\n", + "SAM_SAM_CHECKPOINT_PATH = f\"{HOME}/weights/sam_vit_h_4b8939.pth\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t1ef2h1R9WMb" + }, + "source": [ + "## Download example data\n", + "\n", + "**NONE:** Let's download few example images. Feel free to use your images or videos." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "CL4gVrnl9dmR", + "outputId": "db8f78d2-eaf4-41de-9d1f-e5fd6b588a87" + }, + "source": [ + "!mkdir -p {HOME}/data\n", + "!wget -P {HOME}/data -q https://media.roboflow.com/notebooks/examples/dog.jpeg\n", + "!wget -P {HOME}/data -q https://media.roboflow.com/notebooks/examples/robot.jpeg\n", + "!ls -lh {HOME}/data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "J_a_icikYiVN" + }, + "source": [ + "## Imports\n", + "\n", + "**NOTE:** `FastSAM` code is not distributed via `pip` not it is packaged. Make sure to run code balow from `{HOME}/FastSAM` directory. ⚠️" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "u_NVTXOwYppS", + "outputId": "b86184de-5827-4b9c-eb5c-959539abe699" + }, + "source": [ + "%cd {HOME}/FastSAM\n", + "\n", + "import os\n", + "import cv2\n", + "import torch\n", + "import roboflow\n", + "import base64\n", + "\n", + "import supervision as sv\n", + "import numpy as np\n", + "\n", + "from roboflow import Roboflow\n", + "from fastsam import FastSAM, FastSAMPrompt\n", + "from segment_anything import sam_model_registry, SamAutomaticMaskGenerator, SamPredictor" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "a_Iizsy07pb7" + }, + "source": [ + "## Load FastSAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "ao8tVDVq7ebG" + }, + "source": [ + "DEVICE = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')\n", + "\n", + "fast_sam = FastSAM(FAST_SAM_CHECKPOINT_PATH)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "SvsRFtw_--Nn" + }, + "source": [ + "## FastSAM inference\n", + "\n", + "* `retina_masks=True` determines whether the model uses retina masks for generating segmentation masks.\n", + "* `imgsz=1024` sets the input image size to 1024x1024 pixels for processing by the model.\n", + "* `conf=0.4` sets the minimum confidence threshold for object detection.\n", + "* `iou=0.9` sets the minimum intersection over union threshold for non-maximum suppression to filter out duplicate detections." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "L49P5e-0Iaea" + }, + "source": [ + "IMAGE_PATH = f\"{HOME}/data/dog.jpeg\"" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "KV4XzCg1_fAS", + "outputId": "1df75d90-cf73-4d78-feed-01911a81f6e4" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.4,\n", + " iou=0.9)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()\n", + "prompt_process.plot(annotations=masks, output=f\"{HOME}/output\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uPrxqbaBOBMw" + }, + "source": [ + "**NOTE:** `prompt_process.everything_prompt` returns `torch.Tensor`" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "PUJkojNiKBQa" + }, + "source": [ + "def annotate_image(image_path: str, masks: np.ndarray) -> np.ndarray:\n", + " image = cv2.imread(image_path)\n", + "\n", + " xyxy = sv.mask_to_xyxy(masks=masks)\n", + " detections = sv.Detections(xyxy=xyxy, mask=masks)\n", + "\n", + " mask_annotator = sv.MaskAnnotator()\n", + " return mask_annotator.annotate(scene=image.copy(), detections=detections)" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "wwtmiH6pLA8N", + "outputId": "17948850-f051-4a1f-b2b8-d01c2a1f4f20" + }, + "source": [ + "masks = masks.cpu().numpy().astype(bool)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jYkdczKoRUVB" + }, + "source": [ + "**NOTE:** The quality of the generated masks is quite poor. Let's see if we can improve it by manipulating the parameters." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "dDFGi6EhO2ul", + "outputId": "f5a03357-eb37-446e-dce7-0659e4c2a368" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "OiBPjQW-O59c", + "outputId": "afb011fc-933e-4d69-b4dd-e5e2713f407b" + }, + "source": [ + "masks = masks.cpu().numpy().astype(bool)\n", + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GoymBQ16KJbv" + }, + "source": [ + "## FastSAM box prompt inference" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "w1_T5SXpMNku" + }, + "source": [ + "### Draw Box" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "04-1G1WrMJUm" + }, + "source": [ + "# helper function that loads an image before adding it to the widget\n", + "\n", + "def encode_image(filepath):\n", + " with open(filepath, 'rb') as f:\n", + " image_bytes = f.read()\n", + " encoded = str(base64.b64encode(image_bytes), 'utf-8')\n", + " return \"data:image/jpg;base64,\"+encoded" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xrxd-Ug9MSWG" + }, + "source": [ + "**NOTE:** Execute cell below and use your mouse to draw bounding box on the image 👇" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000, + "referenced_widgets": [ + "a747e095e21448f29d8697fb6b1a7a74", + "6401ca8c2f574ba5ab62f0f06b9e0d06" + ] + }, + "id": "nLA7xmDIMTfx", + "outputId": "06b51195-d54f-44ed-9236-258e94d624c5" + }, + "source": [ + "IS_COLAB = True\n", + "\n", + "if IS_COLAB:\n", + " from google.colab import output\n", + " output.enable_custom_widget_manager()\n", + "\n", + "from jupyter_bbox_widget import BBoxWidget\n", + "\n", + "widget = BBoxWidget()\n", + "widget.image = encode_image(IMAGE_PATH)\n", + "widget" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "td6tYT4INOyD", + "outputId": "73a4e886-dcb8-447c-d3f7-1b3a6496e44d" + }, + "source": [ + "widget.bboxes" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XfUvm9bwNUAe" + }, + "source": [ + "### Generate mask with FastSAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "bI6kPDSROBI-" + }, + "source": [ + "# default_box is going to be used if you will not draw any box on image above\n", + "default_box = {'x': 68, 'y': 247, 'width': 555, 'height': 678, 'label': ''}\n", + "\n", + "box = widget.bboxes[0] if widget.bboxes else default_box\n", + "box = [\n", + " box['x'],\n", + " box['y'],\n", + " box['x'] + box['width'],\n", + " box['y'] + box['height']\n", + "]" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LF879hDqO5iB", + "outputId": "4e7498c4-4007-4614-d46c-0e35608a04a2" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.box_prompt(bbox=box)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yt6i2sNJPEdk" + }, + "source": [ + "**NOTE:** `prompt_process.box_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "EJThTct_PA68", + "outputId": "f6eddd24-f603-4702-8325-6cff7925fe0f" + }, + "source": [ + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vR-THJthhWH5" + }, + "source": [ + "## FastSAM point prompt inference" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xyfpOPPwhpMy" + }, + "source": [ + "### Select point" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "OC7d3pZ3hop9" + }, + "source": [ + "point = [405, 505]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rE5tV4e3h6nG" + }, + "source": [ + "### Generate mask with FastSAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "31FIby6Zh-Wa", + "outputId": "a12c4d1c-837b-4b1b-9240-664797442413" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.point_prompt(points=[point], pointlabel=[1])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d7tpVo8CiM3s" + }, + "source": [ + "**NOTE:** `prompt_process.point_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "lMVpGxShiPky", + "outputId": "224d84ee-e664-400b-c0df-49507d229f62" + }, + "source": [ + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HOC7v6ATHEr6" + }, + "source": [ + "## FastSAM text prompt inference" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "4CzLLtXOHeGk", + "outputId": "cecc50d8-e2be-4b33-850a-a2442e3e30b6" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.text_prompt(text='cap')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KYW79zD0OKsv" + }, + "source": [ + "**NOTE:** `prompt_process.text_prompt` returns `numy.ndarray`" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "zODY5xU1LMCb", + "outputId": "a01540a7-deb3-4555-916c-2ac59928e0f3" + }, + "source": [ + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XFOUWUMZEk7m" + }, + "source": [ + "## SAM vs FastSAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "7PKBZ7Hhahhw" + }, + "source": [ + "IMAGE_PATH = f\"{HOME}/data/robot.jpeg\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "c48hRY7dVJeL" + }, + "source": [ + "### Load SAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "PdKpFCCmaETG" + }, + "source": [ + "MODEL_TYPE = \"vit_h\"\n", + "\n", + "sam = sam_model_registry[MODEL_TYPE](checkpoint=SAM_SAM_CHECKPOINT_PATH).to(device=DEVICE)\n", + "mask_generator = SamAutomaticMaskGenerator(sam)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "V5bPGvtBaewO" + }, + "source": [ + "### SAM inference" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "0HgWsk1yalow" + }, + "source": [ + "image_bgr = cv2.imread(IMAGE_PATH)\n", + "image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)\n", + "\n", + "sam_result = mask_generator.generate(image_rgb)\n", + "sam_detections = sv.Detections.from_sam(sam_result=sam_result)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "t03RrSzMasN6" + }, + "source": [ + "### FastSAM inference" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "xuFGLw8MavQI", + "outputId": "b0118973-1e29-419d-b7c0-2933b97d7ce5" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.everything_prompt()\n", + "masks = masks.cpu().numpy().astype(bool)\n", + "xyxy = sv.mask_to_xyxy(masks=masks)\n", + "fast_sam_detections = sv.Detections(xyxy=xyxy, mask=masks)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IYn8xKySbBnc" + }, + "source": [ + "### FastSAM vs. SAM" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 321 + }, + "id": "c2Qj7sLjbDvo", + "outputId": "e85e3ed3-8b90-409d-cf95-877a6a9b2034" + }, + "source": [ + "mask_annotator = sv.MaskAnnotator()\n", + "\n", + "sam_result = mask_annotator.annotate(scene=image_bgr.copy(), detections=sam_detections)\n", + "fast_sam_result = mask_annotator.annotate(scene=image_bgr.copy(), detections=fast_sam_detections)\n", + "\n", + "sv.plot_images_grid(\n", + " images=[sam_result, fast_sam_result],\n", + " grid_size=(1, 2),\n", + " titles=['SAM', 'FastSAM']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MfU4NQiGWQMC" + }, + "source": [ + "**NOTE:** There is a lot going on in our test image. Let's plot our masks against a blank background." + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 321 + }, + "id": "PIrywlF5QRPz", + "outputId": "8e7c84ae-7869-4523-847a-0678fd1aea53" + }, + "source": [ + "mask_annotator = sv.MaskAnnotator()\n", + "\n", + "sam_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=sam_detections)\n", + "fast_sam_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=fast_sam_detections)\n", + "\n", + "sv.plot_images_grid(\n", + " images=[sam_result, fast_sam_result],\n", + " grid_size=(1, 2),\n", + " titles=['SAM', 'FastSAM']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "51Fs3sDGQmIt" + }, + "source": [ + "### Mask diff\n", + "\n", + "**NOTE:** Let's try to understand which masks generated by SAM were not generated by FastSAM." + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "O5bIUAhYQrqZ" + }, + "source": [ + "def compute_iou(mask1, mask2):\n", + " intersection = np.logical_and(mask1, mask2)\n", + " union = np.logical_or(mask1, mask2)\n", + " return np.sum(intersection) / np.sum(union)\n", + "\n", + "def filter_masks(mask_array1, mask_array2, threshold):\n", + " return np.array([mask1 for mask1 in mask_array1 if max(compute_iou(mask1, mask2) for mask2 in mask_array2) < threshold])" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 420 + }, + "id": "1GM4bL9sRbOm", + "outputId": "421fb4f5-7a42-461b-ba1e-7cd46d2d7bd7" + }, + "source": [ + "diff_masks = filter_masks(sam_detections.mask, fast_sam_detections.mask, 0.5)\n", + "diff_xyxy = sv.mask_to_xyxy(masks=diff_masks)\n", + "diff_detections = sv.Detections(xyxy=diff_xyxy, mask=diff_masks)\n", + "\n", + "mask_annotator = sv.MaskAnnotator()\n", + "diff_result = mask_annotator.annotate(scene=np.zeros_like(image_bgr), detections=diff_detections)\n", + "sv.plot_image(image=diff_result, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0lPZAZZulhF8" + }, + "source": [ + "## Roboflow Benchmark Dataset" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "LWN6XwPuSZBY", + "outputId": "86b18c0e-f6d6-486b-eee6-d004f13f531e" + }, + "source": [ + "%cd {HOME}\n", + "\n", + "roboflow.login()\n", + "rf = Roboflow()\n", + "\n", + "project = rf.workspace(\"roboticfish\").project(\"underwater_object_detection\")\n", + "dataset = project.version(10).download(\"yolov8\")" + ] + }, + { + "cell_type": "code", + "metadata": { + "id": "T6-PaxWXTZ69" + }, + "source": [ + "IMAGE_PATH = os.path.join(dataset.location, 'train', 'images', 'IMG_2311_jpeg_jpg.rf.09ae6820eaff21dc838b1f9b6b20342b.jpg')" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "p-gY5jf1Tife", + "outputId": "630d35d8-26f4-4342-8d82-c7b61be26fe8" + }, + "source": [ + "results = fast_sam(\n", + " source=IMAGE_PATH,\n", + " device=DEVICE,\n", + " retina_masks=True,\n", + " imgsz=1024,\n", + " conf=0.5,\n", + " iou=0.6)\n", + "prompt_process = FastSAMPrompt(IMAGE_PATH, results, device=DEVICE)\n", + "masks = prompt_process.text_prompt(text='Penguin')" + ] + }, + { + "cell_type": "code", + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 653 + }, + "id": "3tBIH0yubHsW", + "outputId": "fc4d0a6a-53c1-4a2b-91f0-75c8f78e10fd" + }, + "source": [ + "annotated_image=annotate_image(image_path=IMAGE_PATH, masks=masks)\n", + "sv.plot_image(image=annotated_image, size=(8, 8))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gP9gEw9Tp8GJ" + }, + "source": [ + "## 🏆 Congratulations\n", + "\n", + "### Learning Resources\n", + "\n", + "Roboflow has produced many resources that you may find interesting as you advance your knowledge of computer vision:\n", + "\n", + "- [Roboflow Notebooks](https://github.com/roboflow/notebooks): A repository of over 20 notebooks that walk through how to train custom models with a range of model types, from YOLOv7 to SegFormer.\n", + "- [Roboflow YouTube](https://www.youtube.com/c/Roboflow): Our library of videos featuring deep dives into the latest in computer vision, detailed tutorials that accompany our notebooks, and more.\n", + "- [Roboflow Discuss](https://discuss.roboflow.com/): Have a question about how to do something on Roboflow? Ask your question on our discussion forum.\n", + "- [Roboflow Models](https://roboflow.com): Learn about state-of-the-art models and their performance. Find links and tutorials to guide your learning.\n", + "\n", + "### Convert data formats\n", + "\n", + "Roboflow provides free utilities to convert data between dozens of popular computer vision formats. Check out [Roboflow Formats](https://roboflow.com/formats) to find tutorials on how to convert data between formats in a few clicks.\n", + "\n", + "### Connect computer vision to your project logic\n", + "\n", + "[Roboflow Templates](https://roboflow.com/templates) is a public gallery of code snippets that you can use to connect computer vision to your project logic. Code snippets range from sending emails after inference to measuring object distance between detections." + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: 0 }, + { modified: 1, original: 1 }, + { modified: 2, original: 2 }, + { modified: 3, original: 3 }, + { modified: 4, original: 4 }, + { modified: 5, original: 5 }, + { modified: 6, original: 6 }, + { modified: 7, original: 7 }, + { modified: 8, original: 8 }, + { modified: 9, original: 9 }, + { modified: 10, original: 10 }, + { modified: 11, original: 11 }, + { modified: 12, original: 12 }, + { modified: 13, original: 13 }, + { modified: 14, original: 14 }, + { modified: 15, original: 15 }, + { modified: 16, original: 16 }, + { modified: 17, original: 17 }, + { modified: 18, original: 19 }, + { modified: 19, original: 20 }, + { modified: 20, original: 22 }, + { modified: 21, original: 23 }, + { modified: 22, original: 24 }, + { modified: 23, original: 25 }, + { modified: 24, original: 26 }, + { modified: 25, original: 27 }, + { modified: 26, original: 28 }, + { modified: 27, original: 29 }, + { modified: 28, original: 30 }, + { modified: 29, original: 31 }, + { modified: 30, original: 32 }, + { modified: 31, original: 33 }, + { modified: 32, original: 34 }, + { modified: 33, original: 35 }, + { modified: 34, original: 36 }, + { modified: 35, original: 37 }, + { modified: 36, original: 38 }, + { modified: 37, original: 39 }, + { modified: 38, original: 40 }, + { modified: 39, original: 41 }, + { modified: 40, original: 42 }, + { modified: 41, original: 43 }, + { modified: 42, original: 44 }, + { modified: 43, original: 45 }, + { modified: 44, original: 46 }, + { modified: 45, original: 47 }, + { modified: 46, original: 48 }, + { modified: 47, original: 49 }, + { modified: 48, original: 50 }, + { modified: 49, original: 51 }, + { modified: 50, original: 52 }, + { modified: 51, original: 53 }, + { modified: 52, original: 54 }, + { modified: 53, original: 55 }, + { modified: 54, original: 56 }, + { modified: 55, original: 57 }, + { modified: 56, original: 58 }, + { modified: 57, original: 59 }, + { modified: 58, original: 60 }, + { modified: 59, original: 61 }, + { modified: 60, original: 62 }, + { modified: 61, original: 63 }, + { modified: 62, original: 64 }, + { modified: 63, original: 65 }, + { modified: 64, original: 66 }, + { modified: 65, original: 67 }, + { modified: 66, original: 68 }, + { modified: 67, original: 69 }, + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 12, originalLength: 1, modifiedStart: 12, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 15, originalLength: 1, modifiedStart: 15, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 18, originalLength: 1, modifiedStart: 18, modifiedLength: 0 } satisfies IDiffChange, + { originalStart: 20, originalLength: 1, modifiedStart: 19, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 21, originalLength: 1, modifiedStart: 20, modifiedLength: 0 } satisfies IDiffChange, + { originalStart: 22, originalLength: 1, modifiedStart: 20, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 23, originalLength: 1, modifiedStart: 21, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 26, originalLength: 1, modifiedStart: 24, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 37, originalLength: 1, modifiedStart: 35, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 44, originalLength: 1, modifiedStart: 42, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 48, originalLength: 1, modifiedStart: 46, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 56, originalLength: 1, modifiedStart: 54, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 58, originalLength: 1, modifiedStart: 56, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 60, originalLength: 1, modifiedStart: 58, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 63, originalLength: 1, modifiedStart: 61, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 68, originalLength: 1, modifiedStart: 66, modifiedLength: 1 } satisfies IDiffChange, + // { originalStart: 53, originalLength: 0, modifiedStart: 53, modifiedLength: 2 } satisfies IDiffChange, + ]); + + }); + test('Insert a few cells (one on top), modify a few and completely misaligned', async () => { + const { mapping, diff } = await mapCells( + [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from cachetools import cached, TTLCache\n", + "from dotenv import load_dotenv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "GITHUB_API_URL = \"https://api.github.com\"\n", + "REPO_OWNER = \"microsoft\"\n", + "ISSUE = 123 # Replace with your milestone number\n", + "load_dotenv()\n", + "TOKEN = os.getenv(\"GH\")\n", + "\n", + "headers = {\n", + " \"Authorization\": f\"Bearer {TOKEN}\",\n", + " \"Accept\": \"application/vnd.github+json\"\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import csv\n", + "from datetime import datetime\n", + "import os\n", + "\n", + "def append_information(repo, open_issues_count, ):\n", + " file_exists = os.path.isfile(filename)\n", + " timestamp = datetime.utcnow().isoformat()\n", + "\n", + " # Open the file in append mode\n", + " with open(filename, 'a', newline='', encoding='utf-8') as csvfile:\n", + " writer = csv.writer(csvfile)\n", + "\n", + " # If the file didn't exist before, write the header row\n", + " if not file_exists:\n", + " writer.writerow([\n", + " \"timestamp\",\n", + " \"repo\",\n", + " \"open_issues_count\",\n", + " \"open_prs_count\",\n", + " \"bug_issues_count\",\n", + " \"feature_request_issues_count\",\n", + " \"invalid_issues_count\"\n", + " ])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_repo_issue_stats(owner, repo, token):\n", + " # Common GitHub GraphQL endpoint\n", + " url = \"https://api.github.com/graphql\"\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {token}\",\n", + " \"Accept\": \"application/vnd.github.v4+json\"\n", + " }\n", + "\n", + " # First query: Get various issue/PR counts from the repository object\n", + " query_repo = \"\"\"\n", + " query ($owner: String!, $name: String!) {\n", + " repository(owner: $owner, name: $name) {\n", + " issues(states: OPEN) {\n", + " totalCount\n", + " }\n", + " pullRequests(states: OPEN) {\n", + " totalCount\n", + " }\n", + " bugIssues: issues(states: OPEN, labels: [\"bug\"]) {\n", + " totalCount\n", + " }\n", + " featureRequestIssues: issues(states: OPEN, labels: [\"feature-request\"]) {\n", + " totalCount\n", + " }\n", + " }\n", + " }\n", + " \"\"\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for REPO in REPOS:\n", + " stats = get_repo_issue_stats(OWNER, REPO, TOKEN)\n", + " print(stats)\n", + "\n", + " append_metrics_to_csv(\n", + " filename=\"repo_metrics.csv\",\n", + " repo=f\"{OWNER}/{REPO}\",\n", + " open_issues_count=stats['open_issues_count'],\n", + " open_prs_count=stats['open_prs_count'],\n", + " bug_issues_count=stats['bug_issues_count'],\n", + " feature_request_issues_count=stats['feature_request_issues_count'],\n", + " invalid_issues_count=stats['invalid_issues_count']\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import plotly.graph_objs as go\n", + "from plotly.subplots import make_subplots\n", + "\n", + "def plot_repo_metrics(repo, csv_filename=\"repo_metrics.csv\"):\n", + " # Load the CSV data\n", + " df = pd.read_csv(csv_filename)\n", + "\n", + " fig.add_trace(go.Scatter(\n", + " x=df_repo['timestamp'],\n", + " y=df_repo['open_issues_count'],\n", + " mode='lines+markers',\n", + " name='Open Issues',\n", + " hovertemplate='Open Issues: %{y}
Time: %{x}'\n", + " ))\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for REPO in REPOS:\n", + " plot_repo_metrics(f\"{OWNER}/{REPO}\")" + ] + } + ] + .map(fromJupyterCell) + , + [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Fetch Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import sys\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from cachetools import cached, TTLCache\n", + "from dotenv import load_dotenv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "GITHUB_API_URL = \"https://api.github.com\"\n", + "REPO_OWNER = \"microsoft\"\n", + "ISSUE = 123\n", + "load_dotenv()\n", + "TOKEN = os.getenv(\"GH\")\n", + "\n", + "headers = {\n", + " \"Authorization\": f\"Bearer {TOKEN}\",\n", + " \"Accept\": \"application/vnd.github+json\"\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import csv\n", + "from datetime import datetime\n", + "import os\n", + "\n", + "def append_information(repo, open_issues_count, ):\n", + " file_exists = os.path.isfile(filename)\n", + " timestamp = datetime.utcnow().isoformat()\n", + "\n", + " # Open the file in append mode\n", + " with open(filename, 'a', newline='', encoding='utf-8') as csvfile:\n", + " writer = csv.writer(csvfile)\n", + "\n", + " # If the file didn't exist before, write the header row\n", + " if not file_exists:\n", + " writer.writerow([\n", + " \"timestamp\",\n", + " \"repo\",\n", + " \"open_issues_count\",\n", + " \"open_prs_count\",\n", + " \"bug_issues_count\",\n", + " \"feature_request_issues_count\",\n", + " \"invalid_issues_count\"\n", + " ])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_repo_issue_stats(owner, repo, token):\n", + " # Common GitHub GraphQL endpoint\n", + " url = \"https://api.github.com/graphql\"\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {token}\",\n", + " \"Accept\": \"application/vnd.github.v4+json\"\n", + " }\n", + "\n", + " # First query: Get various issue/PR counts from the repository object\n", + " query_repo = \"\"\"\n", + " query ($owner: String!, $name: String!) {\n", + " repository(owner: $owner, name: $name) {\n", + " issues(states: OPEN) {\n", + " totalCount\n", + " }\n", + " pullRequests(states: OPEN) {\n", + " totalCount\n", + " }\n", + " bugIssues: issues(states: OPEN, labels: [\"bug\"]) {\n", + " totalCount\n", + " }\n", + " featureRequestIssues: issues(states: OPEN, labels: [\"feature-request\"]) {\n", + " totalCount\n", + " }\n", + " }\n", + " }\n", + " \"\"\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for REPO in REPOS:\n", + " stats = get_repo_issue_stats(OWNER, REPO, TOKEN)\n", + " print(stats)\n", + "\n", + " append_metrics_to_csv(\n", + " filename=\"repo_metrics.csv\",\n", + " repo=f\"{OWNER}/{REPO}\",\n", + " open_issues_count=stats['open_issues_count'],\n", + " open_prs_count=stats['open_prs_count'],\n", + " bug_issues_count=stats['bug_issues_count'],\n", + " feature_request_issues_count=stats['feature_request_issues_count'],\n", + " invalid_issues_count=stats['invalid_issues_count']\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import plotly.graph_objs as go\n", + "from plotly.subplots import make_subplots\n", + "\n", + "def plot_repo_metrics(repo, csv_filename=\"repo_metrics.csv\"):\n", + " # Load the CSV data\n", + " df = pd.read_csv(csv_filename)\n", + "\n", + " fig.add_trace(go.Scatter(\n", + " x=df_repo['timestamp'],\n", + " y=df_repo['open_issues_count'],\n", + " mode='lines+markers',\n", + " name='Open Issues',\n", + " hovertemplate='Open Issues: %{y}
Time: %{x}'\n", + " ))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Header" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for REPO in REPOS:\n", + " plot_repo_metrics(f\"{OWNER}/{REPO}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plot_repo_metrics(f\"{OWNER}/vscode\")" + ] + } + ].map(fromJupyterCell) + ); + + assert.deepStrictEqual(mapping, [ + { modified: 0, original: -1 }, + { modified: 1, original: 0 }, + { modified: 2, original: 1 }, + { modified: 3, original: 2 }, + { modified: 4, original: 3 }, + { modified: 5, original: 4 }, + { modified: 6, original: 5 }, + { modified: 7, original: 6 }, + { modified: 8, original: -1 }, + { modified: 9, original: 7 }, + { modified: 10, original: -1 } + ]); + assert.deepStrictEqual(diff.cellsDiff.changes, [ + { originalStart: 0, originalLength: 0, modifiedStart: 0, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 0, originalLength: 1, modifiedStart: 1, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 1, originalLength: 1, modifiedStart: 2, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 7, originalLength: 0, modifiedStart: 8, modifiedLength: 1 } satisfies IDiffChange, + { originalStart: 8, originalLength: 0, modifiedStart: 10, modifiedLength: 1 } satisfies IDiffChange, + ]); + + + }); + + let uriCounter = 0; + async function mapCells(original: MockNotebookCell[], modified: MockNotebookCell[]) { + const mapping = matchCellBasedOnSimilarties(modified.map(cellToDto), original.map(cellToDto)).map(mapping => ({ modified: mapping.modified, original: mapping.original })); + const originalUri = URI.parse(`file://original${uriCounter++}.ipynb`); + const modifiedUri = URI.parse(`file://modified${uriCounter++}.ipynb`); + worker.$acceptNewModel(originalUri.toString(), {}, {}, original.map((cell, index) => cellToMainCellTdo(originalUri, cell, index))); + worker.$acceptNewModel(modifiedUri.toString(), {}, {}, modified.map((cell, index) => cellToMainCellTdo(modifiedUri, cell, index))); + const diff = await worker.$computeDiff(originalUri.toString(), modifiedUri.toString()); + diff.cellsDiff.changes = diff.cellsDiff.changes.map(change => { + return { + originalStart: change.originalStart, + originalLength: change.originalLength, + modifiedStart: change.modifiedStart, + modifiedLength: change.modifiedLength, + }; + }); + return { mapping, diff }; + } + + + function cellToDto(cell: MockNotebookCell): { getValue(): string; getLinesContent(): string[]; cellKind: CellKind } { + return { + cellKind: cell[2], + getValue: () => cell[0].join('\n'), + getLinesContent: () => cell[0] + }; + } + + function cellToMainCellTdo(notebookUri: URI, cell: MockNotebookCell, index: number): IMainCellDto { + return { + source: cell[0], + language: cell[1], + cellKind: cell[2], + outputs: cell[3] || [], + metadata: cell[4], + eol: '\n', + handle: index, + url: URI.from({ scheme: 'file', path: notebookUri.path, fragment: `cell/${index}` }).toString(), + versionId: 0, + internalMetadata: undefined + } satisfies IMainCellDto; + + } + type MockNotebookCell = [ + lines: string[], + lang: string, + kind: CellKind, + output?: IOutputDto[], + metadata?: NotebookCellMetadata, + ]; + + function fromJupyterCell(cell: { cell_type: string; source: string[] }): MockNotebookCell { + return [ + cell.source, + cell.cell_type === 'code' ? 'python' : 'markdown', + cell.cell_type === 'code' ? CellKind.Code : CellKind.Markup, + ]; + } +}); + + diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts index bbffb0d2..613ee43d 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts @@ -874,6 +874,76 @@ suite('NotebookTextModel', () => { }); }); + test('metadata changes on newly added cells should combine their undo operations', async function () { + await withTestNotebook([ + ['var a = 1;', 'javascript', CellKind.Code, [], {}] + ], async (editor, viewModel, ds) => { + const textModel = editor.textModel; + editor.textModel.applyEdits([ + { + editType: CellEditType.Replace, index: 1, count: 0, cells: [ + ds.add(new TestCell(textModel.viewType, 1, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)), + ds.add(new TestCell(textModel.viewType, 2, 'var f = 6;', 'javascript', CellKind.Code, [], languageService)) + ] + }, + ], true, undefined, () => undefined, undefined, true); + + assert.strictEqual(textModel.cells.length, 3); + + editor.textModel.applyEdits([ + { editType: CellEditType.Metadata, index: 1, metadata: { id: '123' } }, + ], true, undefined, () => undefined, undefined, true); + + assert.strictEqual(textModel.cells[1].metadata.id, '123'); + + await viewModel.undo(); + + assert.strictEqual(textModel.cells.length, 1); + + await viewModel.redo(); + + assert.strictEqual(textModel.cells.length, 3); + }); + }); + + test('changes with non-metadata edit should not combine their undo operations', async function () { + await withTestNotebook([ + ['var a = 1;', 'javascript', CellKind.Code, [], {}] + ], async (editor, viewModel, ds) => { + const textModel = editor.textModel; + editor.textModel.applyEdits([ + { + editType: CellEditType.Replace, index: 1, count: 0, cells: [ + ds.add(new TestCell(textModel.viewType, 1, 'var e = 5;', 'javascript', CellKind.Code, [], languageService)), + ds.add(new TestCell(textModel.viewType, 2, 'var f = 6;', 'javascript', CellKind.Code, [], languageService)) + ] + }, + ], true, undefined, () => undefined, undefined, true); + + assert.strictEqual(textModel.cells.length, 3); + + editor.textModel.applyEdits([ + { editType: CellEditType.Metadata, index: 1, metadata: { id: '123' } }, + { + editType: CellEditType.Output, handle: 0, append: true, outputs: [{ + outputId: 'newOutput', + outputs: [{ mime: Mimes.text, data: valueBytesFromString('cba') }, { mime: 'application/foo', data: valueBytesFromString('cba') }] + }] + } + ], true, undefined, () => undefined, undefined, true); + + assert.strictEqual(textModel.cells[1].metadata.id, '123'); + + await viewModel.undo(); + + assert.strictEqual(textModel.cells.length, 3); + + await viewModel.undo(); + + assert.strictEqual(textModel.cells.length, 1); + }); + }); + test('Destructive sorting in _doApplyEdits #121994', async function () { await withTestNotebook([ ['var a = 1;', 'javascript', CellKind.Code, [{ outputId: 'i42', outputs: [{ mime: 'm/ime', data: valueBytesFromString('test') }] }], {}] diff --git a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts index e1b956a6..4fa628f9 100644 --- a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts +++ b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts @@ -12,9 +12,10 @@ import { OUTPUT_MODE_ID, LOG_MODE_ID } from '../../../services/output/common/out import { OutputLinkComputer } from '../common/outputLinkComputer.js'; import { IDisposable, dispose, Disposable } from '../../../../base/common/lifecycle.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { createWebWorker } from '../../../../base/browser/defaultWorkerFactory.js'; -import { IWorkerClient } from '../../../../base/common/worker/simpleWorker.js'; +import { createWebWorker } from '../../../../base/browser/webWorkerFactory.js'; +import { IWebWorkerClient } from '../../../../base/common/worker/webWorker.js'; import { WorkerTextModelSyncClient } from '../../../../editor/common/services/textModelSync/textModelSync.impl.js'; +import { FileAccess } from '../../../../base/common/network.js'; export class OutputLinkProvider extends Disposable { @@ -88,7 +89,7 @@ export class OutputLinkProvider extends Disposable { } class OutputLinkWorkerClient extends Disposable { - private readonly _workerClient: IWorkerClient; + private readonly _workerClient: IWebWorkerClient; private readonly _workerTextModelSyncClient: WorkerTextModelSyncClient; private readonly _initializeBarrier: Promise; @@ -98,7 +99,7 @@ class OutputLinkWorkerClient extends Disposable { ) { super(); this._workerClient = this._register(createWebWorker( - 'vs/workbench/contrib/output/common/outputLinkComputer', + FileAccess.asBrowserUri('vs/workbench/contrib/output/common/outputLinkComputerMain.js'), 'OutputLinkDetectionWorker' )); this._workerTextModelSyncClient = WorkerTextModelSyncClient.create(this._workerClient, modelService); diff --git a/src/vs/workbench/contrib/output/common/outputLinkComputer.ts b/src/vs/workbench/contrib/output/common/outputLinkComputer.ts index bed7368c..95073543 100644 --- a/src/vs/workbench/contrib/output/common/outputLinkComputer.ts +++ b/src/vs/workbench/contrib/output/common/outputLinkComputer.ts @@ -11,20 +11,20 @@ import * as strings from '../../../../base/common/strings.js'; import { Range } from '../../../../editor/common/core/range.js'; import { isWindows } from '../../../../base/common/platform.js'; import { Schemas } from '../../../../base/common/network.js'; -import { IRequestHandler, IWorkerServer } from '../../../../base/common/worker/simpleWorker.js'; +import { IWebWorkerServerRequestHandler, IWebWorkerServer } from '../../../../base/common/worker/webWorker.js'; import { WorkerTextModelSyncServer, ICommonModel } from '../../../../editor/common/services/textModelSync/textModelSync.impl.js'; export interface IResourceCreator { toResource: (folderRelativePath: string) => URI | null; } -export class OutputLinkComputer implements IRequestHandler { +export class OutputLinkComputer implements IWebWorkerServerRequestHandler { _requestHandlerBrand: any; private readonly workerTextModelSyncServer = new WorkerTextModelSyncServer(); private patterns = new Map(); - constructor(workerServer: IWorkerServer) { + constructor(workerServer: IWebWorkerServer) { this.workerTextModelSyncServer.bindToServer(workerServer); } @@ -181,10 +181,6 @@ export class OutputLinkComputer implements IRequestHandler { } } -/** - * Defines the worker entry point. Must be exported and named `create`. - * @skipMangle - */ -export function create(workerServer: IWorkerServer): OutputLinkComputer { +export function create(workerServer: IWebWorkerServer): OutputLinkComputer { return new OutputLinkComputer(workerServer); } diff --git a/src/vs/workbench/contrib/output/common/outputLinkComputerMain.ts b/src/vs/workbench/contrib/output/common/outputLinkComputerMain.ts index 3f296c6d..53283d5b 100644 --- a/src/vs/workbench/contrib/output/common/outputLinkComputerMain.ts +++ b/src/vs/workbench/contrib/output/common/outputLinkComputerMain.ts @@ -4,6 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { create } from './outputLinkComputer.js'; -import { bootstrapSimpleWorker } from '../../../../base/common/worker/simpleWorkerBootstrap.js'; +import { bootstrapWebWorker } from '../../../../base/common/worker/webWorkerBootstrap.js'; -bootstrapSimpleWorker(create); +bootstrapWebWorker(create); diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts index d896d865..af019346 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts @@ -26,6 +26,7 @@ import { assertIsDefined } from '../../../../base/common/types.js'; import { isEqual } from '../../../../base/common/resources.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; import { DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, IDefineKeybindingEditorContribution } from '../../../services/preferences/common/preferences.js'; +import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; const NLS_KB_LAYOUT_ERROR_MESSAGE = nls.localize('defineKeybinding.kbLayoutErrorMessage', "You won't be able to produce this key combination under your current keyboard layout."); @@ -88,13 +89,14 @@ class DefineKeybindingEditorContribution extends Disposable implements IDefineKe export class KeybindingEditorDecorationsRenderer extends Disposable { private _updateDecorations: RunOnceScheduler; - private readonly _dec = this._editor.createDecorationsCollection(); + private readonly _dec: IEditorDecorationsCollection; constructor( private _editor: ICodeEditor, @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); + this._dec = this._editor.createDecorationsCollection(); this._updateDecorations = this._register(new RunOnceScheduler(() => this._updateDecorationsNow(), 500)); diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 9bdbebe0..d69acc0a 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -24,7 +24,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from '../../../browser/actions/workspaceCommands.js'; import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; -import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { ResourceContextKey, RemoteNameContext, WorkbenchStateContext } from '../../../common/contextkeys.js'; @@ -39,7 +39,6 @@ import { PreferencesContribution } from '../common/preferencesContribution.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { KeybindingsEditorInput } from '../../../services/preferences/browser/keybindingsEditorInput.js'; import { DEFINE_KEYBINDING_EDITOR_CONTRIB_ID, IDefineKeybindingEditorContribution, IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { SettingsEditor2Input } from '../../../services/preferences/common/preferencesEditorInput.js'; @@ -1239,6 +1238,9 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } class SettingsEditorTitleContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.settingsEditorTitleBarActions'; + constructor( @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @@ -1249,13 +1251,13 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo private registerSettingsEditorTitleActions() { const registerOpenUserSettingsEditorFromJsonActionDisposables = this._register(new MutableDisposable()); - const openUserSettingsEditorWhen = ContextKeyExpr.and( - ContextKeyExpr.or( - ResourceContextKey.Resource.isEqualTo(this.userDataProfileService.currentProfile.settingsResource.toString()), - ResourceContextKey.Resource.isEqualTo(this.userDataProfilesService.defaultProfile.settingsResource.toString())), - ContextKeyExpr.not('isInDiffEditor')); const registerOpenUserSettingsEditorFromJsonAction = () => { - registerOpenUserSettingsEditorFromJsonActionDisposables.value = undefined; + const openUserSettingsEditorWhen = ContextKeyExpr.and( + CONTEXT_SETTINGS_EDITOR.toNegated(), + ContextKeyExpr.or( + ResourceContextKey.Resource.isEqualTo(this.userDataProfileService.currentProfile.settingsResource.toString()), + ResourceContextKey.Resource.isEqualTo(this.userDataProfilesService.defaultProfile.settingsResource.toString())), + ContextKeyExpr.not('isInDiffEditor')); registerOpenUserSettingsEditorFromJsonActionDisposables.value = registerAction2(class extends Action2 { constructor() { super({ @@ -1271,9 +1273,9 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo }); } run(accessor: ServicesAccessor, ...args: unknown[]) { - const sanatizedArgs = sanitizeOpenSettingsArgs(args[0]); + const sanitizedArgs = sanitizeOpenSettingsArgs(args[0]); const groupId = getEditorGroupFromArguments(accessor, args)?.id; - return accessor.get(IPreferencesService).openUserSettings({ jsonEditor: false, ...sanatizedArgs, groupId }); + return accessor.get(IPreferencesService).openUserSettings({ jsonEditor: false, ...sanitizedArgs, groupId }); } }); }; @@ -1284,7 +1286,7 @@ class SettingsEditorTitleContribution extends Disposable implements IWorkbenchCo registerOpenUserSettingsEditorFromJsonAction(); })); - const openSettingsJsonWhen = ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()); + const openSettingsJsonWhen = ContextKeyExpr.and(CONTEXT_SETTINGS_JSON_EDITOR.toNegated(), CONTEXT_SETTINGS_EDITOR); this._register(registerAction2(class extends Action2 { constructor() { super({ @@ -1316,10 +1318,9 @@ function getEditorGroupFromArguments(accessor: ServicesAccessor, args: unknown[] return context.groupedEditors[0]?.group; } -const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); registerWorkbenchContribution2(PreferencesActionsContribution.ID, PreferencesActionsContribution, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(PreferencesContribution.ID, PreferencesContribution, WorkbenchPhase.BlockStartup); -workbenchContributionsRegistry.registerWorkbenchContribution(SettingsEditorTitleContribution, LifecyclePhase.Restored); +registerWorkbenchContribution2(SettingsEditorTitleContribution.ID, SettingsEditorTitleContribution, WorkbenchPhase.AfterRestored); registerEditorContribution(SettingsEditorContribution.ID, SettingsEditorContribution, EditorContributionInstantiation.AfterFirstRender); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index c8f0b595..1d6c0b83 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -787,7 +787,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc class WorkspaceConfigurationRenderer extends Disposable { private static readonly supportedKeys = ['folders', 'tasks', 'launch', 'extensions', 'settings', 'remoteAuthority', 'transient']; - private readonly decorations = this.editor.createDecorationsCollection(); + private readonly decorations: editorCommon.IEditorDecorationsCollection; private renderingDelayer: Delayer = new Delayer(200); constructor(private editor: ICodeEditor, private workspaceSettingsEditorModel: SettingsEditorModel, @@ -795,6 +795,7 @@ class WorkspaceConfigurationRenderer extends Disposable { @IMarkerService private readonly markerService: IMarkerService ) { super(); + this.decorations = this.editor.createDecorationsCollection(); this._register(this.editor.getModel()!.onDidChangeContent(() => this.renderingDelayer.trigger(() => this.render()))); } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 57f59b72..762a4622 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISettingsEditorModel, ISetting, ISettingsGroup, ISearchResult, IGroupFilter, SettingMatchType, ISettingMatch, SettingKeyMatchTypes } from '../../../services/preferences/common/preferences.js'; +import { ISettingsEditorModel, ISetting, ISettingsGroup, ISearchResult, IGroupFilter, SettingMatchType, ISettingMatch, SettingKeyMatchTypes, ISettingMatcher } from '../../../services/preferences/common/preferences.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { distinct } from '../../../../base/common/arrays.js'; import * as strings from '../../../../base/common/strings.js'; @@ -82,9 +82,6 @@ function cleanFilter(filter: string): string { } export class LocalSearchProvider implements ISearchProvider { - static readonly EXACT_MATCH_SCORE = 10000; - static readonly START_SCORE = 1000; - constructor( private _filter: string, @IConfigurationService private readonly configurationService: IConfigurationService @@ -92,60 +89,43 @@ export class LocalSearchProvider implements ISearchProvider { this._filter = cleanFilter(this._filter); } - searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken): Promise { + searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise { if (!this._filter) { return Promise.resolve(null); } - let orderedScore = LocalSearchProvider.START_SCORE; // Sort is not stable - const useNewKeyMatchAlgorithm = this.configurationService.getValue('workbench.settings.useWeightedKeySearch') === true; - const settingMatcher = (setting: ISetting) => { - const { matches, matchType, keyMatchScore } = new SettingMatches( + const settingMatcher: ISettingMatcher = (setting: ISetting) => { + let { matches, matchType, keyMatchScore } = new SettingMatches( this._filter, setting, true, - (filter, setting) => preferencesModel.findValueMatches(filter, setting), - useNewKeyMatchAlgorithm, this.configurationService ); if (matchType === SettingMatchType.None || matches.length === 0) { return null; } - - const score = strings.equalsIgnoreCase(this._filter, setting.key) ? - LocalSearchProvider.EXACT_MATCH_SCORE : - orderedScore--; + if (strings.equalsIgnoreCase(this._filter, setting.key)) { + matchType = SettingMatchType.ExactMatch; + } return { matches, matchType, keyMatchScore, - score + score: 0 // only used for RemoteSearchProvider matches. }; }; const filterMatches = preferencesModel.filterSettings(this._filter, this.getGroupFilter(this._filter), settingMatcher); - const exactMatch = filterMatches.find(m => m.score === LocalSearchProvider.EXACT_MATCH_SCORE); - if (exactMatch) { - return Promise.resolve({ - filterMatches: [exactMatch], - exactMatch: true - }); - } else if (useNewKeyMatchAlgorithm) { - // Check the top key match type. - const topKeyMatchType = Math.max(...filterMatches.map(m => (m.matchType & SettingKeyMatchTypes))); - // Always allow description matches as part of https://github.com/microsoft/vscode/issues/239936. - const alwaysAllowedMatchTypes = SettingMatchType.DescriptionOrValueMatch | SettingMatchType.LanguageTagSettingMatch; - const filteredMatches = filterMatches.filter(m => (m.matchType & topKeyMatchType) || (m.matchType & alwaysAllowedMatchTypes)); - return Promise.resolve({ - filterMatches: filteredMatches, - exactMatch: false - }); - } else { - return Promise.resolve({ - filterMatches: filterMatches, - exactMatch: false - }); - } + + // Check the top key match type. + const topKeyMatchType = Math.max(...filterMatches.map(m => (m.matchType & SettingKeyMatchTypes))); + // Always allow description matches as part of https://github.com/microsoft/vscode/issues/239936. + const alwaysAllowedMatchTypes = SettingMatchType.DescriptionOrValueMatch | SettingMatchType.LanguageTagSettingMatch; + const filteredMatches = filterMatches.filter(m => (m.matchType & topKeyMatchType) || (m.matchType & alwaysAllowedMatchTypes) || m.matchType === SettingMatchType.ExactMatch); + return Promise.resolve({ + filterMatches: filteredMatches, + exactMatch: filteredMatches.some(m => m.matchType === SettingMatchType.ExactMatch) + }); } private getGroupFilter(filter: string): IGroupFilter { @@ -169,8 +149,6 @@ export class SettingMatches { searchString: string, setting: ISetting, private searchDescription: boolean, - valuesMatcher: (filter: string, setting: ISetting) => IRange[], - private useNewKeyMatchAlgorithm: boolean, private readonly configurationService: IConfigurationService ) { this.matches = distinct(this._findMatchesInSetting(searchString, setting), (match) => `${match.startLineNumber}_${match.startColumn}_${match.endLineNumber}_${match.endColumn}_`); @@ -205,68 +183,52 @@ export class SettingMatches { const settingKeyAsWords: string = this._keyToLabel(setting.key); const queryWords = new Set(searchString.split(' ')); for (const word of queryWords) { - // Check if the key contains the word. - // Force contiguous matching iff we're using the new algorithm. - const keyMatches = matchesWords(word, settingKeyAsWords, this.useNewKeyMatchAlgorithm); + // Check if the key contains the word. Use contiguous search. + const keyMatches = matchesWords(word, settingKeyAsWords, true); if (keyMatches?.length) { keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); } } - if (this.useNewKeyMatchAlgorithm) { - // New key match algorithm - if (keyMatchingWords.size === queryWords.size) { - // All words in the query matched with something in the setting key. - // Matches "edit format on paste" to "editor.formatOnPaste". - this.matchType |= SettingMatchType.AllWordsInSettingsLabel; - } else if (keyMatchingWords.size >= 2) { - // Matches "edit paste" to "editor.formatOnPaste". - // The if statement reduces noise by preventing "editor formatonpast" from matching all editor settings. - this.matchType |= SettingMatchType.ContiguousWordsInSettingsLabel; - this.keyMatchScore = keyMatchingWords.size; - } - const searchStringAlphaNumeric = this._toAlphaNumeric(searchString); - const keyAlphaNumeric = this._toAlphaNumeric(setting.key); - const keyIdMatches = matchesContiguousSubString(searchStringAlphaNumeric, keyAlphaNumeric); - if (keyIdMatches?.length) { - // Matches "editorformatonp" to "editor.formatonpaste". - keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); - this.matchType |= SettingMatchType.ContiguousQueryInSettingId; - } + if (keyMatchingWords.size === queryWords.size) { + // All words in the query matched with something in the setting key. + // Matches "edit format on paste" to "editor.formatOnPaste". + this.matchType |= SettingMatchType.AllWordsInSettingsLabel; + } else if (keyMatchingWords.size >= 2) { + // Matches "edit paste" to "editor.formatOnPaste". + // The if statement reduces noise by preventing "editor formatonpast" from matching all editor settings. + this.matchType |= SettingMatchType.ContiguousWordsInSettingsLabel; + this.keyMatchScore = keyMatchingWords.size; + } + const searchStringAlphaNumeric = this._toAlphaNumeric(searchString); + const keyAlphaNumeric = this._toAlphaNumeric(setting.key); + const keyIdMatches = matchesContiguousSubString(searchStringAlphaNumeric, keyAlphaNumeric); + if (keyIdMatches?.length) { + // Matches "editorformatonp" to "editor.formatonpaste". + keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); + this.matchType |= SettingMatchType.ContiguousQueryInSettingId; + } - // Fall back to non-contiguous searches if nothing matched yet. - if (this.matchType === SettingMatchType.None) { - keyMatchingWords.clear(); - for (const word of queryWords) { - const keyMatches = matchesWords(word, settingKeyAsWords, false); - if (keyMatches?.length) { - keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); - } - } - if (keyMatchingWords.size >= 2 || (keyMatchingWords.size === 1 && queryWords.size === 1)) { - // Matches "edforonpas" to "editor.formatOnPaste". - // The if statement reduces noise by preventing "editor fomonpast" from matching all editor settings. - this.matchType |= SettingMatchType.NonContiguousWordsInSettingsLabel; - this.keyMatchScore = keyMatchingWords.size; - } else { - const keyIdMatches = matchesSubString(searchStringAlphaNumeric, keyAlphaNumeric); - if (keyIdMatches?.length) { - // Matches "edfmonpas" to "editor.formatOnPaste". - keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); - this.matchType |= SettingMatchType.NonContiguousQueryInSettingId; - } + // Fall back to non-contiguous key (ID) searches if nothing matched yet. + if (this.matchType === SettingMatchType.None) { + keyMatchingWords.clear(); + for (const word of queryWords) { + const keyMatches = matchesWords(word, settingKeyAsWords, false); + if (keyMatches?.length) { + keyMatchingWords.set(word, keyMatches.map(match => this.toKeyRange(setting, match))); } } - } else { - // Old key match algorithm - if (keyMatchingWords.size) { + if (keyMatchingWords.size >= 2 || (keyMatchingWords.size === 1 && queryWords.size === 1)) { + // Matches "edforonpas" to "editor.formatOnPaste". + // The if statement reduces noise by preventing "editor fomonpast" from matching all editor settings. this.matchType |= SettingMatchType.NonContiguousWordsInSettingsLabel; this.keyMatchScore = keyMatchingWords.size; - } - const keyIdMatches = matchesContiguousSubString(searchString, setting.key); - if (keyIdMatches?.length) { - // Handles cases such as "editor.formatonpaste" where the user tries searching for the ID. - keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); - this.matchType |= SettingMatchType.ContiguousQueryInSettingId; + } else { + const keyIdMatches = matchesSubString(searchStringAlphaNumeric, keyAlphaNumeric); + if (keyIdMatches?.length) { + // Matches "edfmonpas" to "editor.formatOnPaste". + keyMatchingWords.set(setting.key, keyIdMatches.map(match => this.toKeyRange(setting, match))); + this.matchType |= SettingMatchType.NonContiguousQueryInSettingId; + } } } @@ -280,11 +242,9 @@ export class SettingMatches { } // Description search - // Old algorithm: search the description if we haven't matched anything yet. - // New algorithm: search the description if we found non-contiguous key matches at best. + // Search the description if we found non-contiguous key matches at best. const hasContiguousKeyMatchTypes = this.matchType >= SettingMatchType.ContiguousWordsInSettingsLabel; - const checkDescription = (!this.useNewKeyMatchAlgorithm && this.matchType === SettingMatchType.None) || (this.useNewKeyMatchAlgorithm && !hasContiguousKeyMatchTypes); - if (this.searchDescription && checkDescription) { + if (this.searchDescription && !hasContiguousKeyMatchTypes) { for (const word of queryWords) { // Search the description lines. for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { @@ -304,10 +264,8 @@ export class SettingMatches { // Value search // Check if the value contains all the words. - // Old algorithm: always search the values. - // New algorithm: search the values if we found non-contiguous key matches at best. - const checkValue = !this.useNewKeyMatchAlgorithm || !hasContiguousKeyMatchTypes; - if (checkValue) { + // Search the values if we found non-contiguous key matches at best. + if (!hasContiguousKeyMatchTypes) { if (setting.enum?.length) { // Search all string values of enums. for (const option of setting.enum) { @@ -459,7 +417,7 @@ class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { this._filter = cleanFilter(filter); } - async searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken | undefined): Promise { + async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise { if ( !this._filter || !this.aiRelatedInformationService.isEnabled() @@ -470,18 +428,19 @@ class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { this._keysProvider.updateModel(preferencesModel); return { - filterMatches: await this.getAiRelatedInformationItems(token) + filterMatches: await this.getAiRelatedInformationItems(token), + exactMatch: false }; } - private async getAiRelatedInformationItems(token?: CancellationToken | undefined) { + private async getAiRelatedInformationItems(token: CancellationToken) { const settingsRecord = this._keysProvider.getSettingsRecord(); const filterMatches: ISettingMatch[] = []; const relatedInformation = await this.aiRelatedInformationService.getRelatedInformation( this._filter, [RelatedInformationType.SettingInformation], - token ?? CancellationToken.None + token ) as SettingInformationResult[]; relatedInformation.sort((a, b) => b.weight - a.weight); @@ -537,7 +496,7 @@ class TfIdfSearchProvider implements IRemoteSearchProvider { return result; } - async searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken | undefined): Promise { + async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise { if (!this._filter) { return null; } @@ -564,15 +523,16 @@ class TfIdfSearchProvider implements IRemoteSearchProvider { } return { - filterMatches: await this.getTfIdfItems(token) + filterMatches: await this.getTfIdfItems(token), + exactMatch: false }; } - private async getTfIdfItems(token?: CancellationToken | undefined): Promise { + private async getTfIdfItems(token: CancellationToken): Promise { const filterMatches: ISettingMatch[] = []; const tfIdfCalculator = new TfIdfCalculator(); tfIdfCalculator.updateDocuments(this._documents); - const tfIdfRankings = tfIdfCalculator.calculateScores(this._filter, token ?? CancellationToken.None); + const tfIdfRankings = tfIdfCalculator.calculateScores(this._filter, token); tfIdfRankings.sort((a, b) => b.score - a.score); const maxScore = tfIdfRankings[0].score; @@ -625,9 +585,9 @@ class RemoteSearchProvider implements IRemoteSearchProvider { this.tfIdfSearchProvider!.setFilter(filter); } - searchModel(preferencesModel: ISettingsEditorModel, token?: CancellationToken): Promise { + async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise { if (!this.filter) { - return Promise.resolve(null); + return null; } if (!this.adaSearchProvider) { @@ -635,9 +595,17 @@ class RemoteSearchProvider implements IRemoteSearchProvider { } // Use TF-IDF search as a fallback, ref https://github.com/microsoft/vscode/issues/224946 - return this.adaSearchProvider.searchModel(preferencesModel, token).then((results) => { - return results?.filterMatches.length ? results : this.tfIdfSearchProvider!.searchModel(preferencesModel, token); - }); + let results = await this.adaSearchProvider.searchModel(preferencesModel, token); + if (results?.filterMatches.length) { + return results; + } + if (!token.isCancellationRequested) { + results = await this.tfIdfSearchProvider!.searchModel(preferencesModel, token); + if (results?.filterMatches.length) { + return results; + } + } + return null; } } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 84321a35..9bc4c943 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -37,6 +37,7 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; export class FolderSettingsActionViewItem extends BaseActionViewItem { @@ -500,13 +501,14 @@ export class EditPreferenceWidget extends Disposable { private _line: number = -1; private _preferences: T[] = []; - private readonly _editPreferenceDecoration = this.editor.createDecorationsCollection(); + private readonly _editPreferenceDecoration: IEditorDecorationsCollection; private readonly _onClick = this._register(new Emitter()); readonly onClick: Event = this._onClick.event; constructor(private editor: ICodeEditor) { super(); + this._editPreferenceDecoration = this.editor.createDecorationsCollection(); this._register(this.editor.onMouseDown((e: IEditorMouseEvent) => { if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN || e.target.detail.isAfterLines || !this.isVisible()) { return; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index ca7251ac..e5de67fb 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -413,6 +413,7 @@ export class SettingsEditor2 extends EditorPane { // Don't block setInput on render (which can trigger an async search) this.onConfigUpdate(undefined, true).then(() => { + // This event runs when the editor closes. this.inputChangeListener.value = input.onWillDispose(() => { this.searchWidget.setValue(''); }); @@ -1540,7 +1541,9 @@ export class SettingsEditor2 extends EditorPane { private refreshSingleElement(element: SettingsTreeSettingElement): void { if (this.isVisible()) { - this.settingsTree.rerender(element); + if (!element.setting.deprecationMessage || element.isConfigured) { + this.settingsTree.rerender(element); + } } } @@ -1634,8 +1637,7 @@ export class SettingsEditor2 extends EditorPane { this.searchDelayer.cancel(); if (this.searchInProgress) { - this.searchInProgress.cancel(); - this.searchInProgress.dispose(); + this.searchInProgress.dispose(true); this.searchInProgress = null; } @@ -1670,7 +1672,8 @@ export class SettingsEditor2 extends EditorPane { const filterModel = this.instantiationService.createInstance(SearchResultModel, this.viewState, this.settingsOrderByTocIndex, this.workspaceTrustManagementService.isWorkspaceTrusted()); const fullResult: ISearchResult = { - filterMatches: [] + filterMatches: [], + exactMatch: false, }; for (const g of this.defaultSettingsEditorModel.settingsGroups.slice(1)) { for (const sect of g.sections) { @@ -1686,23 +1689,24 @@ export class SettingsEditor2 extends EditorPane { private async triggerFilterPreferences(query: string): Promise { if (this.searchInProgress) { - this.searchInProgress.cancel(); + this.searchInProgress.dispose(true); this.searchInProgress = null; } // Trigger the local search. If it didn't find an exact match, trigger the remote search. const searchInProgress = this.searchInProgress = new CancellationTokenSource(); return this.searchDelayer.trigger(async () => { - if (!searchInProgress.token.isCancellationRequested) { - const localResults = await this.localFilterPreferences(query, searchInProgress.token); - if (localResults && !localResults.exactMatch && !searchInProgress.token.isCancellationRequested) { - await this.remoteSearchPreferences(query, searchInProgress.token); - } - - // Update UI only after all the search results are in - // ref https://github.com/microsoft/vscode/issues/224946 - this.onDidFinishSearch(); + if (searchInProgress.token.isCancellationRequested) { + return; } + const localResults = await this.localFilterPreferences(query, searchInProgress.token); + if (localResults && !localResults.exactMatch && !searchInProgress.token.isCancellationRequested) { + await this.remoteSearchPreferences(query, searchInProgress.token); + } + + // Update UI only after all the search results are in + // ref https://github.com/microsoft/vscode/issues/224946 + this.onDidFinishSearch(); }); } @@ -1717,19 +1721,22 @@ export class SettingsEditor2 extends EditorPane { this.renderTree(undefined, true); } - private localFilterPreferences(query: string, token?: CancellationToken): Promise { + private localFilterPreferences(query: string, token: CancellationToken): Promise { const localSearchProvider = this.preferencesSearchService.getLocalSearchProvider(query); - return this.filterOrSearchPreferences(query, SearchResultIdx.Local, localSearchProvider, token); + return this.searchWithProvider(SearchResultIdx.Local, localSearchProvider, token); } - private remoteSearchPreferences(query: string, token?: CancellationToken): Promise { + private remoteSearchPreferences(query: string, token: CancellationToken): Promise { const remoteSearchProvider = this.preferencesSearchService.getRemoteSearchProvider(query); - return this.filterOrSearchPreferences(query, SearchResultIdx.Remote, remoteSearchProvider, token); + if (!remoteSearchProvider) { + return Promise.resolve(null); + } + return this.searchWithProvider(SearchResultIdx.Remote, remoteSearchProvider, token); } - private async filterOrSearchPreferences(query: string, type: SearchResultIdx, searchProvider?: ISearchProvider, token?: CancellationToken): Promise { - const result = await this._filterOrSearchPreferencesModel(query, this.defaultSettingsEditorModel, searchProvider, token); - if (token?.isCancellationRequested) { + private async searchWithProvider(type: SearchResultIdx, searchProvider: ISearchProvider, token: CancellationToken): Promise { + const result = await this._searchPreferencesModel(this.defaultSettingsEditorModel, searchProvider, token); + if (token.isCancellationRequested) { // Handle cancellation like this because cancellation is lost inside the search provider due to async/await return null; } @@ -1782,31 +1789,16 @@ export class SettingsEditor2 extends EditorPane { } } - private _filterOrSearchPreferencesModel(filter: string, model: ISettingsEditorModel, provider?: ISearchProvider, token?: CancellationToken): Promise { - const searchP = provider ? provider.searchModel(model, token) : Promise.resolve(null); - return searchP - .then(undefined, err => { - if (isCancellationError(err)) { - return Promise.reject(err); - } else { - // type SettingsSearchErrorEvent = { - // 'message': string; - // }; - // type SettingsSearchErrorClassification = { - // owner: 'rzhao271'; - // comment: 'Helps understand when settings search errors out'; - // 'message': { 'classification': 'CallstackOrException'; 'purpose': 'FeatureInsight'; 'owner': 'rzhao271'; 'comment': 'The error message of the search error.' }; - // }; - - // const message = getErrorMessage(err).trim(); - // if (message && message !== 'Error') { - // // "Error" = any generic network error - // this.telemetryService.publicLogError2('settingsEditor.searchError', { message }); - // this.logService.info('Setting search error: ' + message); - // } - return null; - } - }); + private async _searchPreferencesModel(model: ISettingsEditorModel, provider: ISearchProvider, token: CancellationToken): Promise { + try { + return await provider.searchModel(model, token); + } catch (err) { + if (isCancellationError(err)) { + return Promise.reject(err); + } else { + return null; + } + } } private layoutSplitView(dimension: DOM.Dimension): void { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 678042ec..c669643a 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -145,7 +145,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const content = localize('trustLabel', "The setting value can only be applied in a trusted workspace."); const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, content, target: workspaceTrustElement, @@ -186,7 +186,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const syncIgnoredHoverContent = localize('syncIgnoredTitle', "This setting is ignored during sync"); const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, content: syncIgnoredHoverContent, target: syncIgnoredElement @@ -329,7 +329,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const content = isPreviewSetting ? PREVIEW_INDICATOR_DESCRIPTION : EXPERIMENTAL_INDICATOR_DESCRIPTION; const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, content, target: this.previewIndicator.element @@ -374,7 +374,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { this.scopeOverridesIndicator.label.text = '$(briefcase) ' + localize('policyLabelText', "Managed by organization"); const content = localize('policyDescription', "This setting is managed by your organization and its actual value cannot be changed."); const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, content, actions: [{ @@ -396,7 +396,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const content = localize('applicationSettingDescription', "The setting is not specific to the current profile, and will retain its value when switching profiles."); const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, content, target: this.scopeOverridesIndicator.element @@ -512,7 +512,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { } const showHover = (focus: boolean) => { - return this.hoverService.showHover({ + return this.hoverService.showInstantHover({ content: new MarkdownString().appendMarkdown(defaultOverrideHoverContent), target: this.defaultOverrideIndicator.element, position: { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 8408d9a1..43df1e38 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isWeb, isWindows } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; import { ExtensionToggleData } from '../common/preferences.js'; @@ -12,6 +13,7 @@ export interface ITOCEntry { order?: number; children?: ITOCEntry[]; settings?: Array; + hide?: boolean; } const defaultCommonlyUsedSettings: string[] = [ @@ -234,12 +236,13 @@ export const tocData: ITOCEntry = { { id: 'features/chat', label: localize('chat', 'Chat'), - settings: ['chat.*', 'inlineChat.*'] + settings: ['chat.*', 'inlineChat.*', 'mcp'] }, { id: 'features/issueReporter', label: localize('issueReporter', 'Issue Reporter'), - settings: ['issueReporter.*'] + settings: ['issueReporter.*'], + hide: !isWeb } ] }, @@ -280,7 +283,8 @@ export const tocData: ITOCEntry = { { id: 'application/other', label: localize('other', "Other"), - settings: ['application.*'] + settings: ['application.*'], + hide: isWindows } ] }, diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index be9840d7..2f49546c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -33,8 +33,9 @@ import { Disposable, DisposableStore, isDisposable, toDisposable } from '../../. import { isIOS } from '../../../../base/common/platform.js'; import { escapeRegExpCharacters } from '../../../../base/common/strings.js'; import { isDefined, isUndefinedOrNull } from '../../../../base/common/types.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { URI } from '../../../../base/common/uri.js'; import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { localize } from '../../../../nls.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -42,6 +43,7 @@ import { ConfigurationTarget, IConfigurationService, getLanguageTagSettingPlainK import { ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IListService, WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js'; @@ -55,23 +57,20 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { getIgnoredSettings } from '../../../../platform/userDataSync/common/settingsMerge.js'; import { IUserDataSyncEnablementService, getDefaultIgnoredSettings } from '../../../../platform/userDataSync/common/userDataSync.js'; +import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { ISetting, ISettingsGroup, SETTINGS_AUTHORITY, SettingValueType } from '../../../services/preferences/common/preferences.js'; +import { getInvalidTypeError } from '../../../services/preferences/common/preferencesValidation.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; +import { LANGUAGE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, compareTwoNullableNumbers } from '../common/preferences.js'; +import { settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; import { settingsMoreActionIcon } from './preferencesIcons.js'; import { SettingsTarget } from './preferencesWidgets.js'; import { ISettingOverrideClickEvent, SettingsTreeIndicatorsLabel, getIndicatorsLabelAriaLabel } from './settingsEditorSettingIndicators.js'; import { ITOCEntry } from './settingsLayout.js'; import { ISettingsEditorViewState, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement, inspectSetting, objectSettingSupportsRemoveDefaultValue, settingKeyToDisplayFormat } from './settingsTreeModels.js'; import { ExcludeSettingWidget, IBoolObjectDataItem, IIncludeExcludeDataItem, IListDataItem, IObjectDataItem, IObjectEnumOption, IObjectKeySuggester, IObjectValueSuggester, IncludeSettingWidget, ListSettingWidget, ObjectSettingCheckboxWidget, ObjectSettingDropdownWidget, ObjectValue, SettingListEvent } from './settingsWidgets.js'; -import { LANGUAGE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, compareTwoNullableNumbers } from '../common/preferences.js'; -import { settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; -import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { ISetting, ISettingsGroup, SETTINGS_AUTHORITY, SettingValueType } from '../../../services/preferences/common/preferences.js'; -import { getInvalidTypeError } from '../../../services/preferences/common/preferencesValidation.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { URI } from '../../../../base/common/uri.js'; const $ = DOM.$; @@ -568,6 +567,7 @@ function _resolveSettingsTree(tocData: ITOCEntry, allSettings: Set[] | undefined; if (tocData.children) { children = tocData.children + .filter(child => child.hide !== true) .map(child => _resolveSettingsTree(child, allSettings, logService)) .filter(child => child.children?.length || child.settings?.length); } @@ -874,7 +874,9 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const descriptionElement = DOM.append(container, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - toDispose.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, () => localize('modified', "The setting has been configured in the current scope."))); + toDispose.add(this._hoverService.setupDelayedHover(modifiedIndicatorElement, { + content: localize('modified', "The setting has been configured in the current scope.") + })); const valueElement = DOM.append(container, $('.setting-item-value')); const controlElement = DOM.append(valueElement, $('div.setting-item-control')); @@ -961,7 +963,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const titleTooltip = setting.key + (element.isConfigured ? ' - Modified' : ''); template.categoryElement.textContent = element.displayCategory ? (element.displayCategory + ': ') : ''; - template.elementDisposables.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), template.categoryElement, titleTooltip)); + template.elementDisposables.add(this._hoverService.setupDelayedHover(template.categoryElement, { content: titleTooltip })); template.labelElement.text = element.displayLabel; template.labelElement.title = titleTooltip; @@ -1993,7 +1995,9 @@ class SettingBoolRenderer extends AbstractSettingRenderer implements ITreeRender const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - toDispose.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), modifiedIndicatorElement, localize('modified', "The setting has been configured in the current scope."))); + toDispose.add(this._hoverService.setupDelayedHover(modifiedIndicatorElement, { + content: localize('modified', "The setting has been configured in the current scope.") + })); const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index d48eb562..629e04c3 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -342,7 +342,7 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { // so we reset the default value source to the non-language-specific default value source for now. this.defaultValueSource = this.setting.nonLanguageSpecificDefaultValueSource; - if (inspected.policyValue) { + if (inspected.policyValue !== undefined) { this.hasPolicyValue = true; isConfigured = false; // The user did not manually configure the setting themselves. displayValue = inspected.policyValue; @@ -596,10 +596,15 @@ export class SettingsTreeModel implements IDisposable { if (tocEntry.settings) { const settingChildren = tocEntry.settings.map(s => this.createSettingsTreeSettingElement(s, element)); for (const child of settingChildren) { - if (!child.setting.deprecationMessage || child.isConfigured) { + if (!child.setting.deprecationMessage) { children.push(child); } else { - child.dispose(); + child.inspectSelf(); + if (child.isConfigured) { + children.push(child); + } else { + child.dispose(); + } } } } @@ -634,7 +639,7 @@ export class SettingsTreeModel implements IDisposable { this._userDataProfileService, this._configurationService); - const nameElements = this._treeElementsBySettingName.get(setting.key) || []; + const nameElements = this._treeElementsBySettingName.get(setting.key) ?? []; nameElements.push(element); this._treeElementsBySettingName.set(setting.key, nameElements); return element; @@ -1021,30 +1026,26 @@ export class SearchResultModel extends SettingsTreeModel { this.cachedUniqueSearchResults = { filterMatches: combinedFilterMatches, - exactMatch: localResult?.exactMatch || remoteResult?.exactMatch + exactMatch: localResult.exactMatch // remote results should never have an exact match }; return this.cachedUniqueSearchResults; } getRawResults(): ISearchResult[] { - return this.rawSearchResults || []; + return this.rawSearchResults ?? []; } setResult(order: SearchResultIdx, result: ISearchResult | null): void { this.cachedUniqueSearchResults = null; this.newExtensionSearchResults = null; - this.rawSearchResults = this.rawSearchResults || []; + this.rawSearchResults ??= []; if (!result) { delete this.rawSearchResults[order]; return; } - if (result.exactMatch) { - this.rawSearchResults = []; - } - this.rawSearchResults[order] = result; this.updateChildren(); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 258984d7..c6f874d2 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -8,30 +8,29 @@ import * as DOM from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; -import { Toggle, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js'; +import { applyDragImage } from '../../../../base/browser/ui/dnd/dnd.js'; import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; import { SelectBox } from '../../../../base/browser/ui/selectBox/selectBox.js'; +import { Toggle, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js'; import { IAction } from '../../../../base/common/actions.js'; import { disposableTimeout } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { isIOS } from '../../../../base/common/platform.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { isDefined, isUndefinedOrNull } from '../../../../base/common/types.js'; -import './media/settingsWidgets.css'; import { localize } from '../../../../nls.js'; import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { settingsDiscardIcon, settingsEditIcon, settingsRemoveIcon } from './preferencesIcons.js'; -import { settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; -import { defaultButtonStyles, getInputBoxStyle, getSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; +import { defaultButtonStyles, getInputBoxStyle, getSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { SettingValueType } from '../../../services/preferences/common/preferences.js'; +import { settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; +import './media/settingsWidgets.css'; +import { settingsDiscardIcon, settingsEditIcon, settingsRemoveIcon } from './preferencesIcons.js'; const $ = DOM.$; @@ -508,12 +507,6 @@ export class ListSettingWidget extends Abst private dragDetails: ListSettingWidgetDragDetails | undefined; - private getDragImage(item: TListDataItem): HTMLElement { - const dragImage = $('.monaco-drag-image'); - dragImage.textContent = item.value.data; - return dragImage; - } - protected renderItem(item: TListDataItem, idx: number): RowElementGroup { const rowElement = $('.setting-list-row'); const valueElement = DOM.append(rowElement, $('.setting-list-value')); @@ -541,13 +534,8 @@ export class ListSettingWidget extends Abst item, itemIndex: idx }; - if (ev.dataTransfer) { - ev.dataTransfer.dropEffect = 'move'; - const dragImage = this.getDragImage(item); - rowElement.ownerDocument.body.appendChild(dragImage); - ev.dataTransfer.setDragImage(dragImage, -10, -10); - setTimeout(() => dragImage.remove(), 0); - } + + applyDragImage(ev, rowElement, item.value.data); })); this.listDisposables.add(DOM.addDisposableListener(rowElement, DOM.EventType.DRAG_OVER, (ev) => { if (!this.dragDetails) { @@ -736,7 +724,7 @@ export class ListSettingWidget extends Abst : localize('listSiblingHintLabel', "List item `{0}` with sibling `${1}`", value.data, sibling); const { rowElement } = rowElementGroup; - this.listDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), rowElement, title)); + this.listDisposables.add(this.hoverService.setupDelayedHover(rowElement, { content: title })); rowElement.setAttribute('aria-label', title); } @@ -802,7 +790,7 @@ export class ExcludeSettingWidget extends ListSettingWidget; + searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise; } export interface IRemoteSearchProvider extends ISearchProvider { diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index b57d8931..e5cec6f2 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -125,13 +125,6 @@ registry.registerConfiguration({ 'description': nls.localize('settingsSearchTocBehavior', "Controls the behavior of the Settings editor Table of Contents while searching. If this setting is being changed in the Settings editor, the setting will take effect after the search query is modified."), 'default': 'filter', 'scope': ConfigurationScope.WINDOW - }, - 'workbench.settings.useWeightedKeySearch': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('useWeightedKeySearch', "Controls whether to use an experimental ranking algorithm for search results in the Settings editor. The newer algorithm is still in development and aims to show fewer and more relevant results."), - 'scope': ConfigurationScope.WINDOW, - 'tags': ['experimental'] } } }); diff --git a/src/vs/workbench/contrib/preferences/common/settingsFilesystemProvider.ts b/src/vs/workbench/contrib/preferences/common/settingsFilesystemProvider.ts index b57328ba..171b2458 100644 --- a/src/vs/workbench/contrib/preferences/common/settingsFilesystemProvider.ts +++ b/src/vs/workbench/contrib/preferences/common/settingsFilesystemProvider.ts @@ -14,6 +14,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import * as JSONContributionRegistry from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; +import { isEqual } from '../../../../base/common/resources.js'; const schemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -25,6 +26,8 @@ export class SettingsFileSystemProvider extends Disposable implements IFileSyste protected readonly _onDidChangeFile = this._register(new Emitter()); readonly onDidChangeFile = this._onDidChangeFile.event; + private static SCHEMA_ASSOCIATIONS = URI.parse(`${Schemas.vscode}://schemas-associations/schemas-associations.json`); + constructor( @IPreferencesService private readonly preferencesService: IPreferencesService, @ILogService private readonly logService: ILogService @@ -33,6 +36,9 @@ export class SettingsFileSystemProvider extends Disposable implements IFileSyste this._register(schemaRegistry.onDidChangeSchema(schemaUri => { this._onDidChangeFile.fire([{ resource: URI.parse(schemaUri), type: FileChangeType.UPDATED }]); })); + this._register(schemaRegistry.onDidChangeSchemaAssociations(() => { + this._onDidChangeFile.fire([{ resource: SettingsFileSystemProvider.SCHEMA_ASSOCIATIONS, type: FileChangeType.UPDATED }]); + })); this._register(preferencesService.onDidDefaultSettingsContentChanged(uri => { this._onDidChangeFile.fire([{ resource: uri, type: FileChangeType.UPDATED }]); })); @@ -47,6 +53,8 @@ export class SettingsFileSystemProvider extends Disposable implements IFileSyste let content: string | undefined; if (uri.authority === 'schemas') { content = this.getSchemaContent(uri); + } else if (uri.authority === SettingsFileSystemProvider.SCHEMA_ASSOCIATIONS.authority) { + content = JSON.stringify(schemaRegistry.getSchemaAssociations()); } else if (uri.authority === 'defaultsettings') { content = this.preferencesService.getDefaultSettingsContent(uri); } @@ -67,6 +75,16 @@ export class SettingsFileSystemProvider extends Disposable implements IFileSyste size: 0 }; } + if (isEqual(uri, SettingsFileSystemProvider.SCHEMA_ASSOCIATIONS)) { + const currentTime = Date.now(); + return { + type: FileType.File, + permissions: FilePermission.Readonly, + mtime: currentTime, + ctime: currentTime, + size: 0 + }; + } throw FileSystemProviderErrorCode.FileNotFound; } diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 3ab1684b..3ae6694f 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -32,7 +32,8 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { IWorkbenchQuickAccessConfiguration } from '../../../browser/quickaccess.js'; import { CHAT_OPEN_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; import { ASK_QUICK_QUESTION_ACTION_ID } from '../../chat/browser/actions/chatQuickInputActions.js'; -import { ChatAgentLocation, IChatAgentService } from '../../chat/common/chatAgents.js'; +import { IChatAgentService } from '../../chat/common/chatAgents.js'; +import { ChatAgentLocation } from '../../chat/common/constants.js'; import { CommandInformationResult, IAiRelatedInformationService, RelatedInformationType } from '../../../services/aiRelatedInformation/common/aiRelatedInformation.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index b01a519c..e64aa7ad 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -151,7 +151,7 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider { const paneComposites = this.paneCompositeService.getPaneComposites(location); diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index 9f53c894..3e8b69d1 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -22,6 +22,7 @@ import { IWorkbenchEnvironmentService } from '../../../services/environment/comm import { IProductService } from '../../../../platform/product/common/productService.js'; import { IUserDataSyncEnablementService, IUserDataSyncService, SyncStatus } from '../../../../platform/userDataSync/common/userDataSync.js'; import { IUserDataSyncWorkbenchService } from '../../../services/userDataSync/common/userDataSync.js'; +import { ChatConfiguration } from '../../chat/common/constants.js'; interface IConfiguration extends IWindowsConfiguration { update?: { mode?: string }; @@ -30,8 +31,10 @@ interface IConfiguration extends IWindowsConfiguration { security?: { workspace?: { trust?: { enabled?: boolean } }; restrictUNCAccess?: boolean }; window: IWindowSettings; workbench?: { enableExperiments?: boolean }; + telemetry?: { feedback?: { enabled?: boolean } }; _extensionsGallery?: { enablePPE?: boolean }; accessibility?: { verbosity?: { debug?: boolean } }; + chat?: { unifiedChatView?: boolean; useFileStorage?: boolean }; } export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution { @@ -41,19 +44,24 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo 'window.nativeTabs', 'window.nativeFullScreen', 'window.clickThroughInactive', + 'window.controlsStyle', 'update.mode', 'editor.accessibilitySupport', 'security.workspace.trust.enabled', 'workbench.enableExperiments', '_extensionsGallery.enablePPE', 'security.restrictUNCAccess', - 'accessibility.verbosity.debug' + 'accessibility.verbosity.debug', + ChatConfiguration.UnifiedChatView, + ChatConfiguration.UseFileStorage, + 'telemetry.feedback.enabled' ]; private readonly titleBarStyle = new ChangeObserver('string'); private readonly nativeTabs = new ChangeObserver('boolean'); private readonly nativeFullScreen = new ChangeObserver('boolean'); private readonly clickThroughInactive = new ChangeObserver('boolean'); + private readonly controlsStyle = new ChangeObserver('string'); private readonly updateMode = new ChangeObserver('string'); private accessibilitySupport: 'on' | 'off' | 'auto' | undefined; private readonly workspaceTrustEnabled = new ChangeObserver('boolean'); @@ -61,6 +69,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private readonly enablePPEExtensionsGallery = new ChangeObserver('boolean'); private readonly restrictUNCAccess = new ChangeObserver('boolean'); private readonly accessibilityVerbosityDebug = new ChangeObserver('boolean'); + private readonly unifiedChatView = new ChangeObserver('boolean'); + private readonly useFileStorage = new ChangeObserver('boolean'); + private readonly telemetryFeedbackEnabled = new ChangeObserver('boolean'); constructor( @IHostService private readonly hostService: IHostService, @@ -117,6 +128,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // macOS: Click through (accept first mouse) processChanged(isMacintosh && this.clickThroughInactive.handleChange(config.window?.clickThroughInactive)); + // Windows/Linux: Window controls style + processChanged(!isMacintosh && this.controlsStyle.handleChange(config.window?.controlsStyle)); + // Update mode processChanged(this.updateMode.handleChange(config.update?.mode)); @@ -136,6 +150,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // Debug accessibility verbosity processChanged(this.accessibilityVerbosityDebug.handleChange(config?.accessibility?.verbosity?.debug)); + + processChanged(this.unifiedChatView.handleChange(config.chat?.unifiedChatView)); + processChanged(this.useFileStorage.handleChange(config.chat?.useFileStorage)); } // Experiments @@ -144,6 +161,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // Profiles processChanged(this.productService.quality !== 'stable' && this.enablePPEExtensionsGallery.handleChange(config._extensionsGallery?.enablePPE)); + // Enable Feedback + processChanged(this.telemetryFeedbackEnabled.handleChange(config.telemetry?.feedback?.enabled)); + if (askToRelaunch && changed && this.hostService.hasFocus) { this.doConfirm( isNative ? diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index d8f7af34..2784bfce 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -8,11 +8,11 @@ import { IRemoteAgentService, remoteConnectionLatencyMeasurer } from '../../../s import { RunOnceScheduler, retry } from '../../../../base/common/async.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { MenuId, IMenuService, MenuItemAction, MenuRegistry, registerAction2, Action2, SubmenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { MenuId, IMenuService, MenuItemAction, MenuRegistry, registerAction2, Action2, SubmenuItemAction, IMenu } from '../../../../platform/actions/common/actions.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from '../../../services/statusbar/browser/statusbar.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; -import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { Schemas } from '../../../../base/common/network.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; @@ -41,7 +41,6 @@ import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { DomEmitter } from '../../../../base/browser/event.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { infoIcon } from '../../extensions/browser/extensionsIcons.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; @@ -51,6 +50,11 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { workbenchConfigurationNodeBase } from '../../../common/configuration.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import Severity from '../../../../base/common/severity.js'; +import { isCancellationError } from '../../../../base/common/errors.js'; +import { toErrorMessage } from '../../../../base/common/errorMessage.js'; +import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; type ActionGroup = [string, Array]; @@ -82,18 +86,16 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr private remoteStatusEntry: IStatusbarEntryAccessor | undefined; - private readonly legacyIndicatorMenu = this._register(this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService)); // to be removed once migration completed - private readonly remoteIndicatorMenu = this._register(this.menuService.createMenu(MenuId.StatusBarRemoteIndicatorMenu, this.contextKeyService)); + private readonly legacyIndicatorMenu: IMenu; // to be removed once migration completed + private readonly remoteIndicatorMenu: IMenu; private remoteMenuActionsGroups: ActionGroup[] | undefined; - private readonly remoteAuthority = this.environmentService.remoteAuthority; - private virtualWorkspaceLocation: { scheme: string; authority: string } | undefined = undefined; private connectionState: 'initializing' | 'connected' | 'reconnecting' | 'disconnected' | undefined = undefined; private connectionToken: string | undefined = undefined; - private readonly connectionStateContextKey = new RawContextKey<'' | 'initializing' | 'disconnected' | 'connected'>('remoteConnectionState', '').bindTo(this.contextKeyService); + private readonly connectionStateContextKey: IContextKey<'' | 'initializing' | 'disconnected' | 'connected'>; private networkState: 'online' | 'offline' | 'high-latency' | undefined = undefined; private measureNetworkConnectionLatencyScheduler: RunOnceScheduler | undefined = undefined; @@ -125,6 +127,10 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr return this._remoteExtensionMetadata; } + private get remoteAuthority(): string | undefined { + return this.environmentService.remoteAuthority; + } + private remoteMetadataInitialized: boolean = false; private readonly _onDidChangeEntries = this._register(new Emitter()); private readonly onDidChangeEntries: Event = this._onDidChangeEntries.event; @@ -147,11 +153,19 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr @ITelemetryService private readonly telemetryService: ITelemetryService, @IProductService private readonly productService: IProductService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IDialogService private readonly dialogService: IDialogService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, @IOpenerService private readonly openerService: IOpenerService, @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); + this.legacyIndicatorMenu = this._register(this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService)); // to be removed once migration completed + this.remoteIndicatorMenu = this._register(this.menuService.createMenu(MenuId.StatusBarRemoteIndicatorMenu, this.contextKeyService)); + + this.connectionStateContextKey = new RawContextKey<'' | 'initializing' | 'disconnected' | 'connected'>('remoteConnectionState', '').bindTo(this.contextKeyService); + // Set initial connection state if (this.remoteAuthority) { this.connectionState = 'initializing'; @@ -618,14 +632,27 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr return markdownTooltip; } - private async installExtension(extensionId: string) { - const galleryExtension = (await this.extensionGalleryService.getExtensions([{ id: extensionId }], CancellationToken.None))[0]; - - await this.extensionManagementService.installFromGallery(galleryExtension, { - isMachineScoped: false, - donotIncludePackAndDependencies: false, - context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true } - }); + private async installExtension(extensionId: string, remoteLabel: string): Promise { + try { + await this.extensionsWorkbenchService.install(extensionId, { + isMachineScoped: false, + donotIncludePackAndDependencies: false, + context: { [EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT]: true } + }); + } catch (error) { + if (!this.lifecycleService.willShutdown) { + const { confirmed } = await this.dialogService.confirm({ + type: Severity.Error, + message: nls.localize('unknownSetupError', "An error occurred while setting up {0}. Would you like to try again?", remoteLabel), + detail: error && !isCancellationError(error) ? toErrorMessage(error) : undefined, + primaryButton: nls.localize('retry', "Retry") + }); + if (confirmed) { + return this.installExtension(extensionId, remoteLabel); + } + } + throw error; + } } private async runRemoteStartCommand(extensionId: string, startCommand: string) { @@ -787,8 +814,13 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr quickPick.busy = true; quickPick.placeholder = nls.localize('remote.startActions.installingExtension', 'Installing extension... '); - await this.installExtension(remoteExtension.id); - quickPick.hide(); + try { + await this.installExtension(remoteExtension.id, selectedItems[0].label); + } catch (error) { + return; + } finally { + quickPick.hide(); + } await this.runRemoteStartCommand(remoteExtension.id, remoteExtension.startCommand); } else { diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index f7c3020d..fb3edaaa 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -26,6 +26,11 @@ import { PersistentConnection } from '../../../../platform/remote/common/remoteA import { IDownloadService } from '../../../../platform/download/common/download.js'; import { DownloadServiceChannel } from '../../../../platform/download/common/downloadIpc.js'; import { RemoteLoggerChannelClient } from '../../../../platform/log/common/logIpc.js'; +import { REMOTE_DEFAULT_IF_LOCAL_EXTENSIONS } from '../../../../platform/remote/common/remote.js'; +import product from '../../../../platform/product/common/product.js'; + + +const EXTENSION_IDENTIFIER_PATTERN = '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$'; export class LabelContribution implements IWorkbenchContribution { @@ -205,7 +210,7 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'object', markdownDescription: localize('remote.extensionKind', "Override the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote. By overriding an extension's default kind using this setting, you specify if that extension should be installed and enabled locally or remotely."), patternProperties: { - '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$': { + [EXTENSION_IDENTIFIER_PATTERN]: { oneOf: [{ type: 'array', items: extensionKindSchema }, extensionKindSchema], default: ['ui'], }, @@ -355,6 +360,16 @@ Registry.as(ConfigurationExtensions.Configuration) enum: ['localhost', 'allInterfaces'], default: 'localhost', description: localize('remote.localPortHost', "Specifies the local host name that will be used for port forwarding.") + }, + [REMOTE_DEFAULT_IF_LOCAL_EXTENSIONS]: { + type: 'array', + markdownDescription: localize('remote.defaultExtensionsIfInstalledLocally.markdownDescription', 'List of extensions to install upon connection to a remote when already installed locally.'), + default: product?.remoteDefaultExtensionsIfInstalledLocally || [], + items: { + type: 'string', + pattern: EXTENSION_IDENTIFIER_PATTERN, + patternErrorMessage: localize('remote.defaultExtensionsIfInstalledLocally.invalidFormat', 'Extension identifier must be in format "publisher.name".') + }, } } }); diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index adda8fa2..e2abe63d 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -7,7 +7,7 @@ import { localize } from '../../../../nls.js'; import { basename } from '../../../../base/common/resources.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from '../common/scm.js'; +import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService, ISCMProvider } from '../common/scm.js'; import { IActivityService, NumberBadge } from '../../../services/activity/common/activity.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; @@ -30,49 +30,11 @@ const ActiveRepositoryContextKeys = { }; export class SCMActiveRepositoryController extends Disposable implements IWorkbenchContribution { - private readonly _countBadgeConfig = observableConfigValue<'all' | 'focused' | 'off'>('scm.countBadge', 'all', this.configurationService); - - private readonly _repositories = observableFromEvent(this, - Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), - () => this.scmService.repositories); - - private readonly _activeRepositoryHistoryItemRefName = derived(reader => { - const repository = this.scmViewService.activeRepository.read(reader); - const historyProvider = repository?.provider.historyProvider.read(reader); - const historyItemRef = historyProvider?.historyItemRef.read(reader); - - return historyItemRef?.name; - }); - - private readonly _countBadgeRepositories = derived(this, reader => { - switch (this._countBadgeConfig.read(reader)) { - case 'all': { - const repositories = this._repositories.read(reader); - return [...Iterable.map(repositories, r => ({ provider: r.provider, resourceCount: this._getRepositoryResourceCount(r) }))]; - } - case 'focused': { - const repository = this.scmViewService.activeRepository.read(reader); - return repository ? [{ provider: repository.provider, resourceCount: this._getRepositoryResourceCount(repository) }] : []; - } - case 'off': - return []; - default: - throw new Error('Invalid countBadge setting'); - } - }); - - private readonly _countBadge = derived(this, reader => { - let total = 0; - - for (const repository of this._countBadgeRepositories.read(reader)) { - const count = repository.provider.count?.read(reader); - const resourceCount = repository.resourceCount.read(reader); - - total = total + (count ?? resourceCount); - } - - return total; - }); + private readonly _repositories: IObservable>; + private readonly _activeRepositoryHistoryItemRefName: IObservable; + private readonly _countBadgeConfig: IObservable<'all' | 'focused' | 'off'>; + private readonly _countBadgeRepositories: IObservable }[]>; + private readonly _countBadge: IObservable; private _activeRepositoryNameContextKey: IContextKey; private _activeRepositoryBranchNameContextKey: IContextKey; @@ -96,11 +58,57 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe { name: 'activeRepositoryBranchName', contextKey: ActiveRepositoryContextKeys.ActiveRepositoryBranchName.key, } ]); + this._countBadgeConfig = observableConfigValue<'all' | 'focused' | 'off'>('scm.countBadge', 'all', this.configurationService); + + this._repositories = observableFromEvent(this, + Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), + () => this.scmService.repositories); + + this._activeRepositoryHistoryItemRefName = derived(reader => { + const repository = this.scmViewService.activeRepository.read(reader); + const historyProvider = repository?.provider.historyProvider.read(reader); + const historyItemRef = historyProvider?.historyItemRef.read(reader); + + return historyItemRef?.name; + }); + + this._countBadgeRepositories = derived(this, reader => { + switch (this._countBadgeConfig.read(reader)) { + case 'all': { + const repositories = this._repositories.read(reader); + return [...Iterable.map(repositories, r => ({ provider: r.provider, resourceCount: this._getRepositoryResourceCount(r) }))]; + } + case 'focused': { + const repository = this.scmViewService.activeRepository.read(reader); + return repository ? [{ provider: repository.provider, resourceCount: this._getRepositoryResourceCount(repository) }] : []; + } + case 'off': + return []; + default: + throw new Error('Invalid countBadge setting'); + } + }); + + this._countBadge = derived(this, reader => { + let total = 0; + + for (const repository of this._countBadgeRepositories.read(reader)) { + const count = repository.provider.count?.read(reader); + const resourceCount = repository.resourceCount.read(reader); + + total = total + (count ?? resourceCount); + } + + return total; + }); + this._register(autorunWithStore((reader, store) => { - this._updateActivityCountBadge(this._countBadge.read(reader), store); + const countBadge = this._countBadge.read(reader); + this._updateActivityCountBadge(countBadge, store); })); this._register(autorunWithStore((reader, store) => { + this._repositories.read(reader); const repository = this.scmViewService.activeRepository.read(reader); const commands = repository?.provider.statusBarCommands.read(reader); @@ -168,6 +176,19 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe this.statusbarService.addEntry(statusbarEntry, `status.scm.${index}`, MainThreadStatusBarAlignment.LEFT, { location: { id: `status.scm.${index - 1}`, priority: 10000 }, alignment: MainThreadStatusBarAlignment.RIGHT, compact: true }) ); } + + // Ssource control provider status bar entry + if (this.scmService.repositoryCount > 1) { + const repositoryStatusbarEntry: IStatusbarEntry = { + name: localize('status.scm.provider', "Source Control Provider"), + text: `$(repo) ${repository.provider.name}`, + ariaLabel: label, + tooltip: label, + command: 'scm.setActiveProvider' + }; + + store.add(this.statusbarService.addEntry(repositoryStatusbarEntry, 'status.scm.provider', MainThreadStatusBarAlignment.LEFT, { location: { id: `status.scm.0`, priority: 10000 }, alignment: MainThreadStatusBarAlignment.LEFT, compact: true })); + } } private _updateActiveRepositoryContextKeys(repositoryName: string | undefined, branchName: string | undefined): void { @@ -177,9 +198,7 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe } export class SCMActiveResourceContextKeyController extends Disposable implements IWorkbenchContribution { - private readonly _repositories = observableFromEvent(this, - Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), - () => this.scmService.repositories); + private readonly _repositories: IObservable>; private readonly _onDidRepositoryChange = new Emitter(); @@ -193,6 +212,10 @@ export class SCMActiveResourceContextKeyController extends Disposable implements const activeResourceHasChangesContextKey = new RawContextKey('scmActiveResourceHasChanges', false, localize('scmActiveResourceHasChanges', "Whether the active resource has changes")); const activeResourceRepositoryContextKey = new RawContextKey('scmActiveResourceRepository', undefined, localize('scmActiveResourceRepository', "The active resource's repository")); + this._repositories = observableFromEvent(this, + Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), + () => this.scmService.repositories); + this._store.add(autorunWithStore((reader, store) => { for (const repository of this._repositories.read(reader)) { store.add(Event.runAndSubscribe(repository.provider.onDidChangeResources, () => { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index a38b3742..6faa64d0 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -650,3 +650,7 @@ border-radius: 2px; opacity: 0.5; } + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item .codicon.codicon-repo { + padding-top: 1px; +} diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts index 01c9eb21..ac52c163 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffDecorator.ts @@ -22,7 +22,8 @@ import { IWorkbenchContribution } from '../../../common/contributions.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { autorun, autorunWithStore, observableFromEvent } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; export const quickDiffDecorationCount = new RawContextKey('quickDiffDecorationCount', 0); @@ -188,8 +189,7 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben private enabled = false; private readonly quickDiffDecorationCount: IContextKey; - private readonly activeEditor = observableFromEvent(this, - this.editorService.onDidActiveEditorChange, () => this.editorService.activeEditor); + private readonly activeEditor: IObservable; // Resource URI -> Code Editor Id -> Decoration (Disposable) private readonly decorators = new ResourceMap>(); @@ -209,6 +209,9 @@ export class QuickDiffWorkbenchController extends Disposable implements IWorkben this.quickDiffDecorationCount = quickDiffDecorationCount.bindTo(contextKeyService); + this.activeEditor = observableFromEvent(this, + this.editorService.onDidActiveEditorChange, () => this.editorService.activeEditor); + const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorations')); this._register(onDidChangeConfiguration(this.onDidChangeConfiguration, this)); this.onDidChangeConfiguration(); diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index de01355f..f22648c7 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -172,7 +172,7 @@ class QuickDiffWidget extends PeekViewWidget { ) { super(editor, { isResizeable: true, frameWidth: 1, keepEditorSelection: true, className: 'dirty-diff' }, instantiationService); - this._disposables.add(themeService.onDidColorThemeChange(e => this._applyTheme(e.theme))); + this._disposables.add(themeService.onDidColorThemeChange(this._applyTheme, this)); this._applyTheme(themeService.getColorTheme()); if (!Iterable.isEmpty(this.model.originalTextModels)) { diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 2995f5cc..a3ce54d9 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -25,9 +25,9 @@ import { ModesRegistry } from '../../../../editor/common/languages/modesRegistry import { Codicon } from '../../../../base/common/codicons.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { ContextKeys, SCMViewPane } from './scmViewPane.js'; -import { SCMViewService } from './scmViewService.js'; +import { RepositoryPicker, SCMViewService } from './scmViewService.js'; import { SCMRepositoriesViewPane } from './scmRepositoriesViewPane.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { Context as SuggestContext } from '../../../../editor/contrib/suggest/browser/suggest.js'; import { MANAGE_TRUST_COMMAND_ID, WorkspaceTrustContext } from '../../workspace/common/workspace.js'; import { IQuickDiffService } from '../common/quickDiff.js'; @@ -534,6 +534,21 @@ CommandsRegistry.registerCommand('scm.openInTerminal', async (accessor, provider await commandService.executeCommand('openInTerminal', provider.rootUri); }); +CommandsRegistry.registerCommand('scm.setActiveProvider', async (accessor) => { + const instantiationService = accessor.get(IInstantiationService); + const scmViewService = accessor.get(ISCMViewService); + + const placeHolder = localize('scmActiveRepositoryPlaceHolder', "Select the active repository, type to filter all repositories"); + const autoQuickItemDescription = localize('scmActiveRepositoryAutoDescription', "The active repository is updated based on focused repository/active editor"); + const repositoryPicker = instantiationService.createInstance(RepositoryPicker, placeHolder, autoQuickItemDescription); + + const result = await repositoryPicker.pickRepository(); + if (result?.repository) { + const repository = result.repository !== 'auto' ? result.repository : undefined; + scmViewService.pinActiveRepository(repository); + } +}); + MenuRegistry.appendMenuItem(MenuId.SCMSourceControl, { group: '100_end', command: { diff --git a/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts b/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts index 3126416f..18908231 100644 --- a/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/scm/browser/scmAccessibilityHelp.ts @@ -76,23 +76,23 @@ class SCMAccessibilityHelpContentProvider extends Disposable implements IAccessi content.push(localize('state-msg1', "Visible repositories: {0}", repositoryList)); } - const activeRepository = this._scmViewService.activeRepository.get(); - if (activeRepository) { - content.push(localize('state-msg2', "Repository: {0}", activeRepository.provider.name)); + const focusedRepository = this._scmViewService.focusedRepository; + if (focusedRepository) { + content.push(localize('state-msg2', "Repository: {0}", focusedRepository.provider.name)); // History Item Reference - const currentHistoryItemRef = activeRepository.provider.historyProvider.get()?.historyItemRef.get(); + const currentHistoryItemRef = focusedRepository.provider.historyProvider.get()?.historyItemRef.get(); if (currentHistoryItemRef) { content.push(localize('state-msg3', "History item reference: {0}", currentHistoryItemRef.name)); } // Commit Message - if (activeRepository.input.visible && activeRepository.input.enabled && activeRepository.input.value !== '') { - content.push(localize('state-msg4', "Commit message: {0}", activeRepository.input.value)); + if (focusedRepository.input.visible && focusedRepository.input.enabled && focusedRepository.input.value !== '') { + content.push(localize('state-msg4', "Commit message: {0}", focusedRepository.input.value)); } // Action Button - const actionButton = activeRepository.provider.actionButton.get(); + const actionButton = focusedRepository.provider.actionButton.get(); if (actionButton) { const label = actionButton.command.tooltip ?? actionButton.command.title; const enablementLabel = actionButton.enabled ? localize('enabled', "enabled") : localize('disabled', "disabled"); @@ -101,11 +101,11 @@ class SCMAccessibilityHelpContentProvider extends Disposable implements IAccessi // Resource Groups const resourceGroups: string[] = []; - for (const resourceGroup of activeRepository.provider.groups) { + for (const resourceGroup of focusedRepository.provider.groups) { resourceGroups.push(`${resourceGroup.label} (${resourceGroup.resources.length} resource(s))`); } - activeRepository.provider.groups.map(g => g.label).join(', '); + focusedRepository.provider.groups.map(g => g.label).join(', '); content.push(localize('state-msg6', "Resource groups: {0}", resourceGroups.join(', '))); } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 64d04fc0..fdde018b 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -318,7 +318,7 @@ class HistoryItemRenderer implements ITreeRenderer('scm.graph.badges', 'filter', this._configurationService); + private readonly _badgesConfig: IObservable<'all' | 'filter'>; constructor( private readonly hoverDelegate: IHoverDelegate, @@ -328,7 +328,9 @@ class HistoryItemRenderer implements ITreeRenderer('scm.graph.badges', 'filter', this._configurationService); + } renderTemplate(container: HTMLElement): HistoryItemTemplate { // hack @@ -749,36 +751,13 @@ type RepositoryState = { }; class SCMHistoryViewModel extends Disposable { - - private readonly _closedRepository = observableFromEvent( - this, - this._scmService.onDidRemoveRepository, - repository => repository); - - private readonly _firstRepository = this._scmService.repositoryCount > 0 ? - constObservable(Iterable.first(this._scmService.repositories)) : - observableFromEvent( - this, - Event.once(this._scmService.onDidAddRepository), - repository => repository - ); - - private readonly _selectedRepository = observableValue<'auto' | ISCMRepository>(this, 'auto'); - - private readonly _graphRepository = derived(reader => { - const selectedRepository = this._selectedRepository.read(reader); - if (selectedRepository !== 'auto') { - return selectedRepository; - } - - return this._scmViewService.activeRepository.read(reader); - }); - /** * The active | selected repository takes precedence over the first repository when the observable * values are updated in the same transaction (or during the initial read of the observable value). */ - readonly repository = latestChangedValue(this, [this._firstRepository, this._graphRepository]); + readonly repository: IObservable; + private readonly _selectedRepository = observableValue<'auto' | ISCMRepository>(this, 'auto'); + readonly onDidChangeHistoryItemsFilter = observableSignal(this); readonly isViewModelEmpty = observableValue(this, false); @@ -804,9 +783,30 @@ class SCMHistoryViewModel extends Disposable { this._scmHistoryItemCountCtx = ContextKeys.SCMHistoryItemCount.bindTo(this._contextKeyService); + const firstRepository = this._scmService.repositoryCount > 0 + ? constObservable(Iterable.first(this._scmService.repositories)) + : observableFromEvent(this, + Event.once(this._scmService.onDidAddRepository), + repository => repository); + + const graphRepository = derived(reader => { + const selectedRepository = this._selectedRepository.read(reader); + if (selectedRepository !== 'auto') { + return selectedRepository; + } + + return this._scmViewService.activeRepository.read(reader); + }); + + this.repository = latestChangedValue(this, [firstRepository, graphRepository]); + + const closedRepository = observableFromEvent(this, + this._scmService.onDidRemoveRepository, + repository => repository); + // Closed repository cleanup this._register(autorun(reader => { - const repository = this._closedRepository.read(reader); + const repository = closedRepository.read(reader); if (!repository) { return; } @@ -1044,7 +1044,7 @@ class SCMHistoryViewModel extends Disposable { type RepositoryQuickPickItem = IQuickPickItem & { repository: 'auto' | ISCMRepository }; -class RepositoryPicker extends Disposable { +class RepositoryPicker { private readonly _autoQuickPickItem: RepositoryQuickPickItem = { label: localize('auto', "Auto"), description: localize('activeRepository', "Show the source control graph for the active repository"), @@ -1054,9 +1054,7 @@ class RepositoryPicker extends Disposable { constructor( @IQuickInputService private readonly _quickInputService: IQuickInputService, @ISCMViewService private readonly _scmViewService: ISCMViewService - ) { - super(); - } + ) { } async pickRepository(): Promise { const picks: (RepositoryQuickPickItem | IQuickPickSeparator)[] = [ diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index 254f02be..755aed69 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -92,6 +92,7 @@ export class SCMRepositoriesViewPane extends ViewPane { this._register(this.list); this._register(this.list.onDidChangeSelection(this.onListSelectionChange, this)); + this._register(this.list.onDidChangeFocus(this.onDidChangeFocus, this)); this._register(this.list.onContextMenu(this.onListContextMenu, this)); this._register(this.scmViewService.onDidChangeRepositories(this.onDidChangeRepositories, this)); @@ -169,6 +170,12 @@ export class SCMRepositoriesViewPane extends ViewPane { } } + private onDidChangeFocus(e: IListEvent): void { + if (e.browserEvent && e.elements.length > 0) { + this.scmViewService.focus(e.elements[0]); + } + } + private updateListSelection(): void { const oldSelection = this.list.getSelection(); const oldSet = new Set(Iterable.map(oldSelection, i => this.list.element(i))); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 013eeeac..83f8a154 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1498,6 +1498,8 @@ class SCMInputWidgetEditorOptions { e => { return e.affectsConfiguration('editor.accessibilitySupport') || e.affectsConfiguration('editor.cursorBlinking') || + e.affectsConfiguration('editor.cursorStyle') || + e.affectsConfiguration('editor.cursorWidth') || e.affectsConfiguration('editor.emptySelectionClipboard') || e.affectsConfiguration('editor.fontFamily') || e.affectsConfiguration('editor.rulers') || @@ -1515,7 +1517,6 @@ class SCMInputWidgetEditorOptions { return { ...getSimpleEditorOptions(this.configurationService), ...this.getEditorOptions(), - cursorWidth: 1, dragAndDrop: true, dropIntoEditor: { enabled: true }, formatOnType: true, @@ -1539,9 +1540,11 @@ class SCMInputWidgetEditorOptions { const lineHeight = this._getEditorLineHeight(fontSize); const accessibilitySupport = this.configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'); const cursorBlinking = this.configurationService.getValue<'blink' | 'smooth' | 'phase' | 'expand' | 'solid'>('editor.cursorBlinking'); + const cursorStyle = this.configurationService.getValue('editor.cursorStyle'); + const cursorWidth = this.configurationService.getValue('editor.cursorWidth') ?? 1; const emptySelectionClipboard = this.configurationService.getValue('editor.emptySelectionClipboard') === true; - return { ...this._getEditorLanguageConfiguration(), accessibilitySupport, cursorBlinking, fontFamily, fontSize, lineHeight, emptySelectionClipboard }; + return { ...this._getEditorLanguageConfiguration(), accessibilitySupport, cursorBlinking, cursorStyle, cursorWidth, fontFamily, fontSize, lineHeight, emptySelectionClipboard }; } private _getEditorFontFamily(): string { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 52af3ec9..e5c16a6e 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -18,9 +18,14 @@ import { binarySearch } from '../../../../base/common/arrays.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { derivedObservableWithCache, derivedOpts, latestChangedValue, observableFromEventOpts } from '../../../../base/common/observable.js'; +import { derivedObservableWithCache, derivedOpts, IObservable, ISettableObservable, latestChangedValue, observableFromEventOpts, observableValue } from '../../../../base/common/observable.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { localize } from '../../../../nls.js'; function getProviderStorageKey(provider: ISCMProvider): string { return `${provider.contextValue}:${provider.label}${provider.rootUri ? `:${provider.rootUri.toString()}` : ''}`; @@ -39,6 +44,41 @@ export const RepositoryContextKeys = { RepositorySortKey: new RawContextKey('scmRepositorySortKey', ISCMRepositorySortKey.DiscoveryTime), }; +export type RepositoryQuickPickItem = IQuickPickItem & { repository: 'auto' | ISCMRepository }; + +export class RepositoryPicker { + private readonly _autoQuickPickItem: RepositoryQuickPickItem; + + constructor( + private readonly _placeHolder: string, + private readonly _autoQuickItemDescription: string, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @ISCMViewService private readonly _scmViewService: ISCMViewService + ) { + this._autoQuickPickItem = { + label: localize('auto', "Auto"), + description: this._autoQuickItemDescription, + repository: 'auto' + } satisfies RepositoryQuickPickItem; + } + + async pickRepository(): Promise { + const picks: (RepositoryQuickPickItem | IQuickPickSeparator)[] = [ + this._autoQuickPickItem, + { type: 'separator' } + ]; + + picks.push(...this._scmViewService.repositories.map(r => ({ + label: r.provider.name, + description: r.provider.rootUri?.fsPath, + iconClass: ThemeIcon.asClassName(Codicon.repo), + repository: r + }))); + + return this._quickInputService.pick(picks, { placeHolder: this._placeHolder }); + } +} + interface ISCMRepositoryView { readonly repository: ISCMRepository; readonly discoveryTime: number; @@ -157,49 +197,17 @@ export class SCMViewService implements ISCMViewService { private _onDidFocusRepository = new Emitter(); readonly onDidFocusRepository = this._onDidFocusRepository.event; - private readonly _focusedRepository = observableFromEventOpts( - { - owner: this, - equalsFn: () => false - }, this.onDidFocusRepository, - () => this.focusedRepository); - - private readonly _activeEditor = observableFromEventOpts( - { - owner: this, - equalsFn: () => false - }, this.editorService.onDidActiveEditorChange, - () => this.editorService.activeEditor); - - private readonly _activeEditorRepository = derivedObservableWithCache(this, - (reader, lastValue) => { - const activeEditor = this._activeEditor.read(reader); - const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor); - if (!activeResource) { - return lastValue; - } - - const repository = this.scmService.getRepository(activeResource); - if (!repository) { - return lastValue; - } - - return Object.create(repository); - }); + readonly activeRepository: IObservable; + private readonly _activeEditorObs: IObservable; + private readonly _activeEditorRepositoryObs: IObservable; /** * The focused repository takes precedence over the active editor repository when the observable * values are updated in the same transaction (or during the initial read of the observable value). - */ - private readonly _activeRepository = latestChangedValue(this, [this._activeEditorRepository, this._focusedRepository]); - - /** - * Derived with a custom equality function - */ - readonly activeRepository = derivedOpts({ - owner: this, - equalsFn: (r1, r2) => r1?.id === r2?.id - }, reader => this._activeRepository.read(reader)); + */ + private readonly _activeRepositoryObs: IObservable; + private readonly _activeRepositoryPinnedObs: ISettableObservable; + private readonly _focusedRepositoryObs: IObservable; private _repositoriesSortKey: ISCMRepositorySortKey; private _sortKeyContextKey: IContextKey; @@ -216,6 +224,49 @@ export class SCMViewService implements ISCMViewService { ) { this.menus = instantiationService.createInstance(SCMMenus); + this._focusedRepositoryObs = observableFromEventOpts( + { + owner: this, + equalsFn: () => false + }, this.onDidFocusRepository, + () => this.focusedRepository); + + this._activeEditorObs = observableFromEventOpts( + { + owner: this, + equalsFn: () => false + }, this.editorService.onDidActiveEditorChange, + () => this.editorService.activeEditor); + + this._activeEditorRepositoryObs = derivedObservableWithCache(this, + (reader, lastValue) => { + const activeEditor = this._activeEditorObs.read(reader); + const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor); + if (!activeResource) { + return lastValue; + } + + const repository = this.scmService.getRepository(activeResource); + if (!repository) { + return lastValue; + } + + return Object.create(repository); + }); + + this._activeRepositoryPinnedObs = observableValue(this, undefined); + this._activeRepositoryObs = latestChangedValue(this, [this._activeEditorRepositoryObs, this._focusedRepositoryObs]); + + this.activeRepository = derivedOpts({ + owner: this, + equalsFn: (r1, r2) => r1?.id === r2?.id + }, reader => { + const activeRepository = this._activeRepositoryObs.read(reader); + const activeRepositoryPinned = this._activeRepositoryPinnedObs.read(reader); + + return activeRepositoryPinned ?? activeRepository; + }); + try { this.previousState = JSON.parse(storageService.get('scm:view:visibleRepositories', StorageScope.WORKSPACE, '')); } catch { @@ -379,6 +430,10 @@ export class SCMViewService implements ISCMViewService { } } + pinActiveRepository(repository: ISCMRepository | undefined): void { + this._activeRepositoryPinnedObs.set(repository, undefined); + } + private compareRepositories(op1: ISCMRepositoryView, op2: ISCMRepositoryView): number { // Sort by discovery time if (this._repositoriesSortKey === ISCMRepositorySortKey.DiscoveryTime) { diff --git a/src/vs/workbench/contrib/scm/browser/workingSet.ts b/src/vs/workbench/contrib/scm/browser/workingSet.ts index 1185f8de..1f53f8d2 100644 --- a/src/vs/workbench/contrib/scm/browser/workingSet.ts +++ b/src/vs/workbench/contrib/scm/browser/workingSet.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, derived } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, derived, IObservable } from '../../../../base/common/observable.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; @@ -28,8 +28,8 @@ interface ISCMRepositoryWorkingSet { export class SCMWorkingSetController extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.scmWorkingSets'; + private _enabledConfig: IObservable; private _workingSets!: Map; - private _enabledConfig = observableConfigValue('scm.workingSets.enabled', false, this.configurationService); private readonly _repositoryDisposables = new DisposableMap(); @@ -42,6 +42,8 @@ export class SCMWorkingSetController extends Disposable implements IWorkbenchCon ) { super(); + this._enabledConfig = observableConfigValue('scm.workingSets.enabled', false, this.configurationService); + this._store.add(autorunWithStore((reader, store) => { if (!this._enabledConfig.read(reader)) { this.storageService.remove('scm.workingSets', StorageScope.WORKSPACE); diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index 55cd6956..60dd34b3 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -14,7 +14,10 @@ import { LineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js import { IChange } from '../../../../editor/common/diff/legacyLinesDiffComputer.js'; import { IColorTheme } from '../../../../platform/theme/common/themeService.js'; import { Color } from '../../../../base/common/color.js'; -import { editorErrorForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; +import { + darken, editorBackground, editorForeground, listInactiveSelectionBackground, opaque, + editorErrorForeground, registerColor, transparent +} from '../../../../platform/theme/common/colorRegistry.js'; export const IQuickDiffService = createDecorator('quickDiff'); @@ -45,6 +48,12 @@ export const overviewRulerAddedForeground = registerColor('editorOverviewRuler.a export const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', transparent(editorGutterDeletedBackground, 0.6), nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.')); +export const editorGutterItemGlyphForeground = registerColor('editorGutter.itemGlyphForeground', + { dark: editorForeground, light: editorForeground, hcDark: Color.black, hcLight: Color.white }, + nls.localize('editorGutterItemGlyphForeground', 'Editor gutter decoration color for gutter item glyphs.') +); +export const editorGutterItemBackground = registerColor('editorGutter.itemBackground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterItemBackground', 'Editor gutter decoration color for gutter item background. This color should be opaque.')); + export interface QuickDiffProvider { label: string; rootUri: URI | undefined; diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index d04c2b09..1987fedd 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -233,6 +233,7 @@ export interface ISCMViewService { * Focused repository or the repository for the active editor */ readonly activeRepository: IObservable; + pinActiveRepository(repository: ISCMRepository | undefined): void; } export const SCM_CHANGES_EDITOR_ID = 'workbench.editor.scmChangesEditor'; diff --git a/src/vs/workbench/contrib/search/browser/AISearch/aiSearchModel.ts b/src/vs/workbench/contrib/search/browser/AISearch/aiSearchModel.ts index df9d8a5d..c630a138 100644 --- a/src/vs/workbench/contrib/search/browser/AISearch/aiSearchModel.ts +++ b/src/vs/workbench/contrib/search/browser/AISearch/aiSearchModel.ts @@ -23,14 +23,18 @@ import { TextSearchHeadingImpl } from '../searchTreeModel/textSearchHeading.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { textSearchResultToMatches } from '../searchTreeModel/match.js'; import { ISearchTreeAIFileMatch } from './aiSearchModelBase.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; export class AITextSearchHeadingImpl extends TextSearchHeadingImpl { + public override hidden: boolean; constructor( parent: ISearchResult, @IInstantiationService instantiationService: IInstantiationService, @IUriIdentityService uriIdentityService: IUriIdentityService ) { super(false, parent, instantiationService, uriIdentityService); + + this.hidden = true; } override name(): string { @@ -64,6 +68,20 @@ export class AITextSearchHeadingImpl extends TextSearchHeadingImpl this._query = query; } + override fileCount(): number { + const uniqueFileUris = new ResourceSet(); + for (const folderMatch of this.folderMatches()) { + if (folderMatch.isEmpty()) { + continue; + } + for (const fileMatch of folderMatch.allDownstreamFileMatches()) { + uniqueFileUris.add(fileMatch.resource); + } + } + + return uniqueFileUris.size; + } + private _createBaseFolderMatch(resource: URI, id: string, index: number, query: IAITextQuery): ISearchTreeFolderMatch { const folderMatch: ISearchTreeFolderMatch = this._register(this.createWorkspaceRootWithResourceImpl(resource, id, index, query)); const disposable = folderMatch.onChange((event) => this._onChange.fire(event)); diff --git a/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts b/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts index ab63b6ec..ab06c9be 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts @@ -99,6 +99,35 @@ registerAction2(class CopyAllCommandAction extends Action2 { } }); +registerAction2(class GetSearchResultsAction extends Action2 { + constructor() { + super({ + id: Constants.SearchCommandIds.GetSearchResultsActionId, + title: nls.localize2('getSearchResultsLabel', "Get Search Results"), + category, + f1: false + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const viewsService = accessor.get(IViewsService); + const labelService = accessor.get(ILabelService); + + const searchView = getSearchView(viewsService); + if (searchView) { + const root = searchView.searchResult; + const textSearchResult = allFolderMatchesToString(root.folderMatches(), labelService); + const aiSearchResult = allFolderMatchesToString(root.folderMatches(true), labelService); + + const text = `${textSearchResult}${lineDelimiter}${lineDelimiter}${aiSearchResult}`; + + return text; + } + + return undefined; + } +}); + //#endregion //#region Helpers diff --git a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts index 0776c52e..7a0f2a21 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts @@ -15,7 +15,7 @@ import { VIEW_ID } from '../../../services/search/common/search.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { SearchStateKey, SearchUIState } from '../common/search.js'; import { category, getSearchView } from './searchActionsBase.js'; import { isSearchTreeMatch, RenderableMatch, ISearchResult, isSearchTreeFolderMatch, isSearchTreeFolderMatchNoRoot, isSearchTreeFolderMatchWorkspaceRoot, isSearchResult, isTextSearchHeading, isSearchTreeFileMatch } from './searchTreeModel/searchTreeCommon.js'; @@ -205,6 +205,34 @@ registerAction2(class ViewAsListAction extends Action2 { } }); +registerAction2(class SearchWithAIAction extends Action2 { + constructor() { + super({ + id: Constants.SearchCommandIds.SearchWithAIActionId, + title: nls.localize2('SearchWithAIAction.label', "Search with AI"), + category, + f1: true, + precondition: Constants.SearchContext.hasAIResultProvider, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchContext.hasAIResultProvider, Constants.SearchContext.SearchViewFocusedKey), + primary: KeyMod.CtrlCmd | KeyCode.KeyI + } + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + const viewer = searchView.getControl(); + searchView.model.searchResult.aiTextSearchResult.hidden = false; + searchView.model.cancelAISearch(true); + searchView.model.clearAiSearchResults(); + await searchView.queueRefreshTree(); + await forcedExpandRecursively(viewer, searchView.model.searchResult.aiTextSearchResult); + } + } +}); //#endregion @@ -274,7 +302,7 @@ function cancelSearch(accessor: ServicesAccessor) { function refreshSearch(accessor: ServicesAccessor) { const viewsService = accessor.get(IViewsService); const searchView = getSearchView(viewsService); - searchView?.triggerQueryChange({ preserveFocus: false }); + searchView?.triggerQueryChange({ preserveFocus: false, shouldUpdateAISearch: !searchView.model.searchResult.aiTextSearchResult.hidden }); } function collapseDeepestExpandedLevel(accessor: ServicesAccessor) { diff --git a/src/vs/workbench/contrib/search/browser/searchMessage.ts b/src/vs/workbench/contrib/search/browser/searchMessage.ts index 37a03f28..ea8990aa 100644 --- a/src/vs/workbench/contrib/search/browser/searchMessage.ts +++ b/src/vs/workbench/contrib/search/browser/searchMessage.ts @@ -10,7 +10,7 @@ import { parseLinkedText } from '../../../../base/common/linkedText.js'; import Severity from '../../../../base/common/severity.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js'; import { TextSearchCompleteMessage, TextSearchCompleteMessageType } from '../../../services/search/common/searchExtTypes.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { Schemas } from '../../../../base/common/network.js'; diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 55c599ca..f94b4227 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -144,7 +144,13 @@ export class TextSearchResultRenderer extends Disposable implements ICompressibl SearchContext.FileFocusKey.bindTo(templateData.contextKeyService).set(false); SearchContext.FolderFocusKey.bindTo(templateData.contextKeyService).set(false); } else { - const aiName = await node.element.parent().searchModel.getAITextResultProviderName(); + let aiName = 'Copilot'; + try { + aiName = (await node.element.parent().searchModel.getAITextResultProviderName()) || 'Copilot'; + } catch { + // ignore + } + const localizedLabel = nls.localize({ key: 'searchFolderMatch.aiText.label', comment: ['This is displayed before the AI text search results, where {0} will be in the place of the AI name (ie: Copilot)'] diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchModel.ts index 104a7ef8..df9c61ce 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchModel.ts @@ -398,6 +398,11 @@ export class SearchModelImpl extends Disposable implements ISearchModel { } return false; } + clearAiSearchResults(): void { + this._aiResultQueue.length = 0; + // it's not clear all as we are only clearing the AI results + this._searchResult.aiTextSearchResult.clear(false); + } override dispose(): void { this.cancelSearch(); this.cancelAISearch(); diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts index 6bdb676f..f2e0fa7e 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts @@ -197,7 +197,9 @@ export class SearchResultImpl extends Disposable implements ISearchResult { add(allRaw: IFileMatch[], searchInstanceID: string, ai: boolean, silent: boolean = false): void { this._plainTextSearchResult.hidden = false; - this._aiTextSearchResult.hidden = false; + if (ai) { + this._aiTextSearchResult.hidden = false; + } if (ai) { this._aiTextSearchResult.add(allRaw, searchInstanceID, silent); diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchTreeCommon.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchTreeCommon.ts index d473600c..bb05360e 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchTreeCommon.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchTreeCommon.ts @@ -107,6 +107,7 @@ export interface ISearchModel { }; cancelSearch(cancelledForNewSearch?: boolean): boolean; cancelAISearch(cancelledForNewSearch?: boolean): boolean; + clearAiSearchResults(): void; dispose(): void; } @@ -168,7 +169,7 @@ export interface ITextSearchHeading { rangeHighlightDecorations: RangeHighlightDecorations; fileCount(): number; count(): number; - clear(): void; + clear(clearAll: boolean): void; dispose(): void; } @@ -335,6 +336,13 @@ export function isSearchTreeMatch(obj: any): obj is ISearchTreeMatch { obj.id().startsWith(MATCH_PREFIX); } +export function isSearchHeader(obj: any): boolean { + return typeof obj === 'object' && + obj !== null && + typeof obj.id === 'function' && + obj.id().startsWith(TEXT_SEARCH_HEADING_PREFIX); +} + export function getFileMatches(matches: (ISearchTreeFileMatch | ISearchTreeFolderMatchWithResource)[]): ISearchTreeFileMatch[] { const folderMatches: ISearchTreeFolderMatchWithResource[] = []; diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/textSearchHeading.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/textSearchHeading.ts index ce7f9670..c301606b 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/textSearchHeading.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/textSearchHeading.ts @@ -249,9 +249,9 @@ export abstract class TextSearchHeadingImpl return this.matches().reduce((prev, match) => prev + match.count(), 0); } - clear(): void { + clear(clearAll: boolean = true): void { this.cachedSearchComplete = undefined; - this.folderMatches().forEach((folderMatch) => folderMatch.clear(true)); + this.folderMatches().forEach((folderMatch) => folderMatch.clear(clearAll)); this.disposeMatches(); this._folderMatches = []; this._otherFilesMatch = null; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index e46972d4..2a7f4d3c 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -71,7 +71,7 @@ import { createEditorFromSearchResult } from '../../searchEditor/browser/searchE import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { IPreferencesService, ISettingsEditorOptions } from '../../../services/preferences/common/preferences.js'; import { ITextQueryBuilderOptions, QueryBuilder } from '../../../services/search/common/queryBuilder.js'; -import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from '../../../services/search/common/search.js'; +import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ISearchService, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from '../../../services/search/common/search.js'; import { TextSearchCompleteMessage } from '../../../services/search/common/searchExtTypes.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; @@ -80,7 +80,7 @@ import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../pl import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { ISearchViewModelWorkbenchService } from './searchTreeModel/searchViewModelWorkbenchService.js'; -import { ISearchTreeMatch, isSearchTreeMatch, RenderableMatch, SearchModelLocation, IChangeEvent, FileMatchOrMatch, ISearchTreeFileMatch, ISearchTreeFolderMatch, ISearchModel, ISearchResult, isSearchTreeFileMatch, isSearchTreeFolderMatch, isSearchTreeFolderMatchNoRoot, isSearchTreeFolderMatchWithResource, isSearchTreeFolderMatchWorkspaceRoot, isSearchResult, isTextSearchHeading, ITextSearchHeading } from './searchTreeModel/searchTreeCommon.js'; +import { ISearchTreeMatch, isSearchTreeMatch, RenderableMatch, SearchModelLocation, IChangeEvent, FileMatchOrMatch, ISearchTreeFileMatch, ISearchTreeFolderMatch, ISearchModel, ISearchResult, isSearchTreeFileMatch, isSearchTreeFolderMatch, isSearchTreeFolderMatchNoRoot, isSearchTreeFolderMatchWithResource, isSearchTreeFolderMatchWorkspaceRoot, isSearchResult, isTextSearchHeading, ITextSearchHeading, isSearchHeader } from './searchTreeModel/searchTreeCommon.js'; import { INotebookFileInstanceMatch, isIMatchInNotebook } from './notebookSearch/notebookSearchModelBase.js'; import { searchMatchComparer } from './searchCompare.js'; import { AIFolderMatchWorkspaceRootImpl } from './AISearch/aiSearchModel.js'; @@ -117,6 +117,7 @@ export class SearchView extends ViewPane { private folderMatchFocused: IContextKey; private folderMatchWithResourceFocused: IContextKey; private matchFocused: IContextKey; + private searchResultHeaderFocused: IContextKey; private isEditableItem: IContextKey; private hasSearchResultsKey: IContextKey; private lastFocusState: 'input' | 'tree' = 'input'; @@ -194,6 +195,7 @@ export class SearchView extends ViewPane { @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IKeybindingService keybindingService: IKeybindingService, @IStorageService private readonly storageService: IStorageService, + @ISearchService private readonly searchService: ISearchService, @IOpenerService openerService: IOpenerService, @IHoverService hoverService: IHoverService, @INotebookService private readonly notebookService: INotebookService, @@ -214,6 +216,7 @@ export class SearchView extends ViewPane { this.fileMatchFocused = Constants.SearchContext.FileFocusKey.bindTo(this.contextKeyService); this.folderMatchFocused = Constants.SearchContext.FolderFocusKey.bindTo(this.contextKeyService); this.folderMatchWithResourceFocused = Constants.SearchContext.ResourceFolderFocusKey.bindTo(this.contextKeyService); + this.searchResultHeaderFocused = Constants.SearchContext.SearchResultHeaderFocused.bindTo(this.contextKeyService); this.hasSearchResultsKey = Constants.SearchContext.HasSearchResults.bindTo(this.contextKeyService); this.matchFocused = Constants.SearchContext.MatchFocusKey.bindTo(this.contextKeyService); this.searchStateKey = SearchStateKey.bindTo(this.contextKeyService); @@ -940,6 +943,7 @@ export class SearchView extends ViewPane { this.fileMatchOrFolderMatchFocus.set(isSearchTreeFileMatch(focus) || isSearchTreeFolderMatch(focus)); this.fileMatchOrFolderMatchWithResourceFocus.set(isSearchTreeFileMatch(focus) || isSearchTreeFolderMatchWithResource(focus)); this.folderMatchWithResourceFocused.set(isSearchTreeFolderMatchWithResource(focus)); + this.searchResultHeaderFocused.set(isSearchHeader(focus)); this.lastFocusState = 'tree'; } @@ -963,6 +967,7 @@ export class SearchView extends ViewPane { this.fileMatchOrFolderMatchFocus.reset(); this.fileMatchOrFolderMatchWithResourceFocus.reset(); this.folderMatchWithResourceFocused.reset(); + this.searchResultHeaderFocused.reset(); this.isEditableItem.reset(); })); } @@ -1310,7 +1315,7 @@ export class SearchView extends ViewPane { } cancelSearch(focus: boolean = true): boolean { - if (this.viewModel.cancelSearch()) { + if (this.viewModel.cancelSearch() && this.viewModel.cancelAISearch()) { if (focus) { this.searchWidget.focus(); } return true; } @@ -1463,7 +1468,7 @@ export class SearchView extends ViewPane { this.searchWidget.focus(false); } - triggerQueryChange(_options?: { preserveFocus?: boolean; triggeredOnType?: boolean; delay?: number; shouldKeepAIResults?: boolean }): void { + triggerQueryChange(_options?: { preserveFocus?: boolean; triggeredOnType?: boolean; delay?: number; shouldKeepAIResults?: boolean; shouldUpdateAISearch?: boolean }): void { const options = { preserveFocus: true, triggeredOnType: false, delay: 0, ..._options }; if (options.triggeredOnType && !this.searchConfig.searchOnType) { return; } @@ -1472,7 +1477,7 @@ export class SearchView extends ViewPane { const delay = options.triggeredOnType ? options.delay : 0; this.triggerQueryDelayer.trigger(() => { - this._onQueryChanged(options.preserveFocus, options.triggeredOnType, options.shouldKeepAIResults); + this._onQueryChanged(options.preserveFocus, options.triggeredOnType, options.shouldKeepAIResults, options.shouldUpdateAISearch); }, delay); } } @@ -1485,7 +1490,7 @@ export class SearchView extends ViewPane { return this.inputPatternIncludes.getValue().trim(); } - private _onQueryChanged(preserveFocus: boolean, triggeredOnType = false, shouldKeepAIResults = false): void { + private _onQueryChanged(preserveFocus: boolean, triggeredOnType = false, shouldKeepAIResults = false, shouldUpdateAISearch = false): void { if (!(this.searchWidget.searchInput?.inputBox.isInputValid())) { return; } @@ -1564,11 +1569,11 @@ export class SearchView extends ViewPane { this.validateQuery(query).then(() => { // ensure that the node is closed when a new search is triggered - if (!shouldKeepAIResults && this.tree.hasNode(this.searchResult.aiTextSearchResult)) { + if (!shouldKeepAIResults && !shouldUpdateAISearch && this.tree.hasNode(this.searchResult.aiTextSearchResult)) { this.tree.collapse(this.searchResult.aiTextSearchResult); } - this.onQueryTriggered(query, options, excludePatternText, includePatternText, triggeredOnType, shouldKeepAIResults); + this.onQueryTriggered(query, options, excludePatternText, includePatternText, triggeredOnType, shouldKeepAIResults, shouldUpdateAISearch); if (!preserveFocus) { this.searchWidget.focus(false, undefined, true); // focus back to input field @@ -1598,7 +1603,7 @@ export class SearchView extends ViewPane { }); } - private onQueryTriggered(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string, triggeredOnType: boolean, shouldKeepAIResults: boolean): void { + private onQueryTriggered(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string, triggeredOnType: boolean, shouldKeepAIResults: boolean, shouldUpdateAISearch: boolean): void { this.addToSearchHistoryDelayer.trigger(() => { this.searchWidget.searchInput?.onSearchSubmit(); this.inputPatternExcludes.onSearchSubmit(); @@ -1609,7 +1614,7 @@ export class SearchView extends ViewPane { this.viewModel.cancelAISearch(true); this.currentSearchQ = this.currentSearchQ - .then(() => this.doSearch(query, excludePatternText, includePatternText, triggeredOnType, shouldKeepAIResults)) + .then(() => this.doSearch(query, excludePatternText, includePatternText, triggeredOnType, shouldKeepAIResults, shouldUpdateAISearch)) .then(() => undefined, () => undefined); } @@ -1663,8 +1668,34 @@ export class SearchView extends ViewPane { } // Special case for when we have an AI provider registered - if (this.shouldShowAIResults()) { - Constants.SearchContext.AIResultsRequested.bindTo(this.contextKeyService).set(!!aiResults); + Constants.SearchContext.AIResultsRequested.bindTo(this.contextKeyService).set(this.shouldShowAIResults() && !!aiResults); + + if (this.shouldShowAIResults() && !allResults) { + const messageEl = this.clearMessage(); + const noResultsMessage = nls.localize('noResultsFallback', "No results found. "); + dom.append(messageEl, noResultsMessage); + + let aiName = 'Copilot'; + try { + aiName = (await this.searchService.getAIName()) || aiName; + } catch (e) { + // ignore + } + + if (aiName) { + const searchWithAIButtonTooltip = appendKeyBindingLabel( + nls.localize('triggerAISearch.tooltip', "Search with {0}", aiName), + this.keybindingService.lookupKeybinding(Constants.SearchCommandIds.SearchWithAIActionId) + ); + const searchWithAIButtonText = nls.localize('searchWithAIButtonTooltip', "Search with {0}.", aiName); + const searchWithAIButton = this.messageDisposables.add(new SearchLinkButton( + searchWithAIButtonText, + () => { + this.commandService.executeCommand(Constants.SearchCommandIds.SearchWithAIActionId); + }, this.hoverService, searchWithAIButtonTooltip)); + dom.append(messageEl, searchWithAIButton.element); + } + if (!aiResults) { return; } @@ -1789,6 +1820,7 @@ export class SearchView extends ViewPane { const result = this.viewModel.addAIResults(); return result.then((complete) => { clearTimeout(slowTimer); + this.updateSearchResultCount(this.viewModel.searchResult.query?.userDisabledExcludesAndIgnoreFiles, this.viewModel.searchResult.query?.onlyOpenEditors, false); return this.onSearchComplete(progressComplete, excludePatternText, includePatternText, complete, false); }, (e) => { clearTimeout(slowTimer); @@ -1796,7 +1828,7 @@ export class SearchView extends ViewPane { }); } - private doSearch(query: ITextQuery, excludePatternText: string, includePatternText: string, triggeredOnType: boolean, shouldKeepAIResults: boolean): Thenable { + private doSearch(query: ITextQuery, excludePatternText: string, includePatternText: string, triggeredOnType: boolean, shouldKeepAIResults: boolean, shouldUpdateAISearch: boolean): Thenable { let progressComplete: () => void; this.progressService.withProgress({ location: this.getProgressLocation(), delay: triggeredOnType ? 300 : 0 }, _progress => { return new Promise(resolve => progressComplete = resolve); @@ -1805,6 +1837,7 @@ export class SearchView extends ViewPane { this.searchWidget.searchInput?.clearMessage(); this.state = SearchUIState.Searching; this.showEmptyStage(); + this.model.searchResult.aiTextSearchResult.hidden = !shouldKeepAIResults && !shouldUpdateAISearch; const slowTimer = setTimeout(() => { this.state = SearchUIState.SlowSearch; @@ -1822,9 +1855,14 @@ export class SearchView extends ViewPane { this.viewModel.replaceString = this.searchWidget.getReplaceValue(); const result = this.viewModel.search(query); - if (!shouldKeepAIResults) { + if (!shouldKeepAIResults || shouldUpdateAISearch) { this.viewModel.searchResult.setAIQueryUsingTextQuery(query); } + + if (shouldUpdateAISearch) { + this.tree.updateChildren(this.searchResult.aiTextSearchResult); + } + return result.asyncResults.then((complete) => { clearTimeout(slowTimer); return this.onSearchComplete(progressComplete, excludePatternText, includePatternText, complete); @@ -2340,8 +2378,13 @@ class SearchViewDataSource implements IAsyncDataSource('folderMatchWithResourceFocus', false), IsEditableItemKey: new RawContextKey('isEditableItem', true), MatchFocusKey: new RawContextKey('matchFocus', false), + SearchResultHeaderFocused: new RawContextKey('searchResultHeaderFocused', false), ViewHasSearchPatternKey: new RawContextKey('viewHasSearchPattern', false), ViewHasReplacePatternKey: new RawContextKey('viewHasReplacePattern', false), ViewHasFilePatternKey: new RawContextKey('viewHasFilePattern', false), diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 2ef0de41..06c660a0 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -201,7 +201,7 @@ export class SearchEditor extends AbstractTextCodeEditor ariaLabel: localize('label.includes', 'Search Include Patterns'), inputBoxStyles: searchEditorInputboxStyles })); - this.inputPatternIncludes.onSubmit(triggeredOnType => this.triggerSearch({ resetCursor: false, delay: triggeredOnType ? this.searchConfig.searchOnTypeDebouncePeriod : 0 })); + this._register(this.inputPatternIncludes.onSubmit(triggeredOnType => this.triggerSearch({ resetCursor: false, delay: triggeredOnType ? this.searchConfig.searchOnTypeDebouncePeriod : 0 }))); this._register(this.inputPatternIncludes.onChangeSearchInEditorsBox(() => this.triggerSearch())); // Excludes diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index 8529260f..45d4b02a 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -7,7 +7,7 @@ import { localize } from '../../../../nls.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { DeferredPromise } from '../../../../base/common/async.js'; @@ -58,11 +58,11 @@ export class SpeechService extends Disposable implements ISpeechService { private readonly providers = new Map(); private readonly providerDescriptors = new Map(); - private readonly hasSpeechProviderContext = HasSpeechProvider.bindTo(this.contextKeyService); + private readonly hasSpeechProviderContext: IContextKey; constructor( @ILogService private readonly logService: ILogService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -70,6 +70,10 @@ export class SpeechService extends Disposable implements ISpeechService { ) { super(); + this.hasSpeechProviderContext = HasSpeechProvider.bindTo(contextKeyService); + this.textToSpeechInProgress = TextToSpeechInProgress.bindTo(contextKeyService); + this.speechToTextInProgress = SpeechToTextInProgress.bindTo(contextKeyService); + this.handleAndRegisterSpeechExtensions(); } @@ -136,7 +140,7 @@ export class SpeechService extends Disposable implements ISpeechService { private activeSpeechToTextSessions = 0; get hasActiveSpeechToTextSession() { return this.activeSpeechToTextSessions > 0; } - private readonly speechToTextInProgress = SpeechToTextInProgress.bindTo(this.contextKeyService); + private readonly speechToTextInProgress: IContextKey; async createSpeechToTextSession(token: CancellationToken, context: string = 'speech'): Promise { const provider = await this.getProvider(); @@ -249,7 +253,7 @@ export class SpeechService extends Disposable implements ISpeechService { private activeTextToSpeechSessions = 0; get hasActiveTextToSpeechSession() { return this.activeTextToSpeechSessions > 0; } - private readonly textToSpeechInProgress = TextToSpeechInProgress.bindTo(this.contextKeyService); + private readonly textToSpeechInProgress: IContextKey; async createTextToSpeechSession(token: CancellationToken, context: string = 'speech'): Promise { const provider = await this.getProvider(); diff --git a/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts b/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts index ba0e35ef..0602ca9c 100644 --- a/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts +++ b/src/vs/workbench/contrib/surveys/browser/nps.contribution.ts @@ -15,6 +15,7 @@ import { Severity, INotificationService, NotificationPriority } from '../../../. import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { URI } from '../../../../base/common/uri.js'; import { platform } from '../../../../base/common/process.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; const PROBABILITY = 0.15; const SESSION_COUNT_KEY = 'nps/sessionCount'; @@ -29,9 +30,10 @@ class NPSContribution implements IWorkbenchContribution { @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IOpenerService openerService: IOpenerService, - @IProductService productService: IProductService + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService ) { - if (!productService.npsSurveyUrl) { + if (!productService.npsSurveyUrl || !configurationService.getValue('telemetry.feedback.enabled')) { return; } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index d6a12341..25385286 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -47,7 +47,7 @@ import { ITextFileService } from '../../../services/textfile/common/textfiles.js import { ITerminalGroupService, ITerminalService } from '../../terminal/browser/terminal.js'; import { ITerminalProfileResolverService } from '../../terminal/common/terminal.js'; -import { ConfiguringTask, ContributedTask, CustomTask, ExecutionEngine, InMemoryTask, ITaskEvent, ITaskIdentifier, ITaskSet, JsonSchemaVersion, KeyedTaskIdentifier, RuntimeType, Task, TASK_RUNNING_STATE, TaskDefinition, TaskEventKind, TaskGroup, TaskRunSource, TaskSettingId, TaskSorter, TaskSourceKind, TasksSchemaProperties, USER_TASKS_GROUP_KEY } from '../common/tasks.js'; +import { ConfiguringTask, ContributedTask, CustomTask, ExecutionEngine, InMemoryTask, ITaskEvent, ITaskIdentifier, ITaskSet, JsonSchemaVersion, KeyedTaskIdentifier, RuntimeType, Task, TASK_RUNNING_STATE, TaskDefinition, TaskGroup, TaskRunSource, TaskSettingId, TaskSorter, TaskSourceKind, TasksSchemaProperties, USER_TASKS_GROUP_KEY, TaskEventKind } from '../common/tasks.js'; import { CustomExecutionSupportedContext, ICustomizationProperties, IProblemMatcherRunOptions, ITaskFilter, ITaskProvider, ITaskService, IWorkspaceFolderTaskResult, ProcessExecutionSupportedContext, ServerlessWebContext, ShellExecutionSupportedContext, TaskCommandsRegistered, TaskExecutionSupportedContext } from '../common/taskService.js'; import { ITaskExecuteResult, ITaskResolver, ITaskSummary, ITaskSystem, ITaskSystemInfo, ITaskTerminateResponse, TaskError, TaskErrors, TaskExecuteKind } from '../common/taskSystem.js'; import { getTemplates as getTaskTemplates } from '../common/taskTemplates.js'; @@ -85,6 +85,7 @@ import { IPreferencesService } from '../../../services/preferences/common/prefer import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { isCancellationError } from '../../../../base/common/errors.js'; + const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const PROBLEM_MATCHER_NEVER_CONFIG = 'task.problemMatchers.neverPrompt'; const USE_SLOW_PICKER = 'task.quickOpen.showAll'; diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 98592cc0..8629aa38 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -20,7 +20,7 @@ import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatus import { IOutputChannelRegistry, Extensions as OutputExt } from '../../../services/output/common/output.js'; -import { ITaskEvent, TaskEventKind, TaskGroup, TaskSettingId, TASKS_CATEGORY, TASK_RUNNING_STATE, TASK_TERMINAL_ACTIVE } from '../common/tasks.js'; +import { ITaskEvent, TaskGroup, TaskSettingId, TASKS_CATEGORY, TASK_RUNNING_STATE, TASK_TERMINAL_ACTIVE, TaskEventKind } from '../common/tasks.js'; import { ITaskService, TaskCommandsRegistered, TaskExecutionSupportedContext } from '../common/taskService.js'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; diff --git a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts index 47813317..d00ad24c 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts @@ -94,7 +94,7 @@ export class TaskTerminalStatus extends Disposable { } terminalData.taskRunEnded = true; terminalData.terminal.statusList.remove(terminalData.status); - if ((event.exitCode === 0) && (terminalData.problemMatcher.numberOfMatches === 0)) { + if ((event.exitCode === 0) && (!terminalData.problemMatcher.maxMarkerSeverity || terminalData.problemMatcher.maxMarkerSeverity < MarkerSeverity.Warning)) { this._accessibilitySignalService.playSignal(AccessibilitySignal.taskCompleted); if (terminalData.task.configurationProperties.isBackground) { for (const status of terminalData.terminal.statusList.statuses) { @@ -103,7 +103,7 @@ export class TaskTerminalStatus extends Disposable { } else { terminalData.terminal.statusList.add(SUCCEEDED_TASK_STATUS); } - } else if (event.exitCode || terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Error) { + } else if (event.exitCode || (terminalData.problemMatcher.maxMarkerSeverity !== undefined && terminalData.problemMatcher.maxMarkerSeverity >= MarkerSeverity.Warning)) { this._accessibilitySignalService.playSignal(AccessibilitySignal.taskFailed); terminalData.terminal.statusList.add(FAILED_TASK_STATUS); } else if (terminalData.problemMatcher.maxMarkerSeverity === MarkerSeverity.Warning) { diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 61d99672..2fcb9837 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -833,6 +833,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { eventCounter++; this._busyTasks[mapKey] = task; this._fireTaskEvent(TaskEvent.general(TaskEventKind.Active, task, terminal?.instanceId)); + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherStarted, task, terminal?.instanceId)); } else if (event.kind === ProblemCollectorEventKind.BackgroundProcessingEnds) { eventCounter--; if (this._busyTasks[mapKey]) { @@ -842,6 +843,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { if (eventCounter === 0) { if ((watchingProblemMatcher.numberOfMatches > 0) && watchingProblemMatcher.maxMarkerSeverity && (watchingProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error)) { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherFoundErrors, task, terminal?.instanceId)); const reveal = task.command.presentation!.reveal; const revealProblems = task.command.presentation!.revealProblems; if (revealProblems === RevealProblemKind.OnProblem) { @@ -850,6 +852,8 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { this._terminalService.setActiveInstance(terminal!); this._terminalGroupService.showPanel(false); } + } else { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherEnded, task, terminal?.instanceId)); } } } @@ -981,7 +985,17 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { const problemMatchers = await this._resolveMatchers(resolver, task.configurationProperties.problemMatchers); const startStopProblemMatcher = new StartStopProblemCollector(problemMatchers, this._markerService, this._modelService, ProblemHandlingStrategy.Clean, this._fileService); this._terminalStatusManager.addTerminal(task, terminal, startStopProblemMatcher); - + startStopProblemMatcher.onDidStateChange((event) => { + if (event.kind === ProblemCollectorEventKind.BackgroundProcessingBegins) { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherStarted, task, terminal?.instanceId)); + } else if (event.kind === ProblemCollectorEventKind.BackgroundProcessingEnds) { + if (startStopProblemMatcher.numberOfMatches && startStopProblemMatcher.maxMarkerSeverity && startStopProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error) { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherFoundErrors, task, terminal?.instanceId)); + } else { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherEnded, task, terminal?.instanceId)); + } + } + }); let processStartedSignaled = false; terminal.processReady.then(() => { if (!processStartedSignaled) { @@ -1044,6 +1058,11 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { delete this._busyTasks[mapKey]; } this._fireTaskEvent(TaskEvent.general(TaskEventKind.Inactive, task, terminal?.instanceId)); + if (startStopProblemMatcher.numberOfMatches && startStopProblemMatcher.maxMarkerSeverity && startStopProblemMatcher.maxMarkerSeverity >= MarkerSeverity.Error) { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherFoundErrors, task, terminal?.instanceId)); + } else { + this._fireTaskEvent(TaskEvent.general(TaskEventKind.ProblemMatcherEnded, task, terminal?.instanceId)); + } this._fireTaskEvent(TaskEvent.general(TaskEventKind.End, task, terminal?.instanceId)); resolve({ exitCode: exitCode ?? undefined }); }); diff --git a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index 3cb325d2..594086ee 100644 --- a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts @@ -362,6 +362,8 @@ export class StartStopProblemCollector extends AbstractProblemCollector implemen private currentOwner: string | undefined; private currentResource: string | undefined; + private _hasStarted: boolean = false; + constructor(problemMatchers: ProblemMatcher[], markerService: IMarkerService, modelService: IModelService, _strategy: ProblemHandlingStrategy = ProblemHandlingStrategy.Clean, fileService?: IFileService) { super(problemMatchers, markerService, modelService, fileService); const ownerSet: { [key: string]: boolean } = Object.create(null); @@ -373,6 +375,10 @@ export class StartStopProblemCollector extends AbstractProblemCollector implemen } protected async processLineInternal(line: string): Promise { + if (!this._hasStarted) { + this._hasStarted = true; + this._onDidStateChange.fire(IProblemCollectorEvent.create(ProblemCollectorEventKind.BackgroundProcessingBegins)); + } const markerMatch = this.tryFindMarker(line); if (!markerMatch) { return; diff --git a/src/vs/workbench/contrib/tasks/common/taskService.ts b/src/vs/workbench/contrib/tasks/common/taskService.ts index ac2f0c34..ec687197 100644 --- a/src/vs/workbench/contrib/tasks/common/taskService.ts +++ b/src/vs/workbench/contrib/tasks/common/taskService.ts @@ -108,3 +108,8 @@ export interface ITaskService { extensionCallbackTaskComplete(task: Task, result: number | undefined): Promise; } + +export interface ITaskTerminalStatus { + terminalId: number; + status: string; +} diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index c78cb6d4..2c56cfc6 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -1102,18 +1102,6 @@ export class TaskSorter { } } -export const enum TaskEventKind { - DependsOnStarted = 'dependsOnStarted', - AcquiredInput = 'acquiredInput', - Start = 'start', - ProcessStarted = 'processStarted', - Active = 'active', - Inactive = 'inactive', - Changed = 'changed', - Terminated = 'terminated', - ProcessEnded = 'processEnded', - End = 'end' -} export const enum TaskRunType { @@ -1125,6 +1113,49 @@ export interface ITaskChangedEvent { kind: TaskEventKind.Changed; } + + +export enum TaskEventKind { + /** Indicates that a task's properties or configuration have changed */ + Changed = 'changed', + + /** Indicates that a task has begun executing */ + ProcessStarted = 'processStarted', + + /** Indicates that a task process has completed */ + ProcessEnded = 'processEnded', + + /** Indicates that a task was terminated, either by user action or by the system */ + Terminated = 'terminated', + + /** Indicates that a task has started running */ + Start = 'start', + + /** Indicates that a task has acquired all needed input/variables to execute */ + AcquiredInput = 'acquiredInput', + + /** Indicates that a dependent task has started */ + DependsOnStarted = 'dependsOnStarted', + + /** Indicates that a task is actively running/processing */ + Active = 'active', + + /** Indicates that a task is paused/waiting but not complete */ + Inactive = 'inactive', + + /** Indicates that a task has completed fully */ + End = 'end', + + /** Indicates that a task's problem matcher has started */ + ProblemMatcherStarted = 'problemMatcherStarted', + + /** Indicates that a task's problem matcher has ended */ + ProblemMatcherEnded = 'problemMatcherEnded', + + /** Indicates that a task's problem matcher has found errors */ + ProblemMatcherFoundErrors = 'problemMatcherFoundErrors' +} + interface ITaskCommon { taskId: string; runType: TaskRunType; @@ -1157,8 +1188,13 @@ export interface ITaskStartedEvent extends ITaskCommon { resolvedVariables: Map; } +export interface ITaskProblemMatcherEndedEvent extends ITaskCommon { + kind: TaskEventKind.ProblemMatcherEnded; + hasErrors: boolean; +} + export interface ITaskGeneralEvent extends ITaskCommon { - kind: TaskEventKind.AcquiredInput | TaskEventKind.DependsOnStarted | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.End; + kind: TaskEventKind.AcquiredInput | TaskEventKind.DependsOnStarted | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.End | TaskEventKind.ProblemMatcherEnded | TaskEventKind.ProblemMatcherStarted | TaskEventKind.ProblemMatcherFoundErrors; terminalId: number | undefined; } @@ -1168,7 +1204,8 @@ export type ITaskEvent = | ITaskProcessEndedEvent | ITaskTerminatedEvent | ITaskStartedEvent - | ITaskGeneralEvent; + | ITaskGeneralEvent + | ITaskProblemMatcherEndedEvent; export const enum TaskRunSource { System, @@ -1224,7 +1261,7 @@ export namespace TaskEvent { }; } - export function general(kind: TaskEventKind.AcquiredInput | TaskEventKind.DependsOnStarted | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.End, task: Task, terminalId?: number): ITaskGeneralEvent { + export function general(kind: TaskEventKind.AcquiredInput | TaskEventKind.DependsOnStarted | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.End | TaskEventKind.ProblemMatcherEnded | TaskEventKind.ProblemMatcherStarted | TaskEventKind.ProblemMatcherFoundErrors, task: Task, terminalId?: number): ITaskGeneralEvent { return { ...common(task), kind, diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index 06e08fcd..15ffde58 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -154,15 +154,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr private onTextFileModelResolved(e: ITextFileResolveEvent): void { const settingsType = this.getTypeIfSettings(e.model.resource); - if (settingsType) { - type SettingsReadClassification = { - owner: 'isidorn'; - settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the settings file that was read.' }; - comment: 'Track when a settings file was read, for example from an editor.'; - }; - - this.telemetryService.publicLog2<{ settingsType: string }, SettingsReadClassification>('settingsRead', { settingsType }); // Do not log read to user settings.json and .vscode folder as a fileGet event as it ruins our JSON usage data - } else { + if (!settingsType) { type FileGetClassification = { owner: 'isidorn'; comment: 'Track when a file was read, for example from an editor.'; @@ -174,14 +166,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr private onTextFileModelSaved(e: ITextFileSaveEvent): void { const settingsType = this.getTypeIfSettings(e.model.resource); - if (settingsType) { - type SettingsWrittenClassification = { - owner: 'isidorn'; - settingsType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the settings file that was written to.' }; - comment: 'Track when a settings file was written to, for example from an editor.'; - }; - this.telemetryService.publicLog2<{ settingsType: string }, SettingsWrittenClassification>('settingsWritten', { settingsType }); // Do not log write to user settings.json and .vscode folder as a filePUT event as it ruins our JSON usage data - } else { + if (!settingsType) { type FilePutClassfication = { owner: 'isidorn'; comment: 'Track when a file was written to, for example from an editor.'; diff --git a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts index 1ec81c70..9da2be1b 100644 --- a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts +++ b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts @@ -87,7 +87,8 @@ export class EnvironmentVariableInfoChangesActive implements IEnvironmentVariabl return { id: TerminalStatus.EnvironmentVariableInfoChangesActive, severity: Severity.Info, - tooltip: this._getInfo(scope), + tooltip: undefined, // The action is present when details aren't shown + detailedTooltip: this._getInfo(scope), hoverActions: this._getActions(scope) }; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index cee30a6d..4a4236d5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getFontSnippets } from '../../../../base/browser/fonts.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Schemas } from '../../../../base/common/network.js'; import { isIOS, isWindows } from '../../../../base/common/platform.js'; @@ -49,6 +50,7 @@ import { registerTerminalConfiguration } from '../common/terminalConfiguration.j import { TerminalContextKeyStrings, TerminalContextKeys } from '../common/terminalContextKey.js'; import { terminalStrings } from '../common/terminalStrings.js'; import { registerSendSequenceKeybinding } from './terminalKeybindings.js'; +import { TerminalTelemetryContribution } from './terminalTelemetry.js'; // Register services registerSingleton(ITerminalLogService, TerminalLogService, InstantiationType.Delayed); @@ -63,10 +65,11 @@ registerSingleton(ITerminalProfileService, TerminalProfileService, Instantiation // This contribution blocks startup as it's critical to enable the web embedder window.createTerminal API registerWorkbenchContribution2(TerminalMainContribution.ID, TerminalMainContribution, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(RemoteTerminalBackendContribution.ID, RemoteTerminalBackendContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(TerminalTelemetryContribution.ID, TerminalTelemetryContribution, WorkbenchPhase.AfterRestored); // Register configurations registerTerminalPlatformConfiguration(); -registerTerminalConfiguration(); +registerTerminalConfiguration(getFontSnippets); // Register editor/dnd contributions Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(TerminalEditorInput.ID, TerminalInputSerializer); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 664eea28..ff14b2b4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -42,7 +42,6 @@ import { IPreferencesService } from '../../../services/preferences/common/prefer import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { isAbsolute } from '../../../../base/common/path.js'; -import { AbstractVariableResolverService } from '../../../services/configurationResolver/common/variableResolver.js'; import { ITerminalQuickPickItem } from './terminalProfileQuickpick.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { getIconId, getColorClass, getUriClasses } from './terminalIcon.js'; @@ -62,6 +61,7 @@ import { editorGroupToColumn } from '../../../services/editor/common/editorGroup import { InstanceContext } from './terminalContextMenu.js'; import { AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; import { TerminalTabList } from './terminalTabsList.js'; +import { ConfigurationResolverExpression } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); @@ -904,7 +904,7 @@ export function registerTerminalActions() { registerActiveInstanceAction({ id: TerminalCommandId.SelectToPreviousCommand, - title: localize2('workbench.action.terminal.selectToPreviousCommand', 'Select To Previous Command'), + title: localize2('workbench.action.terminal.selectToPreviousCommand', 'Select to Previous Command'), keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow, when: TerminalContextKeys.focus, @@ -919,7 +919,7 @@ export function registerTerminalActions() { registerActiveInstanceAction({ id: TerminalCommandId.SelectToNextCommand, - title: localize2('workbench.action.terminal.selectToNextCommand', 'Select To Next Command'), + title: localize2('workbench.action.terminal.selectToNextCommand', 'Select to Next Command'), keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow, when: TerminalContextKeys.focus, @@ -934,7 +934,7 @@ export function registerTerminalActions() { registerActiveXtermAction({ id: TerminalCommandId.SelectToPreviousLine, - title: localize2('workbench.action.terminal.selectToPreviousLine', 'Select To Previous Line'), + title: localize2('workbench.action.terminal.selectToPreviousLine', 'Select to Previous Line'), precondition: sharedWhenClause.terminalAvailable, run: async (xterm, _, instance) => { xterm.markTracker.selectToPreviousLine(); @@ -945,7 +945,7 @@ export function registerTerminalActions() { registerActiveXtermAction({ id: TerminalCommandId.SelectToNextLine, - title: localize2('workbench.action.terminal.selectToNextLine', 'Select To Next Line'), + title: localize2('workbench.action.terminal.selectToNextLine', 'Select to Next Line'), precondition: sharedWhenClause.terminalAvailable, run: async (xterm, _, instance) => { xterm.markTracker.selectToNextLine(); @@ -1682,7 +1682,7 @@ async function resolveWorkspaceFolderCwd(folder: IWorkspaceFolder, configuration } const resolvedCwdConfig = await configurationResolverService.resolveAsync(folder, cwdConfig); - return isAbsolute(resolvedCwdConfig) || resolvedCwdConfig.startsWith(AbstractVariableResolverService.VARIABLE_LHS) + return isAbsolute(resolvedCwdConfig) || resolvedCwdConfig.startsWith(ConfigurationResolverExpression.VARIABLE_LHS) ? { folder, isAbsolute: true, isOverridden: true, cwd: URI.from({ ...folder.uri, path: resolvedCwdConfig }) } : { folder, isAbsolute: false, isOverridden: true, cwd: URI.joinPath(folder.uri, resolvedCwdConfig) }; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts b/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts index 4e295a44..f5f78bd1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts @@ -58,7 +58,7 @@ export class TerminalIconPicker extends Disposable { this._iconSelectBox.dispose(); })); this._iconSelectBox.clearInput(); - const hoverWidget = this._hoverService.showHover({ + const hoverWidget = this._hoverService.showInstantHover({ content: this._iconSelectBox.domNode, target: getActiveDocument().body, position: { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index e0a7854c..8149a20e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -90,6 +90,7 @@ import type { IMenu } from '../../../../platform/actions/common/actions.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { TerminalContribCommandId } from '../terminalContribExports.js'; import type { IProgressState } from '@xterm/addon-progress'; +import { refreshShellIntegrationInfoStatus } from './terminalTooltip.js'; const enum Constants { /** @@ -469,11 +470,15 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (capability === TerminalCapability.CommandDetection) { const commandDetection = this.capabilities.get(capability); if (commandDetection) { + commandDetection.promptInputModel.setShellType(this.shellType); capabilityListeners.set(capability, Event.any( commandDetection.promptInputModel.onDidStartInput, commandDetection.promptInputModel.onDidChangeInput, commandDetection.promptInputModel.onDidFinishInput - )(() => this._labelComputer?.refreshLabel(this))); + )(() => { + this._labelComputer?.refreshLabel(this); + refreshShellIntegrationInfoStatus(this); + })); } } })); @@ -846,6 +851,18 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Init winpty compat and link handler after process creation as they rely on the // underlying process OS this._register(this._processManager.onProcessReady(async (processTraits) => { + // Respond to DA1 with basic conformance. Note that including this is required to avoid + // a long delay in conpty 1.22+ where it waits for the response. + // Reference: https://github.com/microsoft/terminal/blob/3760caed97fa9140a40777a8fbc1c95785e6d2ab/src/terminal/adapter/adaptDispatch.cpp#L1471-L1495 + if (processTraits?.windowsPty?.backend === 'conpty') { + this._register(xterm.raw.parser.registerCsiHandler({ final: 'c' }, params => { + if (params.length === 0 || params.length === 1 && params[0] === 0) { + this._processManager.write('\x1b[?61;4c'); + return true; + } + return false; + })); + } if (this._processManager.os) { lineDataEventAddon.setOperatingSystem(this._processManager.os); } @@ -860,6 +877,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { })); this._register(xterm.onDidChangeProgress(() => this._labelComputer?.refreshLabel(this))); + // Register and update the terminal's shell integration status + this._register(Event.runAndSubscribe(xterm.shellIntegration.onDidChangeSeenSequences, () => { + if (xterm.shellIntegration.seenSequences.size > 0) { + refreshShellIntegrationInfoStatus(this); + } + })); + // Set up updating of the process cwd on key press, this is only needed when the cwd // detection capability has not been registered if (!this.capabilities.has(TerminalCapability.CwdDetection)) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 42b3f065..c998d9c0 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -1171,13 +1171,16 @@ export class TerminalService extends Disposable implements ITerminalService { } protected _showBackgroundTerminal(instance: ITerminalInstance): void { + const index = this._backgroundedTerminalInstances.indexOf(instance); + if (index === -1) { + return; + } this._backgroundedTerminalInstances.splice(this._backgroundedTerminalInstances.indexOf(instance), 1); const disposables = this._backgroundedTerminalDisposables.get(instance.instanceId); if (disposables) { dispose(disposables); } this._backgroundedTerminalDisposables.delete(instance.instanceId); - instance.shellLaunchConfig.hideFromUser = false; this._terminalGroupService.createGroup(instance); // Make active automatically if it's the first instance diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts index ecc8d382..bdbbe661 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -24,6 +24,7 @@ export const enum TerminalStatus { Disconnected = 'disconnected', RelaunchNeeded = 'relaunch-needed', EnvironmentVariableInfoChangesActive = 'env-var-info-changes-active', + ShellIntegrationInfo = 'shell-integration-info', ShellIntegrationAttentionNeeded = 'shell-integration-attention-needed' } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index 87239108..3213dd89 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -458,8 +458,8 @@ export class TerminalTabbedView extends Disposable { if (!instance) { return; } - this._hoverService.showHover({ - ...getInstanceHoverInfo(instance), + this._hoverService.showInstantHover({ + ...getInstanceHoverInfo(instance, this._storageService), target: this._terminalContainer, trapFocus: true }, true); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 5d8140d6..bf8e35c8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -53,6 +53,8 @@ import { TerminalContextActionRunner } from './terminalContextMenu.js'; import type { IHoverAction } from '../../../../base/browser/ui/hover/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; +import { TerminalStorageKeys } from '../common/terminalStorageKeys.js'; const $ = DOM.$; @@ -82,6 +84,7 @@ export class TerminalTabList extends WorkbenchList { @IInstantiationService instantiationService: IInstantiationService, @IDecorationsService decorationsService: IDecorationsService, @IThemeService private readonly _themeService: IThemeService, + @IStorageService private readonly _storageService: IStorageService, @ILifecycleService lifecycleService: ILifecycleService, @IHoverService private readonly _hoverService: IHoverService, ) { @@ -128,7 +131,8 @@ export class TerminalTabList extends WorkbenchList { this.reveal(i); } this.refresh(); - }) + }), + this._storageService.onDidChangeValue(StorageScope.APPLICATION, TerminalStorageKeys.TabsShowDetailed, this.disposables)(() => this.refresh()), ]; // Dispose of instance listeners on shutdown to avoid extra work and so tabs don't disappear @@ -229,8 +233,8 @@ export class TerminalTabList extends WorkbenchList { return; } - this._hoverService.showHover({ - ...getInstanceHoverInfo(instance), + this._hoverService.showInstantHover({ + ...getInstanceHoverInfo(instance, this._storageService), target: this.getHTMLElement(), trapFocus: true }, true); @@ -257,6 +261,7 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer { + // Wait for process ready so the shell launch config is fully resolved + await instance.processReady; + this._logCreateInstance(instance.shellLaunchConfig); + })); + } + + private _logCreateInstance(shellLaunchConfig: IShellLaunchConfig): void { + type TerminalCreationTelemetryData = { + shellType: string; + isReconnect: boolean; + isCustomPtyImplementation: boolean; + isLoginShell: boolean; + }; + type TerminalCreationTelemetryClassification = { + owner: 'tyriar'; + comment: 'Track details about terminal creation, such as the shell type'; + shellType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The path of the file as a hash.' }; + isReconnect: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the terminal is reconnecting to an existing instance.' }; + isCustomPtyImplementation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the terminal was using a custom PTY implementation.' }; + isLoginShell: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the arguments contain -l or --login.' }; + }; + this._telemetryService.publicLog2('terminal/createInstance', { + shellType: getSanitizedShellType(shellLaunchConfig), + isReconnect: !!shellLaunchConfig.attachPersistentProcess, + isCustomPtyImplementation: !!shellLaunchConfig.customPtyImplementation, + isLoginShell: (typeof shellLaunchConfig.args === 'string' ? shellLaunchConfig.args.split(' ') : shellLaunchConfig.args)?.some(arg => arg === '-l' || arg === '--login') ?? false, + }); + } +} + +const enum AllowedShellType { + Unknown = 'unknown', + + // Windows only + CommandPrompt = 'cmd', + GitBash = 'git-bash', + WindowsPowerShell = 'windows-powershell', + Wsl = 'wsl', + + // All platforms + Bash = 'bash', + Csh = 'csh', + Dash = 'dash', + Fish = 'fish', + Ksh = 'ksh', + Nushell = 'nu', + Pwsh = 'pwsh', + Sh = 'sh', + Ssh = 'ssh', + Tcsh = 'tcsh', + Tmux = 'tmux', + Zsh = 'zsh', + + // Lanugage REPLs + Julia = 'julia', + Node = 'node', + Python = 'python', + RubyIrb = 'irb', +} + +// Types that match the executable name directly +const shellTypeExecutableAllowList: Set = new Set([ + AllowedShellType.CommandPrompt, + AllowedShellType.Wsl, + + AllowedShellType.Bash, + AllowedShellType.Csh, + AllowedShellType.Dash, + AllowedShellType.Fish, + AllowedShellType.Ksh, + AllowedShellType.Nushell, + AllowedShellType.Pwsh, + AllowedShellType.Sh, + AllowedShellType.Ssh, + AllowedShellType.Tcsh, + AllowedShellType.Tmux, + AllowedShellType.Zsh, + + AllowedShellType.Julia, + AllowedShellType.Node, + AllowedShellType.RubyIrb, +]) satisfies Set; + +// Dynamic executables that map to a single type +const shellTypeExecutableRegexAllowList: { regex: RegExp; type: AllowedShellType }[] = [ + { regex: /^python(?:\d+(?:\.\d+)?)?$/i, type: AllowedShellType.Python }, +]; + +// Path-based look ups +const shellTypePathRegexAllowList: { regex: RegExp; type: AllowedShellType }[] = [ + // Git bash uses bash.exe, so look up based on the path + { regex: /Git\\bin\\bash\.exe$/i, type: AllowedShellType.GitBash }, + // WindowsPowerShell should always be installed on this path, we cannot just look at the + // executable name since powershell is the CLI on other platforms sometimes (eg. snap package) + { regex: /WindowsPowerShell\\v1.0\\powershell.exe$/i, type: AllowedShellType.WindowsPowerShell }, + // WSL executables will represent some other shell in the end, but it's difficult to determine + // when we log + { regex: /Windows\\System32\\(?:bash|wsl)\.exe$/i, type: AllowedShellType.Wsl }, +]; + +function getSanitizedShellType(shellLaunchConfig: IShellLaunchConfig): AllowedShellType { + if (!shellLaunchConfig.executable) { + return AllowedShellType.Unknown; + } + const executableFile = basename(shellLaunchConfig.executable); + const executableFileWithoutExt = executableFile.replace(/\.[^\.]+$/, ''); + for (const entry of shellTypePathRegexAllowList) { + if (entry.regex.test(shellLaunchConfig.executable)) { + return entry.type; + } + } + for (const entry of shellTypeExecutableRegexAllowList) { + if (entry.regex.test(executableFileWithoutExt)) { + return entry.type; + } + } + if ((shellTypeExecutableAllowList).has(executableFileWithoutExt)) { + return executableFileWithoutExt as AllowedShellType; + } + return AllowedShellType.Unknown; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index fd934009..3c232558 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts @@ -5,53 +5,51 @@ import { localize } from '../../../../nls.js'; import { ITerminalInstance } from './terminal.js'; -import { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; import { asArray } from '../../../../base/common/arrays.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import type { IHoverAction } from '../../../../base/browser/ui/hover/hover.js'; +import { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; +import { TerminalStatus } from './terminalStatusList.js'; +import Severity from '../../../../base/common/severity.js'; +import { StorageScope, StorageTarget, type IStorageService } from '../../../../platform/storage/common/storage.js'; +import { TerminalStorageKeys } from '../common/terminalStorageKeys.js'; +import type { ITerminalStatusHoverAction } from '../common/terminal.js'; +import { basename } from '../../../../base/common/path.js'; -export function getInstanceHoverInfo(instance: ITerminalInstance): { content: MarkdownString; actions: IHoverAction[] } { +export function getInstanceHoverInfo(instance: ITerminalInstance, storageService: IStorageService): { content: MarkdownString; actions: IHoverAction[] } { + const showDetailed = parseInt(storageService.get(TerminalStorageKeys.TabsShowDetailed, StorageScope.APPLICATION) ?? '0'); let statusString = ''; const statuses = instance.statusList.statuses; - const actions = []; + const actions: ITerminalStatusHoverAction[] = []; for (const status of statuses) { - statusString += `\n\n---\n\n${status.icon ? `$(${status.icon?.id}) ` : ''}${status.tooltip || status.id}`; + if (showDetailed) { + if (status.detailedTooltip ?? status.tooltip) { + statusString += `\n\n---\n\n${status.icon ? `$(${status.icon?.id}) ` : ''}` + (status.detailedTooltip ?? status.tooltip ?? ''); + } + } else { + if (status.tooltip) { + statusString += `\n\n---\n\n${status.icon ? `$(${status.icon?.id}) ` : ''}` + (status.tooltip ?? ''); + } + } if (status.hoverActions) { actions.push(...status.hoverActions); } } + actions.push({ + commandId: 'toggleDetailedInfo', + label: showDetailed ? localize('hideDetails', 'Hide Details') : localize('showDetails', 'Show Details'), + run() { + storageService.store(TerminalStorageKeys.TabsShowDetailed, (showDetailed + 1) % 2, StorageScope.APPLICATION, StorageTarget.USER); + }, + }); - const shellProcessString = getShellProcessTooltip(instance, true); - const shellIntegrationString = getShellIntegrationTooltip(instance, true); - const content = new MarkdownString(instance.title + shellProcessString + shellIntegrationString + statusString, { supportThemeIcons: true }); + const shellProcessString = getShellProcessTooltip(instance, !!showDetailed); + const content = new MarkdownString(instance.title + shellProcessString + statusString, { supportThemeIcons: true }); return { content, actions }; } -export function getShellIntegrationTooltip(instance: ITerminalInstance, markdown: boolean): string { - const shellIntegrationCapabilities: TerminalCapability[] = []; - if (instance.capabilities.has(TerminalCapability.CommandDetection)) { - shellIntegrationCapabilities.push(TerminalCapability.CommandDetection); - } - if (instance.capabilities.has(TerminalCapability.CwdDetection)) { - shellIntegrationCapabilities.push(TerminalCapability.CwdDetection); - } - let shellIntegrationString = ''; - if (shellIntegrationCapabilities.length > 0) { - shellIntegrationString += `${markdown ? '\n\n---\n\n' : '\n\n'}${localize('shellIntegration.enabled', "Shell integration activated")}`; - } else { - if (instance.shellLaunchConfig.ignoreShellIntegration) { - shellIntegrationString += `${markdown ? '\n\n---\n\n' : '\n\n'}${localize('launchFailed.exitCodeOnlyShellIntegration', "The terminal process failed to launch. Disabling shell integration with terminal.integrated.shellIntegration.enabled might help.")}`; - } else { - if (instance.usedShellIntegrationInjection) { - shellIntegrationString += `${markdown ? '\n\n---\n\n' : '\n\n'}${localize('shellIntegration.activationFailed', "Shell integration failed to activate")}`; - } - } - } - return shellIntegrationString; -} - -export function getShellProcessTooltip(instance: ITerminalInstance, markdown: boolean): string { +export function getShellProcessTooltip(instance: ITerminalInstance, showDetailed: boolean): string { const lines: string[] = []; if (instance.processId && instance.processId > 0) { @@ -59,8 +57,16 @@ export function getShellProcessTooltip(instance: ITerminalInstance, markdown: bo } if (instance.shellLaunchConfig.executable) { - let commandLine = instance.shellLaunchConfig.executable; - const args = asArray(instance.injectedArgs || instance.shellLaunchConfig.args || []).map(x => `'${x}'`).join(' '); + let commandLine = ''; + if (!showDetailed && instance.shellLaunchConfig.executable.length > 32) { + const base = basename(instance.shellLaunchConfig.executable); + const sepIndex = instance.shellLaunchConfig.executable.length - base.length - 1; + const sep = instance.shellLaunchConfig.executable.substring(sepIndex, sepIndex + 1); + commandLine += `…${sep}${base}`; + } else { + commandLine += instance.shellLaunchConfig.executable; + } + const args = asArray(instance.injectedArgs || instance.shellLaunchConfig.args || []).map(x => x.match(/\s/) ? `'${x}'` : x).join(' '); if (args) { commandLine += ` ${args}`; } @@ -68,5 +74,40 @@ export function getShellProcessTooltip(instance: ITerminalInstance, markdown: bo lines.push(localize('shellProcessTooltip.commandLine', 'Command line: {0}', commandLine)); } - return lines.length ? `${markdown ? '\n\n---\n\n' : '\n\n'}${lines.join('\n')}` : ''; + return lines.length ? `\n\n---\n\n${lines.join('\n')}` : ''; +} + +export function refreshShellIntegrationInfoStatus(instance: ITerminalInstance) { + if (!instance.xterm) { + return; + } + const cmdDetectionType = ( + instance.capabilities.get(TerminalCapability.CommandDetection)?.hasRichCommandDetection + ? localize('shellIntegration.rich', 'Rich') + : instance.capabilities.has(TerminalCapability.CommandDetection) + ? localize('shellIntegration.basic', 'Basic') + : instance.usedShellIntegrationInjection + ? localize('shellIntegration.injectionFailed', "Injection failed to activate") + : localize('shellIntegration.no', 'No') + ); + + const detailedAdditions: string[] = []; + const seenSequences = Array.from(instance.xterm.shellIntegration.seenSequences); + if (seenSequences.length > 0) { + detailedAdditions.push(`Seen sequences: ${seenSequences.map(e => `\`${e}\``).join(', ')}`); + } + const combinedString = instance.capabilities.get(TerminalCapability.CommandDetection)?.promptInputModel.getCombinedString(); + if (combinedString !== undefined) { + detailedAdditions.push(`Prompt input: \`${combinedString}\``); + } + const detailedAdditionsString = detailedAdditions.length > 0 + ? '\n\n' + detailedAdditions.map(e => `- ${e}`).join('\n') + : ''; + + instance.statusList.add({ + id: TerminalStatus.ShellIntegrationInfo, + severity: Severity.Info, + tooltip: `${localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}`, + detailedTooltip: `${localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}${detailedAdditionsString}` + }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 7e22be4d..42ff0ac1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -50,6 +50,7 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { InstanceContext, TerminalContextActionRunner } from './terminalContextMenu.js'; import { MicrotaskDelay } from '../../../../base/common/symbols.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; export class TerminalViewPane extends ViewPane { private _parentDomElement: HTMLElement | undefined; @@ -660,7 +661,8 @@ class SingleTabHoverDelegate implements IHoverDelegate { constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @IHoverService private readonly _hoverService: IHoverService, - @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService + @IStorageService private readonly _storageService: IStorageService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, ) { } @@ -675,8 +677,8 @@ class SingleTabHoverDelegate implements IHoverDelegate { if (!instance) { return; } - const hoverInfo = getInstanceHoverInfo(instance); - return this._hoverService.showHover({ + const hoverInfo = getInstanceHoverInfo(instance, this._storageService); + return this._hoverService.showInstantHover({ ...options, content: hoverInfo.content, actions: hoverInfo.actions diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts index 235a4586..5f6aec12 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts @@ -44,7 +44,7 @@ export class TerminalHover extends Disposable implements ITerminalWidget { return; } const target = new CellHoverTarget(container, this._targetOptions); - const hover = this._hoverService.showHover({ + const hover = this._hoverService.showInstantHover({ target, content: this._text, actions: this._actions, diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index e612cca0..55513eb4 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -7,7 +7,7 @@ import type { IDecoration, ITerminalAddon, Terminal } from '@xterm/xterm'; import * as dom from '../../../../../base/browser/dom.js'; import { IAction, Separator } from '../../../../../base/common/actions.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, dispose, toDisposable } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; @@ -32,7 +32,7 @@ interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposa export class DecorationAddon extends Disposable implements ITerminalAddon, IDecorationAddon { protected _terminal: Terminal | undefined; - private _capabilityDisposables: Map = new Map(); + private _capabilityDisposables: DisposableMap = this._register(new DisposableMap()); private _decorations: Map = new Map(); private _placeholderDecoration: IDecoration | undefined; private _showGutterDecorations?: boolean; @@ -77,14 +77,6 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco this._register(lifecycleService.onWillShutdown(() => this._disposeAllDecorations())); } - private _removeCapabilityDisposables(c: TerminalCapability): void { - const disposables = this._capabilityDisposables.get(c); - if (disposables) { - dispose(disposables); - } - this._capabilityDisposables.delete(c); - } - private _createCapabilityDisposables(c: TerminalCapability): void { const store = new DisposableStore(); const capability = this._capabilities.get(c); @@ -106,6 +98,10 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco this._capabilityDisposables.set(c, store); } + private _removeCapabilityDisposables(c: TerminalCapability): void { + this._capabilityDisposables.deleteAndDispose(c); + } + registerMarkDecoration(mark: IMarkProperties): IDecoration | undefined { if (!this._terminal || (!this._showGutterDecorations && !this._showOverviewRulerDecorations)) { return undefined; @@ -212,11 +208,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco } private _getCommandDetectionListeners(capability: ICommandDetectionCapability): IDisposable[] { - if (this._capabilityDisposables.has(TerminalCapability.CommandDetection)) { - const disposables = this._capabilityDisposables.get(TerminalCapability.CommandDetection)!; - dispose(disposables); - this._capabilityDisposables.delete(capability.type); - } + this._removeCapabilityDisposables(TerminalCapability.CommandDetection); + const commandDetectionListeners = []; // Command started if (capability.executingCommandObject?.marker) { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index b1a9006b..b835c0b5 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -256,7 +256,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach } })); - this._register(this._themeService.onDidColorThemeChange(e => this._updateTheme(e.theme))); + this._register(this._themeService.onDidColorThemeChange(theme => this._updateTheme(theme))); this._register(this._logService.onDidChangeLogLevel(e => this.raw.options.logLevel = vscodeToXtermLogLevel(e))); // Refire events diff --git a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts index 8def96e4..5aeedf77 100644 --- a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts @@ -24,6 +24,7 @@ import { IPtyHostProcessReplayEvent } from '../../../../../platform/terminal/com import { ISerializableEnvironmentDescriptionMap as ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from '../../../../../platform/terminal/common/environmentVariable.js'; import type * as performance from '../../../../../base/common/performance.js'; import { RemoteTerminalChannelEvent, RemoteTerminalChannelRequest } from './terminal.js'; +import { ConfigurationResolverExpression } from '../../../../services/configurationResolver/common/configurationResolverExpression.js'; export const REMOTE_TERMINAL_CHANNEL_NAME = 'remoteterminal'; @@ -133,20 +134,15 @@ export class RemoteTerminalChannelClient implements IPtyHostController { // But then we will keep only some variables, since the rest need to be resolved on the remote side const resolvedVariables = Object.create(null); const lastActiveWorkspace = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) ?? undefined : undefined; - let allResolvedVariables: Map | undefined = undefined; + const expr = ConfigurationResolverExpression.parse({ shellLaunchConfig, configuration }); try { - allResolvedVariables = (await this._resolverService.resolveAnyMap(lastActiveWorkspace, { - shellLaunchConfig, - configuration - })).resolvedVariables; + await this._resolverService.resolveAsync(lastActiveWorkspace, expr); } catch (err) { this._logService.error(err); } - if (allResolvedVariables) { - for (const [name, value] of allResolvedVariables.entries()) { - if (/^config:/.test(name) || name === 'selectedText' || name === 'lineNumber') { - resolvedVariables[name] = value; - } + for (const [{ inner }, resolved] of expr.resolved()) { + if (/^config:/.test(inner) || inner === 'selectedText' || inner === 'lineNumber') { + resolvedVariables[inner] = resolved.value; } } diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index b5f2e1f5..727dcb02 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -198,6 +198,9 @@ if [ "$__vsc_stable" = "0" ]; then builtin printf "\e]633;P;ContinuationPrompt=$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')\a" fi +# Report this shell supports rich command detection +builtin printf '\e]633;P;HasRichCommandDetection=True\a' + __vsc_report_prompt() { # Expand the original PS1 similarly to how bash would normally # See https://stackoverflow.com/a/37137981 for technique @@ -242,8 +245,12 @@ __updateEnvCacheAA() { __trackMissingEnvVarsAA() { if [ "$use_associative_array" = 1 ]; then declare -A currentEnvMap - while IFS='=' read -r key value; do - currentEnvMap["$key"]="$value" + while IFS= read -r line; do + if [[ "$line" == *"="* ]]; then + local key="${line%%=*}" + local value="${line#*=}" + currentEnvMap["$key"]="$value" + fi done < <(env) for key in "${!vsc_aa_env[@]}"; do @@ -309,34 +316,52 @@ __vsc_update_env() { if [ "$use_associative_array" = 1 ]; then if [ ${#vsc_aa_env[@]} -eq 0 ]; then # Associative array is empty, do not diff, just add - while IFS='=' read -r key value; do - vsc_aa_env["$key"]="$value" - builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" - done < <(env) + # Use null byte instead of a newline to support multi-line values (e.g. PS1 values) + while IFS= read -r -d $'\0' line; do + if [[ "$line" == *"="* ]]; then + # %% removes longest match of =* Ensure we get everything before first equal sign. + local key="${line%%=*}" + # # removes shortest match of *= Ensure we get everything after first equal sign. Preserving additional equal signs. + local value="${line#*=}" + vsc_aa_env["$key"]="$value" + builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" + fi + done < <(env -0) # env command with null bytes as separator instead of newlines else # Diff approach for associative array - while IFS='=' read -r key value; do - __updateEnvCacheAA "$key" "$value" - done < <(env) + while IFS= read -r -d $'\0' line; do + if [[ "$line" == *"="* ]]; then + local key="${line%%=*}" + local value="${line#*=}" + __updateEnvCacheAA "$key" "$value" + fi + done < <(env -0) __trackMissingEnvVarsAA fi else if [[ -z ${vsc_env_keys[@]} ]] && [[ -z ${vsc_env_values[@]} ]]; then - # Non associative arrays are both empty, do not diff, just add - while IFS='=' read -r key value; do - vsc_env_keys+=("$key") - vsc_env_values+=("$value") - builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" - done < <(env) + # Non associative arrays are both empty, do not diff, just add + while IFS= read -r -d $'\0' line; do + if [[ "$line" == *"="* ]]; then + local key="${line%%=*}" + local value="${line#*=}" + vsc_env_keys+=("$key") + vsc_env_values+=("$value") + builtin printf '\e]633;EnvSingleEntry;%s;%s;%s\a' "$key" "$(__vsc_escape_value "$value")" "$__vsc_nonce" + fi + done < <(env -0) else # Diff approach for non-associative arrays - while IFS='=' read -r key value; do - __updateEnvCache "$key" "$value" - done < <(env) + while IFS= read -r -d $'\0' line; do + if [[ "$line" == *"="* ]]; then + local key="${line%%=*}" + local value="${line#*=}" + __updateEnvCache "$key" "$value" + fi + done < <(env -0) __trackMissingEnvVars fi - fi builtin printf '\e]633;EnvSingleEnd;%s;\a' $__vsc_nonce fi diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index 43738d95..62b93ad9 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -80,18 +80,19 @@ __vsc_escape_value() { builtin emulate -L zsh # Process text byte by byte, not by codepoint. - builtin local LC_ALL=C str="$1" i byte token out='' + builtin local LC_ALL=C str="$1" i byte token out='' val for (( i = 0; i < ${#str}; ++i )); do + # Escape backslashes, semi-colons specially, then special ASCII chars below space (0x20). byte="${str:$i:1}" - - # Escape backslashes, semi-colons and newlines - if [ "$byte" = "\\" ]; then + val=$(printf "%d" "'$byte") + if (( val < 31 )); then + # For control characters, use hex encoding + token=$(printf "\\\\x%02x" "'$byte") + elif [ "$byte" = "\\" ]; then token="\\\\" elif [ "$byte" = ";" ]; then token="\\x3b" - elif [ "$byte" = $'\n' ]; then - token="\x0a" else token="$byte" fi @@ -112,6 +113,11 @@ unset VSCODE_NONCE __vscode_shell_env_reporting="$VSCODE_SHELL_ENV_REPORTING" unset VSCODE_SHELL_ENV_REPORTING +builtin printf "\e]633;P;ContinuationPrompt=%s\a" "$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')" + +# Report this shell supports rich command detection +builtin printf '\e]633;P;HasRichCommandDetection=True\a' + __vsc_prompt_start() { builtin printf '\e]633;A\a' } diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish index e67701b0..f486869b 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish @@ -35,6 +35,9 @@ set -e VSCODE_PATH_PREFIX set -g vsc_env_keys set -g vsc_env_values +# Tracks if the shell has been initialized, this prevents +set -g vsc_initialized 0 + set -g __vsc_applied_env_vars 0 function __vsc_apply_env_vars if test $__vsc_applied_env_vars -eq 1; @@ -109,6 +112,11 @@ end # Sent when a command line is cleared or reset, but no command was run. # Marks the cleared line with neither success nor failure. function __vsc_cmd_clear --on-event fish_cancel + if test $vsc_initialized -eq 0; + return + end + __vsc_esc E "" $__vsc_nonce + __vsc_esc C __vsc_esc D end @@ -170,6 +178,7 @@ function __vsc_fish_prompt_start # evaluated __vsc_apply_env_vars __vsc_esc A + set -g vsc_initialized 1 end # Sent at the end of the prompt. @@ -207,4 +216,8 @@ function __init_vscode_shell_integration end end end + +# Report this shell supports rich command detection +__vsc_esc P HasRichCommandDetection=True + __preserve_fish_prompt diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index de5e863b..4f4f5b72 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -16,6 +16,7 @@ if ($ExecutionContext.SessionState.LanguageMode -ne "FullLanguage") { $Global:__VSCodeOriginalPrompt = $function:Prompt $Global:__LastHistoryId = -1 +$Global:__VSCodeIsInExecution = $false # Store the nonce in script scope and unset the global $Nonce = $env:VSCODE_NONCE @@ -73,8 +74,10 @@ function Global:Prompt() { Set-StrictMode -Off $LastHistoryEntry = Get-History -Count 1 $Result = "" - # Skip finishing the command if the first command has not yet started - if ($Global:__LastHistoryId -ne -1) { + # Skip finishing the command if the first command has not yet started or an execution has not + # yet begun + if ($Global:__LastHistoryId -ne -1 -and $Global:__VSCodeIsInExecution -eq $true) { + $Global:__VSCodeIsInExecution = $false if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) { # Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command) $Result += "$([char]0x1b)]633;D`a" @@ -93,7 +96,7 @@ function Global:Prompt() { $Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a" } # Send current environment variables as JSON - # OSC 633 ; Env ; ; + # OSC 633 ; EnvJson ; ; if ($__vscode_shell_env_reporting -eq "1") { $envMap = @{} Get-ChildItem Env: | ForEach-Object { $envMap[$_.Name] = $_.Value } @@ -124,12 +127,14 @@ function Global:Prompt() { # Only send the command executed sequence when PSReadLine is loaded, if not shell integration should # still work thanks to the command line sequence if (Get-Module -Name PSReadLine) { + [Console]::Write("$([char]0x1b)]633;P;HasRichCommandDetection=True`a") $__VSCodeOriginalPSConsoleHostReadLine = $function:PSConsoleHostReadLine function Global:PSConsoleHostReadLine { $CommandLine = $__VSCodeOriginalPSConsoleHostReadLine.Invoke() + $Global:__VSCodeIsInExecution = $true # Command line - # OSC 633 ; E ; ; ST + # OSC 633 ; E [; [; ]] ST $Result = "$([char]0x1b)]633;E;" $Result += $(__VSCode-Escape-Value $CommandLine) # Only send the nonce if the OS is not Windows 10 as it seems to echo to the terminal @@ -148,6 +153,12 @@ if (Get-Module -Name PSReadLine) { $CommandLine } + + # Set ContinuationPrompt property + $ContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt + if ($ContinuationPrompt) { + [Console]::Write("$([char]0x1b)]633;P;ContinuationPrompt=$(__VSCode-Escape-Value $ContinuationPrompt)`a") + } } # Set IsWindows property @@ -159,14 +170,6 @@ else { [Console]::Write("$([char]0x1b)]633;P;IsWindows=$IsWindows`a") } -# Set ContinuationPrompt property -if ($isStable -eq "0") { - $ContinuationPrompt = (Get-PSReadLineOption).ContinuationPrompt - if ($ContinuationPrompt) { - [Console]::Write("$([char]0x1b)]633;P;ContinuationPrompt=$(__VSCode-Escape-Value $ContinuationPrompt)`a") - } -} - # Set always on key handlers which map to default VS Code keybindings function Set-MappedKeyHandler { param ([string[]] $Chord, [string[]]$Sequence) diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 2a717f4a..3683a60a 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -370,6 +370,10 @@ export interface ITerminalStatus { * What to show for this status in the terminal's hover. */ tooltip?: string | undefined; + /** + * What to show for this status in the terminal's hover when details are toggled. + */ + detailedTooltip?: string | undefined; /** * Actions to expose on hover. */ diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 22c96907..28aa98cc 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; +import { IJSONSchemaSnippet } from '../../../../base/common/jsonSchema.js'; import { isMacintosh, isWindows } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; @@ -16,19 +17,19 @@ import { terminalContribConfiguration, TerminalContribSettingId } from '../termi import { DEFAULT_COMMANDS_TO_SKIP_SHELL, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, MAXIMUM_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, SUGGESTIONS_FONT_WEIGHT } from './terminal.js'; const terminalDescriptors = '\n- ' + [ - '`\${cwd}`: ' + localize("cwd", "the terminal's current working directory"), + '`\${cwd}`: ' + localize("cwd", "the terminal's current working directory."), '`\${cwdFolder}`: ' + localize('cwdFolder', "the terminal's current working directory, displayed for multi-root workspaces or in a single root workspace when the value differs from the initial working directory. On Windows, this will only be displayed when shell integration is enabled."), - '`\${workspaceFolder}`: ' + localize('workspaceFolder', "the workspace in which the terminal was launched"), - '`\${workspaceFolderName}`: ' + localize('workspaceFolderName', "the `name` of the workspace in which the terminal was launched"), - '`\${local}`: ' + localize('local', "indicates a local terminal in a remote workspace"), - '`\${process}`: ' + localize('process', "the name of the terminal process"), - '`\${progress}`: ' + localize('progress', "the progress state as reported by the `OSC 9;4` sequence"), - '`\${separator}`: ' + localize('separator', "a conditional separator {0} that only shows when surrounded by variables with values or static text.", '(` - `)'), - '`\${sequence}`: ' + localize('sequence', "the name provided to the terminal by the process"), - '`\${task}`: ' + localize('task', "indicates this terminal is associated with a task"), - '`\${shellType}`: ' + localize('shellType', "the detected shell type"), - '`\${shellCommand}`: ' + localize('shellCommand', "the command being executed according to shell integration. This also requires high confidence in the detected command line which may not work in some prompt frameworks."), - '`\${shellPromptInput}`: ' + localize('shellPromptInput', "the shell's full prompt input according to shell integration"), + '`\${workspaceFolder}`: ' + localize('workspaceFolder', "the workspace in which the terminal was launched."), + '`\${workspaceFolderName}`: ' + localize('workspaceFolderName', "the `name` of the workspace in which the terminal was launched."), + '`\${local}`: ' + localize('local', "indicates a local terminal in a remote workspace."), + '`\${process}`: ' + localize('process', "the name of the terminal process."), + '`\${progress}`: ' + localize('progress', "the progress state as reported by the `OSC 9;4` sequence."), + '`\${separator}`: ' + localize('separator', "a conditional separator {0} that only shows when it's surrounded by variables with values or static text.", '(` - `)'), + '`\${sequence}`: ' + localize('sequence', "the name provided to the terminal by the process."), + '`\${task}`: ' + localize('task', "indicates this terminal is associated with a task."), + '`\${shellType}`: ' + localize('shellType', "the detected shell type."), + '`\${shellCommand}`: ' + localize('shellCommand', "the command being executed according to shell integration. This also requires high confidence in the detected command line, which may not work in some prompt frameworks."), + '`\${shellPromptInput}`: ' + localize('shellPromptInput', "the shell's full prompt input according to shell integration."), ].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations let terminalTitle = localize('terminalTitle', "Controls the terminal title. Variables are substituted based on the context:"); @@ -174,7 +175,7 @@ const terminalConfiguration: IConfigurationNode = { }, [TerminalSettingId.FontFamily]: { markdownDescription: localize('terminal.integrated.fontFamily', "Controls the font family of the terminal. Defaults to {0}'s value.", '`#editor.fontFamily#`'), - type: 'string' + type: 'string', }, [TerminalSettingId.FontLigaturesEnabled]: { markdownDescription: localize('terminal.integrated.fontLigatures.enabled', "Controls whether font ligatures are enabled in the terminal. Ligatures will only work if the configured {0} supports them.", `\`#${TerminalSettingId.FontFamily}#\``), @@ -643,9 +644,13 @@ const terminalConfiguration: IConfigurationNode = { } }; -export function registerTerminalConfiguration() { +export async function registerTerminalConfiguration(getFontSnippets: () => Promise) { const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration(terminalConfiguration); + const fontsSnippets = await getFontSnippets(); + if (terminalConfiguration.properties) { + terminalConfiguration.properties[TerminalSettingId.FontFamily].defaultSnippets = fontsSnippets; + } } Registry.as(WorkbenchExtensions.ConfigurationMigration) diff --git a/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts b/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts index 6a495eca..915da5be 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts @@ -7,6 +7,7 @@ export const enum TerminalStorageKeys { SuggestedRendererType = 'terminal.integrated.suggestedRendererType', TabsListWidthHorizontal = 'tabs-list-width-horizontal', TabsListWidthVertical = 'tabs-list-width-vertical', + TabsShowDetailed = 'terminal.integrated.tabs.showDetailed', DeprecatedEnvironmentVariableCollections = 'terminal.integrated.environmentVariableCollections', EnvironmentVariableCollections = 'terminal.integrated.environmentVariableCollectionsV2', TerminalBufferState = 'terminal.integrated.bufferState', diff --git a/src/vs/workbench/contrib/terminal/common/terminalStrings.ts b/src/vs/workbench/contrib/terminal/common/terminalStrings.ts index 101fb7f7..ed00de1f 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalStrings.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalStrings.ts @@ -36,9 +36,9 @@ export const terminalStrings = { rename: localize2('workbench.action.terminal.rename', "Rename..."), toggleSizeToContentWidth: localize2('workbench.action.terminal.sizeToContentWidthInstance', "Toggle Size to Content Width"), focusHover: localize2('workbench.action.terminal.focusHover', "Focus Hover"), - sendSequence: localize2('workbench.action.terminal.sendSequence', "Send Custom Sequence To Terminal"), + sendSequence: localize2('workbench.action.terminal.sendSequence', "Send Custom Sequence to Terminal"), newWithCwd: localize2('workbench.action.terminal.newWithCwd', "Create New Terminal Starting in a Custom Working Directory"), renameWithArgs: localize2('workbench.action.terminal.renameWithArg', "Rename the Currently Active Terminal"), - scrollToPreviousCommand: localize2('workbench.action.terminal.scrollToPreviousCommand', "Scroll To Previous Command"), - scrollToNextCommand: localize2('workbench.action.terminal.scrollToNextCommand', "Scroll To Next Command") + scrollToPreviousCommand: localize2('workbench.action.terminal.scrollToPreviousCommand', "Scroll to Previous Command"), + scrollToNextCommand: localize2('workbench.action.terminal.scrollToNextCommand', "Scroll to Next Command") }; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts index f29ac9f6..4bef1ee6 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts @@ -23,13 +23,14 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../../pla import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { ITerminalCapabilityStore, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { ChatAgentLocation, IChatAgent, IChatAgentService } from '../../../chat/common/chatAgents.js'; +import { IChatAgent, IChatAgentService } from '../../../chat/common/chatAgents.js'; import { IDetachedTerminalInstance, ITerminalContribution, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService, IXtermTerminal } from '../../../terminal/browser/terminal.js'; import { registerTerminalContribution, type IDetachedCompatibleTerminalContributionContext, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; import { TerminalInstance } from '../../../terminal/browser/terminalInstance.js'; import { TerminalInitialHintSettingId } from '../common/terminalInitialHintConfiguration.js'; import './media/terminalInitialHint.css'; import { TerminalChatCommandId } from './terminalChat.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; const $ = dom.$; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 782aca3e..5e19dbde 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -9,8 +9,9 @@ import { localize2 } from '../../../../../nls.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IChatWidgetService } from '../../../chat/browser/chat.js'; -import { ChatAgentLocation } from '../../../chat/common/chatAgents.js'; +import { ChatContextKeys } from '../../../chat/common/chatContextKeys.js'; import { IChatService } from '../../../chat/common/chatService.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; import { AbstractInline1ChatAction } from '../../../inlineChat/browser/inlineChatActions.js'; import { isDetachedTerminalInstance } from '../../../terminal/browser/terminal.js'; import { registerActiveXtermAction } from '../../../terminal/browser/terminalActions.js'; @@ -30,6 +31,7 @@ registerActiveXtermAction({ }, f1: true, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.hasChatAgent ), @@ -74,7 +76,10 @@ registerActiveXtermAction({ }], icon: Codicon.close, f1: true, - precondition: TerminalChatContextKeys.visible, + precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, + TerminalChatContextKeys.visible, + ), run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { return; @@ -90,6 +95,7 @@ registerActiveXtermAction({ shortTitle: localize2('run', 'Run'), category: AbstractInline1ChatAction.category, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), TerminalChatContextKeys.responseContainsCodeBlock, @@ -122,6 +128,7 @@ registerActiveXtermAction({ shortTitle: localize2('runFirst', 'Run First'), category: AbstractInline1ChatAction.category, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), TerminalChatContextKeys.responseContainsMultipleCodeBlocks @@ -154,6 +161,7 @@ registerActiveXtermAction({ category: AbstractInline1ChatAction.category, icon: Codicon.insert, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), TerminalChatContextKeys.responseContainsCodeBlock, @@ -186,6 +194,7 @@ registerActiveXtermAction({ shortTitle: localize2('insertFirst', 'Insert First'), category: AbstractInline1ChatAction.category, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), TerminalChatContextKeys.responseContainsMultipleCodeBlocks @@ -218,6 +227,7 @@ registerActiveXtermAction({ icon: Codicon.refresh, category: AbstractInline1ChatAction.category, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), ), @@ -259,6 +269,7 @@ registerActiveXtermAction({ title: localize2('viewInChat', 'View in Chat'), category: AbstractInline1ChatAction.category, precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalChatContextKeys.requestActive.negate(), ), diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts index 868a887e..cbdeafdd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from '../../../../../base/common/event.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IChatAgentService, ChatAgentLocation } from '../../../chat/common/chatAgents.js'; +import { IChatAgentService } from '../../../chat/common/chatAgents.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; import { TerminalChatContextKeys } from './terminalChat.js'; - export class TerminalChatEnabler { static Id = 'terminalChat.enabler'; @@ -22,7 +23,7 @@ export class TerminalChatEnabler { @IContextKeyService contextKeyService: IContextKeyService, ) { this._ctxHasProvider = TerminalChatContextKeys.hasChatAgent.bindTo(contextKeyService); - this._store.add(chatAgentService.onDidChangeAgents(() => { + this._store.add(Event.runAndSubscribe(chatAgentService.onDidChangeAgents, () => { const hasTerminalAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Terminal)); this._ctxHasProvider.set(hasTerminalAgent); })); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index b2be6193..3a8cde6a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -5,29 +5,30 @@ import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { Dimension, getActiveWindow, IFocusTracker, trackFocus } from '../../../../../base/browser/dom.js'; +import { CancelablePromise, createCancelablePromise, DeferredPromise } from '../../../../../base/common/async.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, observableValue, type IObservable } from '../../../../../base/common/observable.js'; import { MicrotaskDelay } from '../../../../../base/common/symbols.js'; -import './media/terminalChatWidget.css'; import { localize } from '../../../../../nls.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ChatAgentLocation } from '../../../chat/common/chatAgents.js'; -import { InlineChatWidget } from '../../../inlineChat/browser/inlineChatWidget.js'; -import { ITerminalInstance, type IXtermTerminal } from '../../../terminal/browser/terminal.js'; -import { MENU_TERMINAL_CHAT_WIDGET_INPUT_SIDE_TOOLBAR, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from './terminalChat.js'; -import { TerminalStickyScrollContribution } from '../../stickyScroll/browser/terminalStickyScrollContribution.js'; -import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js'; -import { CancelablePromise, createCancelablePromise, DeferredPromise } from '../../../../../base/common/async.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { IChatAcceptInputOptions, showChatView } from '../../../chat/browser/chat.js'; -import { ChatModel, IChatResponseModel, isCellTextEditOperation } from '../../../chat/common/chatModel.js'; -import { IChatService, IChatProgress } from '../../../chat/common/chatService.js'; -import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { MenuId } from '../../../../../platform/actions/common/actions.js'; import type { IChatViewState } from '../../../chat/browser/chatWidget.js'; -import { autorun, observableValue, type IObservable } from '../../../../../base/common/observable.js'; +import { IChatAgentService } from '../../../chat/common/chatAgents.js'; +import { ChatModel, IChatResponseModel, isCellTextEditOperation } from '../../../chat/common/chatModel.js'; +import { IChatProgress, IChatService } from '../../../chat/common/chatService.js'; +import { ChatAgentLocation } from '../../../chat/common/constants.js'; +import { InlineChatWidget } from '../../../inlineChat/browser/inlineChatWidget.js'; +import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js'; +import { ITerminalInstance, type IXtermTerminal } from '../../../terminal/browser/terminal.js'; +import { TerminalStickyScrollContribution } from '../../stickyScroll/browser/terminalStickyScrollContribution.js'; +import './media/terminalChatWidget.css'; +import { MENU_TERMINAL_CHAT_WIDGET_INPUT_SIDE_TOOLBAR, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from './terminalChat.js'; const enum Constants { HorizontalMargin = 10, @@ -100,6 +101,7 @@ export class TerminalChatWidget extends Disposable { @IStorageService private readonly _storageService: IStorageService, @IViewsService private readonly _viewsService: IViewsService, @IInstantiationService instantiationService: IInstantiationService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService, ) { super(); @@ -225,7 +227,8 @@ export class TerminalChatWidget extends Disposable { } private _resetPlaceholder() { - this.inlineChatWidget.placeholder = this._model.value?.welcomeMessage?.title ?? localize('askAI', 'Ask AI'); + const defaultAgent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Terminal); + this.inlineChatWidget.placeholder = defaultAgent?.description ?? localize('askAI', 'Ask AI'); } async reveal(viewState?: IChatViewState): Promise { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts index d69b250f..ddf420a3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/test/browser/terminalInitialHint.test.ts @@ -13,8 +13,9 @@ import { getActiveDocument } from '../../../../../../base/browser/dom.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { strictEqual } from 'assert'; import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; -import { ChatAgentLocation, IChatAgent } from '../../../../chat/common/chatAgents.js'; +import { IChatAgent } from '../../../../chat/common/chatAgents.js'; import { importAMDNodeModule } from '../../../../../../amdX.js'; +import { ChatAgentLocation } from '../../../../chat/common/constants.js'; suite('Terminal Initial Hint Addon', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/media/terminalSymbolIcons.css b/src/vs/workbench/contrib/terminalContrib/suggest/browser/media/terminalSymbolIcons.css index 6d918851..515978e5 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/media/terminalSymbolIcons.css +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/media/terminalSymbolIcons.css @@ -3,7 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor .codicon.codicon-symbol-method-arrow, -.monaco-workbench .codicon.codicon-symbol-method-arrow { color: var(--vscode-terminalSymbolIcon-aliasForeground); } -.monaco-editor .codicon.codicon-flag, -.monaco-workbench .codicon.codicon-flag { color: var(--vscode-terminalSymbolIcon-flagForeground); } +.monaco-editor .codicon.codicon-terminal-symbol-alias, +.monaco-workbench .codicon.codicon-terminal-symbol-alias { color: var(--vscode-terminalSymbolIcon-aliasForeground); } + +.monaco-editor .codicon.codicon-terminal-symbol-flag, +.monaco-workbench .codicon.codicon-terminal-symbol-flag { color: var(--vscode-terminalSymbolIcon-flagForeground); } + +.monaco-editor .codicon.codicon-terminal-symbol-option-value, +.monaco-workbench .codicon.codicon-terminal-symbol-option-value { color: var(--vscode-terminalSymbolIcon-optionValueForeground); } + +.monaco-editor .codicon.codicon-terminal-symbol-method, +.monaco-workbench .codicon.codicon-terminal-symbol-method { color: var(--vscode-terminalSymbolIcon-methodForeground); } + +.monaco-editor .codicon.codicon-terminal-symbol-argument, +.monaco-workbench .codicon.codicon-terminal-symbol-argument { color: var(--vscode-terminalSymbolIcon-argumentForeground); } + +.monaco-editor .codicon.codicon-terminal-symbol-option, +.monaco-workbench .codicon.codicon-terminal-symbol-option { color: var(--vscode-terminalSymbolIcon-optionForeground); } + +.monaco-editor .codicon.codicon-terminal-symbol-inline-suggestion, +.monaco-workbench .codicon.codicon-terminal-symbol-inline-suggestion { color: var(--vscode-terminalSymbolIcon-inlineSuggestionForeground); } + +.monaco-editor .codicon.codicon-terminal-symbol-file, +.monaco-workbench .codicon.codicon-terminal-symbol-file { color: var(--vscode-terminalSymbolIcon-fileForeground); } + +.monaco-editor .codicon.codicon-terminal-symbol-folder, +.monaco-workbench .codicon.codicon-terminal-symbol-folder { color: var(--vscode-terminalSymbolIcon-folderForeground); } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts index 9928faaf..1b3429c3 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/pwshCompletionProviderAddon.ts @@ -9,23 +9,21 @@ import type { ITerminalAddon, Terminal } from '@xterm/xterm'; import { Event, Emitter } from '../../../../../base/common/event.js'; import { ShellIntegrationOscPs } from '../../../../../platform/terminal/common/xterm/shellIntegrationAddon.js'; import * as dom from '../../../../../base/browser/dom.js'; -import { IPromptInputModel, IPromptInputModelState } from '../../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js'; +import { IPromptInputModel } from '../../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js'; import { sep } from '../../../../../base/common/path.js'; import { SuggestAddon } from './terminalSuggestAddon.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { ITerminalSuggestConfiguration, terminalSuggestConfigSection, TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration.js'; +import { ITerminalSuggestConfiguration, terminalSuggestConfigSection } from '../common/terminalSuggestConfiguration.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { GeneralShellType } from '../../../../../platform/terminal/common/terminal.js'; import { ITerminalCapabilityStore, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { DeferredPromise } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { ITerminalCompletion, TerminalCompletionItemKind } from './terminalCompletionItem.js'; export const enum VSCodeSuggestOscPt { Completions = 'Completions', - CompletionsPwshCommands = 'CompletionsPwshCommands', } export type CompressedPwshCompletion = [ @@ -42,13 +40,8 @@ export type PwshCompletion = { CustomIcon?: string; }; -const enum Constants { - CachedPwshCommandsStorageKey = 'terminal.suggest.pwshCommands' -} - const enum RequestCompletionsSequence { Contextual = '\x1b[24~e', // F12,e - Global = '\x1b[24~f', // F12,f } export class PwshCompletionProviderAddon extends Disposable implements ITerminalAddon, ITerminalCompletionProvider { @@ -56,13 +49,11 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal triggerCharacters?: string[] | undefined; isBuiltin?: boolean = true; static readonly ID = 'pwsh-shell-integration'; - static cachedPwshCommands: Set; readonly shellTypes = [GeneralShellType.PowerShell]; private _lastUserDataTimestamp: number = 0; private _terminal?: Terminal; private _mostRecentCompletion?: ITerminalCompletion; private _promptInputModel?: IPromptInputModel; - private _currentPromptInputState?: IPromptInputModelState; private _enableWidget: boolean = true; isPasting: boolean = false; private _completionsDeferred: DeferredPromise | null = null; @@ -73,10 +64,8 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal readonly onDidRequestSendText = this._onDidRequestSendText.event; constructor( - providedPwshCommands: Set | undefined, capabilities: ITerminalCapabilityStore, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IStorageService private readonly _storageService: IStorageService ) { super(); this._register(Event.runAndSubscribe(Event.any( @@ -92,29 +81,6 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal this._promptInputModel = undefined; } })); - PwshCompletionProviderAddon.cachedPwshCommands = providedPwshCommands || new Set(); - - // Attempt to load cached pwsh commands if not already loaded - if (PwshCompletionProviderAddon.cachedPwshCommands.size === 0) { - const config = this._storageService.get(Constants.CachedPwshCommandsStorageKey, StorageScope.APPLICATION, undefined); - if (config !== undefined) { - const completions = JSON.parse(config); - for (const c of completions) { - PwshCompletionProviderAddon.cachedPwshCommands.add(c); - } - } - } - - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalSuggestSettingId.Enabled)) { - this.clearSuggestCache(); - } - })); - } - - clearSuggestCache(): void { - PwshCompletionProviderAddon.cachedPwshCommands.clear(); - this._storageService.remove(Constants.CachedPwshCommandsStorageKey, StorageScope.APPLICATION); } activate(xterm: Terminal): void { @@ -143,8 +109,6 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal case VSCodeSuggestOscPt.Completions: this._handleCompletionsSequence(this._terminal, data, command, args); return true; - case VSCodeSuggestOscPt.CompletionsPwshCommands: - return this._handleCompletionsPwshCommandsSequence(this._terminal, data, command, args); } // Unrecognized sequence @@ -175,38 +139,14 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal let replacementIndex = 0; let replacementLength = this._promptInputModel.cursorIndex; - this._currentPromptInputState = { - value: this._promptInputModel.value, - prefix: this._promptInputModel.prefix, - suffix: this._promptInputModel.suffix, - cursorIndex: this._promptInputModel.cursorIndex, - ghostTextIndex: this._promptInputModel.ghostTextIndex - }; - - let leadingLineContent = this._currentPromptInputState.prefix.substring(replacementIndex, replacementIndex + replacementLength); - - const firstChar = leadingLineContent.length === 0 ? '' : leadingLineContent[0]; - const isGlobalCommand = !leadingLineContent.includes(' ') && firstChar !== '['; - // This is a TabExpansion2 result - if (!isGlobalCommand) { - replacementIndex = parseInt(args[0]); - replacementLength = parseInt(args[1]); - leadingLineContent = this._promptInputModel.prefix; - } + replacementIndex = parseInt(args[0]); + replacementLength = parseInt(args[1]); + const payload = data.slice(command.length + args[0].length + args[1].length + args[2].length + 4/*semi-colons*/); const rawCompletions: PwshCompletion | PwshCompletion[] | CompressedPwshCompletion[] | CompressedPwshCompletion = args.length === 0 || payload.length === 0 ? undefined : JSON.parse(payload); const completions = parseCompletionsFromShell(rawCompletions, replacementIndex, replacementLength); - // This is a global command, add cached commands list to completions - if (isGlobalCommand) { - for (const c of PwshCompletionProviderAddon.cachedPwshCommands) { - c.replacementIndex = replacementIndex; - c.replacementLength = replacementLength; - completions.push(c); - } - } - if (this._mostRecentCompletion?.kind === TerminalCompletionItemKind.Folder && completions.every(c => c.kind === TerminalCompletionItemKind.Folder)) { completions.push(this._mostRecentCompletion); } @@ -214,22 +154,6 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal this._resolveCompletions(completions); } - private async _handleCompletionsPwshCommandsSequence(terminal: Terminal, data: string, command: string, args: string[]): Promise { - const type = args[0]; - const rawCompletions: PwshCompletion | PwshCompletion[] | CompressedPwshCompletion[] | CompressedPwshCompletion = JSON.parse(data.slice(command.length + type.length + 2/*semi-colons*/)); - const completions = parseCompletionsFromShell(rawCompletions, 0, 0); - - const set = PwshCompletionProviderAddon.cachedPwshCommands; - set.clear(); - for (const c of completions) { - set.add(c); - } - - this._storageService.store(Constants.CachedPwshCommandsStorageKey, JSON.stringify(Array.from(set.values())), StorageScope.APPLICATION, StorageTarget.MACHINE); - - return true; - } - private _resolveCompletions(result: ITerminalCompletion[] | undefined) { if (!this._completionsDeferred) { return; @@ -251,11 +175,6 @@ export class PwshCompletionProviderAddon extends Disposable implements ITerminal return Promise.resolve(undefined); } - // Request global pwsh completions if there are none cached - if (PwshCompletionProviderAddon.cachedPwshCommands.size === 0) { - this._onDidRequestSendText.fire(RequestCompletionsSequence.Global); - } - // Ensure that a key has been pressed since the last accepted completion in order to prevent // completions being requested again right after accepting a completion if (this._lastUserDataTimestamp > SuggestAddon.lastAcceptedCompletionTimestamp) { diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts index 51ca3fed..ca79d22a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminal.suggest.contribution.ts @@ -17,7 +17,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { GeneralShellType, TerminalLocation } from '../../../../../platform/terminal/common/terminal.js'; import { ITerminalContribution, ITerminalInstance, IXtermTerminal } from '../../../terminal/browser/terminal.js'; -import { registerActiveInstanceAction } from '../../../terminal/browser/terminalActions.js'; +import { registerActiveInstanceAction, registerTerminalAction } from '../../../terminal/browser/terminalActions.js'; import { registerTerminalContribution, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; import { TerminalContextKeys } from '../../../terminal/common/terminalContextKey.js'; import { TerminalSuggestCommandId } from '../common/terminal.suggest.js'; @@ -93,8 +93,12 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo } private async _loadPwshCompletionAddon(xterm: RawXtermTerminal): Promise { - // Disable when shell type is not powershell - if (this._ctx.instance.shellType !== GeneralShellType.PowerShell) { + // Disable when shell type is not powershell. A naive check is done for Windows PowerShell + // as we don't differentiate it in shellType + if ( + this._ctx.instance.shellType !== GeneralShellType.PowerShell || + this._ctx.instance.shellLaunchConfig.executable?.endsWith('WindowsPowerShell\\v1.0\\powershell.exe') + ) { this._pwshAddon.clear(); return; } @@ -106,11 +110,10 @@ class TerminalSuggestContribution extends DisposableStore implements ITerminalCo return; } - const pwshCompletionProviderAddon = this._pwshAddon.value = this._instantiationService.createInstance(PwshCompletionProviderAddon, undefined, this._ctx.instance.capabilities); + const pwshCompletionProviderAddon = this._pwshAddon.value = this._instantiationService.createInstance(PwshCompletionProviderAddon, this._ctx.instance.capabilities); xterm.loadAddon(pwshCompletionProviderAddon); this.add(pwshCompletionProviderAddon); this.add(pwshCompletionProviderAddon.onDidRequestSendText(text => { - this._ctx.instance.focus(); this._ctx.instance.sendText(text, false); })); this.add(this._terminalCompletionService.registerTerminalCompletionProvider('builtinPwsh', pwshCompletionProviderAddon.id, pwshCompletionProviderAddon)); @@ -202,6 +205,23 @@ registerTerminalContribution(TerminalSuggestContribution.ID, TerminalSuggestCont // #region Actions +registerTerminalAction({ + id: TerminalSuggestCommandId.ConfigureSettings, + title: localize2('workbench.action.terminal.configureSuggestSettings', 'Configure'), + f1: false, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible), + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Comma, + weight: KeybindingWeight.WorkbenchContrib + }, + menu: { + id: MenuId.MenubarTerminalSuggestStatusMenu, + group: 'right', + order: 1 + }, + run: (c, accessor) => accessor.get(IPreferencesService).openSettings({ query: terminalSuggestConfigSection }) +}); + registerActiveInstanceAction({ id: TerminalSuggestCommandId.RequestCompletions, title: localize2('workbench.action.terminal.requestCompletions', 'Request Completions'), @@ -228,7 +248,8 @@ registerActiveInstanceAction({ keybinding: { // Up is bound to other workbench keybindings that this needs to beat primary: KeyCode.UpArrow, - weight: KeybindingWeight.WorkbenchContrib + 1 + weight: KeybindingWeight.WorkbenchContrib + 1, + when: ContextKeyExpr.or(SimpleSuggestContext.HasNavigated, ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.UpArrowNavigatesHistory}`, false)) }, run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.addon?.selectPreviousSuggestion() }); @@ -360,27 +381,20 @@ registerActiveInstanceAction({ }); registerActiveInstanceAction({ - id: TerminalSuggestCommandId.ConfigureSettings, - title: localize2('workbench.action.terminal.configureSuggestSettings', 'Configure'), + id: TerminalSuggestCommandId.HideSuggestWidgetAndNavigateHistory, + title: localize2('workbench.action.terminal.hideSuggestWidgetAndNavigateHistory', 'Hide Suggest Widget and Navigate History'), f1: false, precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.focus, TerminalContextKeys.isOpen, TerminalContextKeys.suggestWidgetVisible), - keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Comma, - weight: KeybindingWeight.WorkbenchContrib + keybinding: + { + primary: KeyCode.UpArrow, + when: ContextKeyExpr.and(SimpleSuggestContext.HasNavigated.negate(), ContextKeyExpr.equals(`config.${TerminalSuggestSettingId.UpArrowNavigatesHistory}`, true)), + weight: KeybindingWeight.WorkbenchContrib + 2 }, - menu: { - id: MenuId.MenubarTerminalSuggestStatusMenu, - group: 'right', - order: 1 - }, - run: (activeInstance, c, accessor) => accessor.get(IPreferencesService).openSettings({ query: terminalSuggestConfigSection }) -}); - -registerActiveInstanceAction({ - id: TerminalSuggestCommandId.ClearSuggestCache, - title: localize2('workbench.action.terminal.clearSuggestCache', 'Clear Suggest Cache'), - f1: true, - run: (activeInstance) => TerminalSuggestContribution.get(activeInstance)?.pwshAddon?.clearSuggestCache() + run: (activeInstance) => { + TerminalSuggestContribution.get(activeInstance)?.addon?.hideSuggestWidget(true); + activeInstance.sendText('\u001b[A', false); // Up arrow + } }); // #endregion diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts index c83bbe13..b6559e2a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionModel.ts @@ -47,7 +47,11 @@ const compareCompletionsFn = (leadingLineContent: string, a: TerminalCompletionI // Sort files of the same name by extension const isArg = leadingLineContent.includes(' '); - if (!isArg && a.labelLowExcludeFileExt === b.labelLowExcludeFileExt) { + if (!isArg && a.completion.kind === TerminalCompletionItemKind.File && b.completion.kind === TerminalCompletionItemKind.File) { + // If the file name excluding the extension is different, just do a regular sort + if (a.labelLowExcludeFileExt !== b.labelLowExcludeFileExt) { + return a.labelLowExcludeFileExt.localeCompare(b.labelLowExcludeFileExt, undefined, { ignorePunctuation: true }); + } // Then by label length ascending (excluding file extension if it's a file) score = a.labelLowExcludeFileExt.length - b.labelLowExcludeFileExt.length; if (score !== 0) { @@ -81,20 +85,38 @@ const compareCompletionsFn = (leadingLineContent: string, a: TerminalCompletionI } // Sort by folder depth (eg. `vscode/` should come before `vscode-.../`) - if (a.labelLowNormalizedPath && b.labelLowNormalizedPath) { - // Directories - // Count depth of path (number of / or \ occurrences) - score = count(a.labelLowNormalizedPath, '/') - count(b.labelLowNormalizedPath, '/'); - if (score !== 0) { - return score; - } + if (a.completion.kind === TerminalCompletionItemKind.Folder && b.completion.kind === TerminalCompletionItemKind.Folder) { + if (a.labelLowNormalizedPath && b.labelLowNormalizedPath) { + // Directories + // Count depth of path (number of / or \ occurrences) + score = count(a.labelLowNormalizedPath, '/') - count(b.labelLowNormalizedPath, '/'); + if (score !== 0) { + return score; + } - // Ensure shorter prefixes appear first - if (b.labelLowNormalizedPath.startsWith(a.labelLowNormalizedPath)) { - return -1; // `a` is a prefix of `b`, so `a` should come first + // Ensure shorter prefixes appear first + if (b.labelLowNormalizedPath.startsWith(a.labelLowNormalizedPath)) { + return -1; // `a` is a prefix of `b`, so `a` should come first + } + if (a.labelLowNormalizedPath.startsWith(b.labelLowNormalizedPath)) { + return 1; // `b` is a prefix of `a`, so `b` should come first + } } - if (a.labelLowNormalizedPath.startsWith(b.labelLowNormalizedPath)) { - return 1; // `b` is a prefix of `a`, so `b` should come first + } + + if (a.completion.kind !== b.completion.kind) { + // Sort by kind + if ((a.completion.kind === TerminalCompletionItemKind.Method || a.completion.kind === TerminalCompletionItemKind.Alias) && (b.completion.kind !== TerminalCompletionItemKind.Method && b.completion.kind !== TerminalCompletionItemKind.Alias)) { + return -1; // Methods and aliases should come first + } + if ((b.completion.kind === TerminalCompletionItemKind.Method || b.completion.kind === TerminalCompletionItemKind.Alias) && (a.completion.kind !== TerminalCompletionItemKind.Method && a.completion.kind !== TerminalCompletionItemKind.Alias)) { + return 1; // Methods and aliases should come first + } + if ((a.completion.kind === TerminalCompletionItemKind.File || a.completion.kind === TerminalCompletionItemKind.Folder) && (b.completion.kind !== TerminalCompletionItemKind.File && b.completion.kind !== TerminalCompletionItemKind.Folder)) { + return 1; // Resources should come last + } + if ((b.completion.kind === TerminalCompletionItemKind.File || b.completion.kind === TerminalCompletionItemKind.Folder) && (a.completion.kind !== TerminalCompletionItemKind.File && a.completion.kind !== TerminalCompletionItemKind.Folder)) { + return -1; // Resources should come last } } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index eb7f998c..af55e2bf 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -16,6 +16,7 @@ import { TerminalSuggestSettingId } from '../common/terminalSuggestConfiguration import { TerminalCompletionItemKind, type ITerminalCompletion } from './terminalCompletionItem.js'; import { env as processEnv } from '../../../../../base/common/process.js'; import type { IProcessEnvironment } from '../../../../../base/common/platform.js'; +import { timeout } from '../../../../../base/common/async.js'; export const ITerminalCompletionService = createDecorator('terminalCompletionService'); @@ -166,7 +167,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo if (provider.shellTypes && !provider.shellTypes.includes(shellType)) { return undefined; } - const completions = await provider.provideCompletions(promptValue, cursorPosition, allowFallbackCompletions, token); + const completions = await Promise.race([ + provider.provideCompletions(promptValue, cursorPosition, allowFallbackCompletions, token), + timeout(5000) + ]); if (!completions) { return undefined; } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 693bfefc..7757d450 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -5,7 +5,6 @@ import type { ITerminalAddon, Terminal } from '@xterm/xterm'; import * as dom from '../../../../../base/browser/dom.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { combinedDisposable, Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { sep } from '../../../../../base/common/path.js'; @@ -36,6 +35,9 @@ import { GOLDEN_LINE_HEIGHT_RATIO, MINIMUM_LINE_HEIGHT } from '../../../../../ed import { TerminalCompletionModel } from './terminalCompletionModel.js'; import { TerminalCompletionItem, TerminalCompletionItemKind, type ITerminalCompletion } from './terminalCompletionItem.js'; import { IntervalTimer, TimeoutTimer } from '../../../../../base/common/async.js'; +import { localize } from '../../../../../nls.js'; +import { TerminalSuggestTelemetry } from './terminalSuggestTelemetry.js'; +import { terminalSymbolAliasIcon, terminalSymbolArgumentIcon, terminalSymbolEnumMember, terminalSymbolFileIcon, terminalSymbolFlagIcon, terminalSymbolInlineSuggestionIcon, terminalSymbolMethodIcon, terminalSymbolOptionIcon, terminalSymbolFolderIcon } from './terminalSymbolIcons.js'; export interface ISuggestController { isPasting: boolean; @@ -44,7 +46,7 @@ export interface ISuggestController { selectNextSuggestion(): void; selectNextPageSuggestion(): void; acceptSelectedSuggestion(suggestion?: Pick, 'item' | 'model'>): void; - hideSuggestWidget(cancelAnyRequests: boolean): void; + hideSuggestWidget(cancelAnyRequests: boolean, wasClosedByUser?: boolean): void; } export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggestController { private _terminal?: Terminal; @@ -89,16 +91,29 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest readonly onDidFontConfigurationChange = this._onDidFontConfigurationChange.event; private _kindToIconMap = new Map([ - [TerminalCompletionItemKind.File, Codicon.file], - [TerminalCompletionItemKind.Folder, Codicon.folder], - [TerminalCompletionItemKind.Method, Codicon.symbolMethod], - [TerminalCompletionItemKind.Alias, Codicon.symbolMethodArrow], - [TerminalCompletionItemKind.Argument, Codicon.symbolVariable], - [TerminalCompletionItemKind.Option, Codicon.symbolEnum], - [TerminalCompletionItemKind.OptionValue, Codicon.symbolEnumMember], - [TerminalCompletionItemKind.Flag, Codicon.flag], - [TerminalCompletionItemKind.InlineSuggestion, Codicon.star], - [TerminalCompletionItemKind.InlineSuggestionAlwaysOnTop, Codicon.star], + [TerminalCompletionItemKind.File, terminalSymbolFileIcon], + [TerminalCompletionItemKind.Folder, terminalSymbolFolderIcon], + [TerminalCompletionItemKind.Method, terminalSymbolMethodIcon], + [TerminalCompletionItemKind.Alias, terminalSymbolAliasIcon], + [TerminalCompletionItemKind.Argument, terminalSymbolArgumentIcon], + [TerminalCompletionItemKind.Option, terminalSymbolOptionIcon], + [TerminalCompletionItemKind.OptionValue, terminalSymbolEnumMember], + [TerminalCompletionItemKind.Flag, terminalSymbolFlagIcon], + [TerminalCompletionItemKind.InlineSuggestion, terminalSymbolInlineSuggestionIcon], + [TerminalCompletionItemKind.InlineSuggestionAlwaysOnTop, terminalSymbolInlineSuggestionIcon], + ]); + + private _kindToKindLabelMap = new Map([ + [TerminalCompletionItemKind.File, localize('file', 'File')], + [TerminalCompletionItemKind.Folder, localize('folder', 'Folder')], + [TerminalCompletionItemKind.Method, localize('method', 'Method')], + [TerminalCompletionItemKind.Alias, localize('alias', 'Alias')], + [TerminalCompletionItemKind.Argument, localize('argument', 'Argument')], + [TerminalCompletionItemKind.Option, localize('option', 'Option')], + [TerminalCompletionItemKind.OptionValue, localize('optionValue', 'Option Value')], + [TerminalCompletionItemKind.Flag, localize('flag', 'Flag')], + [TerminalCompletionItemKind.InlineSuggestion, localize('inlineSuggestion', 'Inline Suggestion')], + [TerminalCompletionItemKind.InlineSuggestionAlwaysOnTop, localize('inlineSuggestionAlwaysOnTop', 'Inline Suggestion')], ]); private readonly _inlineCompletion: ITerminalCompletion = { @@ -111,11 +126,13 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest provider: 'core', detail: 'Inline suggestion', kind: TerminalCompletionItemKind.InlineSuggestion, + kindLabel: 'Inline suggestion', icon: this._kindToIconMap.get(TerminalCompletionItemKind.InlineSuggestion), }; private readonly _inlineCompletionItem = new TerminalCompletionItem(this._inlineCompletion); private _shouldSyncWhenReady: boolean = false; + private _suggestTelemetry: TerminalSuggestTelemetry | undefined; constructor( shellType: TerminalShellType | undefined, @@ -160,10 +177,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (commandDetection) { if (this._promptInputModel !== commandDetection.promptInputModel) { this._promptInputModel = commandDetection.promptInputModel; + this._suggestTelemetry = this._register(this._instantiationService.createInstance(TerminalSuggestTelemetry, commandDetection, this._promptInputModel)); this._promptInputModelSubscriptions.value = combinedDisposable( this._promptInputModel.onDidChangeInput(e => this._sync(e)), this._promptInputModel.onDidFinishInput(() => { - this._mostRecentPromptInputState = undefined; this.hideSuggestWidget(true); }), ); @@ -203,6 +220,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._lastUserData = e.key; this._lastUserDataTimestamp = Date.now(); })); + this._register(xterm.onScroll(() => this.hideSuggestWidget(true))); } private async _handleCompletionProviders(terminal: Terminal | undefined, token: CancellationToken, explicitlyInvoked?: boolean): Promise { @@ -244,7 +262,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._requestedCompletionsIndex = this._currentPromptInputState.cursorIndex; const quickSuggestionsConfig = this._configurationService.getValue(terminalSuggestConfigSection).quickSuggestions; - const allowFallbackCompletions = explicitlyInvoked || quickSuggestionsConfig === true || typeof quickSuggestionsConfig === 'object' && quickSuggestionsConfig.unknown === 'on'; + const allowFallbackCompletions = explicitlyInvoked || quickSuggestionsConfig.unknown === 'on'; const providedCompletions = await this._terminalCompletionService.provideCompletions(this._currentPromptInputState.prefix, this._currentPromptInputState.cursorIndex, allowFallbackCompletions, this.shellType, this._capabilities, token, doNotRequestExtensionCompletions); if (token.isCancellationRequested) { @@ -257,6 +275,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest const completions = providedCompletions?.flat() || []; if (!explicitlyInvoked && !completions.length) { + this.hideSuggestWidget(true); return; } @@ -284,12 +303,13 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest // Add any "ghost text" suggestion suggested by the shell. This aligns with behavior of the // editor and how it interacts with inline completions. This object is tracked and reused as // it may change on input. - this._refreshInlineCompletion(); + this._refreshInlineCompletion(completions); // Add any missing icons based on the completion item kind for (const completion of completions) { if (!completion.icon && completion.kind !== undefined) { completion.icon = this._kindToIconMap.get(completion.kind); + completion.kindLabel = this._kindToKindLabelMap.get(completion.kind); } } @@ -349,11 +369,27 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest await this._handleCompletionProviders(this._terminal, token, explicitlyInvoked); } + private _addPropertiesToInlineCompletionItem(completions: ITerminalCompletion[]): void { + const inlineCompletionLabel = (typeof this._inlineCompletionItem.completion.label === 'string' ? this._inlineCompletionItem.completion.label : this._inlineCompletionItem.completion.label.label).trim(); + const inlineCompletionMatchIndex = completions.findIndex(c => typeof c.label === 'string' ? c.label === inlineCompletionLabel : c.label.label === inlineCompletionLabel); + if (inlineCompletionMatchIndex !== -1) { + // Remove the existing inline completion item from the completions list + const richCompletionMatchingInline = completions.splice(inlineCompletionMatchIndex, 1)[0]; + // Apply its properties to the inline completion item + this._inlineCompletionItem.completion.label = richCompletionMatchingInline.label; + this._inlineCompletionItem.completion.detail = richCompletionMatchingInline.detail; + this._inlineCompletionItem.completion.documentation = richCompletionMatchingInline.documentation; + } else if (this._inlineCompletionItem.completion) { + this._inlineCompletionItem.completion.detail = undefined; + this._inlineCompletionItem.completion.documentation = undefined; + } + } + private _requestTriggerCharQuickSuggestCompletions(): boolean { if (!this._wasLastInputVerticalArrowKey()) { // Only request on trigger character when it's a regular input, or on an arrow if the widget // is already visible - if (!this._wasLastInputArrowKey() || this._terminalSuggestWidgetVisibleContextKey.get()) { + if (!this._wasLastInputIncludedEscape() || this._terminalSuggestWidgetVisibleContextKey.get()) { this.requestCompletions(); return true; } @@ -369,6 +405,14 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return !!this._lastUserData?.match(/^\x1b[\[O]?[A-B]$/); } + /** + * Whether the last input included the escape character. Typically this will mean it was more + * than just a simple character, such as arrow keys, home, end, etc. + */ + private _wasLastInputIncludedEscape(): boolean { + return !!this._lastUserData?.includes('\x1b'); + } + private _wasLastInputArrowKey(): boolean { // Never request completions if the last key sequence was up or down as the user was likely // navigating history @@ -386,9 +430,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest if (!this._terminalSuggestWidgetVisibleContextKey.get()) { const commandLineHasSpace = promptInputState.prefix.trim().match(/\s/); if ( - (typeof config.quickSuggestions === 'boolean' && config.quickSuggestions) || - (typeof config.quickSuggestions === 'object' && !commandLineHasSpace && config.quickSuggestions.commands !== 'off') || - (typeof config.quickSuggestions === 'object' && commandLineHasSpace && config.quickSuggestions.arguments !== 'off') + (!commandLineHasSpace && config.quickSuggestions.commands !== 'off') || + (commandLineHasSpace && config.quickSuggestions.arguments !== 'off') ) { if (promptInputState.prefix.match(/[^\s]$/)) { sent = this._requestTriggerCharQuickSuggestCompletions(); @@ -426,16 +469,20 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } // If the cursor moved to the left - if (this._mostRecentPromptInputState && promptInputState.cursorIndex < this._mostRecentPromptInputState.cursorIndex) { - // Backspace or left past a trigger character - if (config.suggestOnTriggerCharacters && !sent && this._mostRecentPromptInputState.cursorIndex > 0) { - const char = this._mostRecentPromptInputState.value[this._mostRecentPromptInputState.cursorIndex - 1]; - if ( - // Only trigger on `\` and `/` if it's a directory. Not doing so causes problems - // with git branches in particular - this._isFilteringDirectories && char.match(/[\\\/]$/) - ) { - sent = this._requestTriggerCharQuickSuggestCompletions(); + if (this._mostRecentPromptInputState && promptInputState.cursorIndex < this._mostRecentPromptInputState.cursorIndex && promptInputState.cursorIndex > 0) { + // We only want to refresh via trigger characters in this case if the widget is + // already visible + if (this._terminalSuggestWidgetVisibleContextKey.get()) { + // Backspace or left past a trigger character + if (config.suggestOnTriggerCharacters && !sent && this._mostRecentPromptInputState.cursorIndex > 0) { + const char = this._mostRecentPromptInputState.value[this._mostRecentPromptInputState.cursorIndex - 1]; + if ( + // Only trigger on `\` and `/` if it's a directory. Not doing so causes problems + // with git branches in particular + this._isFilteringDirectories && char.match(/[\\\/]$/) + ) { + sent = this._requestTriggerCharQuickSuggestCompletions(); + } } } } @@ -488,7 +535,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest this._suggestWidget.setLineContext(lineContext); } - this._refreshInlineCompletion(); + this._refreshInlineCompletion(this._model?.items.map(i => i.completion) || []); // Hide and clear model if there are no more items if (!this._suggestWidget.hasCompletions()) { @@ -501,14 +548,14 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return; } const xtermBox = this._screen!.getBoundingClientRect(); - this._suggestWidget.showSuggestions(0, false, false, { + this._suggestWidget.showSuggestions(0, false, true, { left: xtermBox.left + this._terminal.buffer.active.cursorX * dimensions.width, top: xtermBox.top + this._terminal.buffer.active.cursorY * dimensions.height, height: dimensions.height }); } - private _refreshInlineCompletion() { + private _refreshInlineCompletion(completions: ITerminalCompletion[]): void { const oldIsInvalid = this._inlineCompletionItem.isInvalid; if (!this._currentPromptInputState || this._currentPromptInputState.ghostTextIndex === -1) { this._inlineCompletionItem.isInvalid = true; @@ -526,6 +573,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest // Reset the completion item as the object reference must remain the same but its // contents will differ across syncs. This is done so we don't need to reassign the // model and the slowdown/flickering that could potentially cause. + this._addPropertiesToInlineCompletionItem(completions); + const x = new TerminalCompletionItem(this._inlineCompletion); this._inlineCompletionItem.idx = x.idx; this._inlineCompletionItem.score = x.score; @@ -592,6 +641,10 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return fontInfo; } + private _getAdvancedExplainModeDetails(): string | undefined { + return `promptInputModel: ${this._promptInputModel?.getCombinedString()}`; + } + private _showCompletions(model: TerminalCompletionModel, explicitlyInvoked?: boolean): void { if (!this._terminal?.element) { return; @@ -608,8 +661,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return; } const xtermBox = this._screen!.getBoundingClientRect(); - - suggestWidget.showSuggestions(0, false, false, { + suggestWidget.showSuggestions(0, false, !explicitlyInvoked, { left: xtermBox.left + this._terminal.buffer.active.cursorX * dimensions.width, top: xtermBox.top + this._terminal.buffer.active.cursorY * dimensions.height, height: dimensions.height @@ -628,7 +680,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest showStatusBarSettingId: TerminalSuggestSettingId.ShowStatusBar }, this._getFontInfo.bind(this), - this._onDidFontConfigurationChange.event.bind(this) + this._onDidFontConfigurationChange.event.bind(this), + this._getAdvancedExplainModeDetails.bind(this) )) as any as SimpleSuggestWidget; this._suggestWidget.list.style(getListStyles({ listInactiveFocusBackground: editorSuggestWidgetSelectedBackground, @@ -690,6 +743,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest } const initialPromptInputState = this._mostRecentPromptInputState; if (!suggestion || !initialPromptInputState || this._leadingLineContent === undefined || !this._model) { + this._suggestTelemetry?.acceptCompletion(undefined, this._mostRecentPromptInputState?.value); return; } SuggestAddon.lastAcceptedCompletionTimestamp = Date.now(); @@ -775,6 +829,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest // Send the completion this._onAcceptedCompletion.fire(resultSequence); + this._suggestTelemetry?.acceptCompletion(completion, this._mostRecentPromptInputState?.value); this.hideSuggestWidget(true); } @@ -826,3 +881,4 @@ export function normalizePathSeparator(path: string, sep: string): string { } return path.replaceAll('/', '\\'); } + diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts new file mode 100644 index 00000000..4eef8c24 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestTelemetry.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { ICommandDetectionCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { IPromptInputModel } from '../../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js'; +import { ITerminalCompletion, TerminalCompletionItemKind } from './terminalCompletionItem.js'; + +export class TerminalSuggestTelemetry extends Disposable { + private _acceptedCompletions: Array<{ label: string; kind?: string }> | undefined; + + private _kindMap = new Map([ + [TerminalCompletionItemKind.File, 'File'], + [TerminalCompletionItemKind.Folder, 'Folder'], + [TerminalCompletionItemKind.Method, 'Method'], + [TerminalCompletionItemKind.Alias, 'Alias'], + [TerminalCompletionItemKind.Argument, 'Argument'], + [TerminalCompletionItemKind.Option, 'Option'], + [TerminalCompletionItemKind.OptionValue, 'Option Value'], + [TerminalCompletionItemKind.Flag, 'Flag'], + [TerminalCompletionItemKind.InlineSuggestion, 'Inline Suggestion'], + [TerminalCompletionItemKind.InlineSuggestionAlwaysOnTop, 'Inline Suggestion'], + ]); + + constructor( + commandDetection: ICommandDetectionCapability, + private readonly _promptInputModel: IPromptInputModel, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + ) { + super(); + this._register(commandDetection.onCommandFinished((e) => { + this._sendTelemetryInfo(false, e.exitCode); + this._acceptedCompletions = undefined; + })); + this._register(this._promptInputModel.onDidInterrupt(() => { + this._sendTelemetryInfo(true); + this._acceptedCompletions = undefined; + })); + } + acceptCompletion(completion: ITerminalCompletion | undefined, commandLine?: string): void { + if (!completion || !commandLine) { + this._acceptedCompletions = undefined; + return; + } + this._acceptedCompletions = this._acceptedCompletions || []; + this._acceptedCompletions.push({ label: typeof completion.label === 'string' ? completion.label : completion.label.label, kind: this._kindMap.get(completion.kind!) }); + } + private _sendTelemetryInfo(fromInterrupt?: boolean, exitCode?: number): void { + const commandLine = this._promptInputModel?.value; + for (const completion of this._acceptedCompletions || []) { + const label = completion?.label; + const kind = completion?.kind; + + if (label === undefined || commandLine === undefined || kind === undefined) { + return; + } + + let outcome: string; + if (fromInterrupt) { + outcome = CompletionOutcome.Interrupted; + } else if (commandLine.trim() && commandLine.includes(label)) { + outcome = CompletionOutcome.Accepted; + } else if (inputContainsFirstHalfOfLabel(commandLine, label)) { + outcome = CompletionOutcome.AcceptedWithEdit; + } else { + outcome = CompletionOutcome.Deleted; + } + this._telemetryService.publicLog2<{ + kind: string | undefined; + outcome: string; + exitCode: number | undefined; + }, { + owner: 'meganrogge'; + comment: 'This data is collected to understand the outcome of a terminal completion acceptance.'; + kind: { + classification: 'SystemMetaData'; + purpose: 'FeatureInsight'; + comment: 'The completion item\'s kind'; + }; + outcome: { + classification: 'SystemMetaData'; + purpose: 'FeatureInsight'; + comment: 'The outcome of the accepted completion'; + }; + exitCode: { + classification: 'SystemMetaData'; + purpose: 'FeatureInsight'; + comment: 'The exit code from the command'; + }; + }>('terminal.suggest.acceptedCompletion', { + kind, + outcome, + exitCode + }); + } + } +} + +const enum CompletionOutcome { + Accepted = 'Accepted', + Deleted = 'Deleted', + AcceptedWithEdit = 'AcceptedWithEdit', + Interrupted = 'Interrupted' +} + +function inputContainsFirstHalfOfLabel(commandLine: string, label: string): boolean { + return commandLine.includes(label.substring(0, Math.ceil(label.length / 2))); +} + diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSymbolIcons.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSymbolIcons.ts index a2039d2c..5f216f62 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSymbolIcons.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSymbolIcons.ts @@ -4,10 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import './media/terminalSymbolIcons.css'; -import { SYMBOL_ICON_ENUMERATOR_FOREGROUND, SYMBOL_ICON_METHOD_FOREGROUND } from '../../../../../editor/contrib/symbolIcons/browser/symbolIcons.js'; +import { SYMBOL_ICON_ENUMERATOR_FOREGROUND, SYMBOL_ICON_ENUMERATOR_MEMBER_FOREGROUND, SYMBOL_ICON_METHOD_FOREGROUND, SYMBOL_ICON_VARIABLE_FOREGROUND, SYMBOL_ICON_FILE_FOREGROUND, SYMBOL_ICON_FOLDER_FOREGROUND } from '../../../../../editor/contrib/symbolIcons/browser/symbolIcons.js'; import { registerColor } from '../../../../../platform/theme/common/colorUtils.js'; import { localize } from '../../../../../nls.js'; +import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; export const TERMINAL_SYMBOL_ICON_FLAG_FOREGROUND = registerColor('terminalSymbolIcon.flagForeground', SYMBOL_ICON_ENUMERATOR_FOREGROUND, localize('terminalSymbolIcon.flagForeground', 'The foreground color for an flag icon. These icons will appear in the terminal suggest widget.')); - export const TERMINAL_SYMBOL_ICON_ALIAS_FOREGROUND = registerColor('terminalSymbolIcon.aliasForeground', SYMBOL_ICON_METHOD_FOREGROUND, localize('terminalSymbolIcon.aliasForeground', 'The foreground color for an alias icon. These icons will appear in the terminal suggest widget.')); +export const TERMINAL_SYMBOL_ICON_OPTION_VALUE_FOREGROUND = registerColor('terminalSymbolIcon.optionValueForeground', SYMBOL_ICON_ENUMERATOR_MEMBER_FOREGROUND, localize('terminalSymbolIcon.enumMemberForeground', 'The foreground color for an enum member icon. These icons will appear in the terminal suggest widget.')); +export const TERMINAL_SYMBOL_ICON_METHOD_FOREGROUND = registerColor('terminalSymbolIcon.methodForeground', SYMBOL_ICON_METHOD_FOREGROUND, localize('terminalSymbolIcon.methodForeground', 'The foreground color for a method icon. These icons will appear in the terminal suggest widget.')); +export const TERMINAL_SYMBOL_ICON_ARGUMENT_FOREGROUND = registerColor('terminalSymbolIcon.argumentForeground', SYMBOL_ICON_VARIABLE_FOREGROUND, localize('terminalSymbolIcon.argumentForeground', 'The foreground color for an argument icon. These icons will appear in the terminal suggest widget.')); +export const TERMINAL_SYMBOL_ICON_OPTION_FOREGROUND = registerColor('terminalSymbolIcon.optionForeground', SYMBOL_ICON_ENUMERATOR_FOREGROUND, localize('terminalSymbolIcon.optionForeground', 'The foreground color for an option icon. These icons will appear in the terminal suggest widget.')); +export const TERMINAL_SYMBOL_ICON_INLINE_SUGGESTION_FOREGROUND = registerColor('terminalSymbolIcon.inlineSuggestionForeground', null, localize('terminalSymbolIcon.inlineSuggestionForeground', 'The foreground color for an inline suggestion icon. These icons will appear in the terminal suggest widget.')); +export const TERMINAL_SYMBOL_ICON_FILE_FOREGROUND = registerColor('terminalSymbolIcon.fileForeground', SYMBOL_ICON_FILE_FOREGROUND, localize('terminalSymbolIcon.fileForeground', 'The foreground color for a file icon. These icons will appear in the terminal suggest widget.')); +export const TERMINAL_SYMBOL_ICON_FOLDER_FOREGROUND = registerColor('terminalSymbolIcon.folderForeground', SYMBOL_ICON_FOLDER_FOREGROUND, localize('terminalSymbolIcon.folderForeground', 'The foreground color for a folder icon. These icons will appear in the terminal suggest widget.')); + +export const terminalSymbolFlagIcon = registerIcon('terminal-symbol-flag', Codicon.flag, localize('terminalSymbolFlagIcon', 'Icon for flags in the terminal suggest widget.'), TERMINAL_SYMBOL_ICON_FLAG_FOREGROUND); +export const terminalSymbolAliasIcon = registerIcon('terminal-symbol-alias', Codicon.symbolMethod, localize('terminalSymbolAliasIcon', 'Icon for aliases in the terminal suggest widget.'), TERMINAL_SYMBOL_ICON_ALIAS_FOREGROUND); +export const terminalSymbolEnumMember = registerIcon('terminal-symbol-option-value', Codicon.symbolEnumMember, localize('terminalSymbolOptionValue', 'Icon for enum members in the terminal suggest widget.'), TERMINAL_SYMBOL_ICON_OPTION_VALUE_FOREGROUND); +export const terminalSymbolMethodIcon = registerIcon('terminal-symbol-method', Codicon.symbolMethod, localize('terminalSymbolMethodIcon', 'Icon for methods in the terminal suggest widget.'), TERMINAL_SYMBOL_ICON_METHOD_FOREGROUND); +export const terminalSymbolArgumentIcon = registerIcon('terminal-symbol-argument', Codicon.symbolVariable, localize('terminalSymbolArgumentIcon', 'Icon for arguments in the terminal suggest widget.'), TERMINAL_SYMBOL_ICON_ARGUMENT_FOREGROUND); +export const terminalSymbolOptionIcon = registerIcon('terminal-symbol-option', Codicon.symbolEnum, localize('terminalSymbolOptionIcon', 'Icon for options in the terminal suggest widget.'), TERMINAL_SYMBOL_ICON_OPTION_FOREGROUND); +export const terminalSymbolInlineSuggestionIcon = registerIcon('terminal-symbol-inline-suggestion', Codicon.star, localize('terminalSymbolInlineSuggestionIcon', 'Icon for inline suggestions in the terminal suggest widget.'), TERMINAL_SYMBOL_ICON_INLINE_SUGGESTION_FOREGROUND); +export const terminalSymbolFileIcon = registerIcon('terminal-symbol-file', Codicon.symbolFile, localize('terminalSymbolFileIcon', 'Icon for files in the terminal suggest widget.'), TERMINAL_SYMBOL_ICON_FILE_FOREGROUND); +export const terminalSymbolFolderIcon = registerIcon('terminal-symbol-folder', Codicon.symbolFolder, localize('terminalSymbolFolderIcon', 'Icon for folders in the terminal suggest widget.'), TERMINAL_SYMBOL_ICON_FOLDER_FOREGROUND); diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts index 30d9c86a..f9d66c0a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminal.suggest.ts @@ -11,7 +11,7 @@ export const enum TerminalSuggestCommandId { AcceptSelectedSuggestion = 'workbench.action.terminal.acceptSelectedSuggestion', AcceptSelectedSuggestionEnter = 'workbench.action.terminal.acceptSelectedSuggestionEnter', HideSuggestWidget = 'workbench.action.terminal.hideSuggestWidget', - ClearSuggestCache = 'workbench.action.terminal.clearSuggestCache', + HideSuggestWidgetAndNavigateHistory = 'workbench.action.terminal.hideSuggestWidgetAndNavigateHistory', RequestCompletions = 'workbench.action.terminal.requestCompletions', ResetWidgetSize = 'workbench.action.terminal.resetSuggestWidgetSize', ToggleDetails = 'workbench.action.terminal.suggestToggleDetails', @@ -27,7 +27,6 @@ export const defaultTerminalSuggestCommandsToSkipShell = [ TerminalSuggestCommandId.AcceptSelectedSuggestion, TerminalSuggestCommandId.AcceptSelectedSuggestionEnter, TerminalSuggestCommandId.HideSuggestWidget, - TerminalSuggestCommandId.ClearSuggestCache, TerminalSuggestCommandId.RequestCompletions, TerminalSuggestCommandId.ToggleDetails, TerminalSuggestCommandId.ToggleDetailsFocus, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index db600bb8..f5cf2b9a 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -18,6 +18,7 @@ export const enum TerminalSuggestSettingId { ShowStatusBar = 'terminal.integrated.suggest.showStatusBar', CdPath = 'terminal.integrated.suggest.cdPath', InlineSuggestion = 'terminal.integrated.suggest.inlineSuggestion', + UpArrowNavigatesHistory = 'terminal.integrated.suggest.upArrowNavigatesHistory', } export const windowsDefaultExecutableExtensions: string[] = [ @@ -43,7 +44,7 @@ export const terminalSuggestConfigSection = 'terminal.integrated.suggest'; export interface ITerminalSuggestConfiguration { enabled: boolean; - quickSuggestions: /*Legacy - was this when experimental*/boolean | { + quickSuggestions: { commands: 'off' | 'on'; arguments: 'off' | 'on'; unknown: 'off' | 'on'; @@ -169,7 +170,14 @@ export const terminalSuggestConfiguration: IStringDictionary injectionRange.endIndex)) && continueCursor) { + if (cursor.endIndex < injectionRange.startIndex) { + continueCursor = cursor.gotoNextSibling(); + } else { + continueCursor = cursor.gotoFirstChild(); + } + } + } + + private _treeSitterTokenize(textModelTreeSitter: ITextModelTreeSitter, tree: Parser.Tree, languageId: string): IToken[] { const cursor = tree.walk(); cursor.gotoFirstChild(); let cursorResult: boolean = true; const tokens: IToken[] = []; const tokenizationSupport = TreeSitterTokenizationRegistry.get(languageId); + const cursors: { cursor: Parser.TreeCursor; languageId: string; startOffset: number; endOffset: number }[] = [{ cursor, languageId, startOffset: 0, endOffset: textModelTreeSitter.textModel.getValueLength() }]; do { - if (cursor.currentNode.childCount === 0) { - const capture = tokenizationSupport?.captureAtPositionTree(cursor.currentNode.startPosition.row + 1, cursor.currentNode.startPosition.column + 1, tree); - tokens.push({ - c: cursor.currentNode.text.replace(/\r/g, ''), - t: capture && capture.length > 0 ? capture[capture.length - 1].name : '', - r: { - dark_plus: undefined, - light_plus: undefined, - dark_vs: undefined, - light_vs: undefined, - hc_black: undefined, - } - }); + const current = cursors[cursors.length - 1]; + const currentCursor = current.cursor; + const currentLanguageId = current.languageId; + const isOutsideRange: boolean = (currentCursor.currentNode.endIndex > current.endOffset); - while (!(cursorResult = cursor.gotoNextSibling())) { - if (!(cursorResult = cursor.gotoParent())) { - break; + if (!isOutsideRange && (currentCursor.currentNode.childCount === 0)) { + const range = new Range(currentCursor.currentNode.startPosition.row + 1, currentCursor.currentNode.startPosition.column + 1, currentCursor.currentNode.endPosition.row + 1, currentCursor.currentNode.endPosition.column + 1); + const injection = textModelTreeSitter.getInjection(currentCursor.currentNode.startIndex, currentLanguageId); + const treeSitterRange = injection?.ranges!.find(r => r.startIndex <= currentCursor.currentNode.startIndex && r.endIndex >= currentCursor.currentNode.endIndex); + if (injection?.tree && treeSitterRange && (treeSitterRange.startIndex === currentCursor.currentNode.startIndex)) { + const injectionLanguageId = injection.languageId; + const injectionTree = injection.tree; + const injectionCursor = injectionTree.walk(); + this._moveInjectionCursorToRange(injectionCursor, treeSitterRange); + cursors.push({ cursor: injectionCursor, languageId: injectionLanguageId, startOffset: treeSitterRange.startIndex, endOffset: treeSitterRange.endIndex }); + while ((currentCursor.endIndex <= treeSitterRange.endIndex) && (currentCursor.gotoNextSibling() || currentCursor.gotoParent())) { } + } else { + const capture = tokenizationSupport?.captureAtRangeTree(range, tree, textModelTreeSitter); + tokens.push({ + c: currentCursor.currentNode.text.replace(/\r/g, ''), + t: capture?.map(cap => cap.name).join(' ') ?? '', + r: { + dark_plus: undefined, + light_plus: undefined, + dark_vs: undefined, + light_vs: undefined, + hc_black: undefined, + } + }); + while (!(cursorResult = currentCursor.gotoNextSibling())) { + if (!(cursorResult = currentCursor.gotoParent())) { + break; + } } } + } else { - cursorResult = cursor.gotoFirstChild(); + cursorResult = currentCursor.gotoFirstChild(); + } + if (cursors.length > 1 && ((!cursorResult && currentCursor === cursors[cursors.length - 1].cursor) || isOutsideRange)) { + cursors.pop(); + cursorResult = true; } } while (cursorResult); return tokens; @@ -324,14 +362,32 @@ class Snapper { }); } - public async captureTreeSitterSyntaxTokens(fileName: string, content: string): Promise { - const languageId = this.languageService.guessLanguageIdByFilepathOrFirstLine(URI.file(fileName)); + public async captureTreeSitterSyntaxTokens(resource: URI, content: string): Promise { + const languageId = this.languageService.guessLanguageIdByFilepathOrFirstLine(resource); if (languageId) { - const tree = await this.treeSitterParserService.getTree(content, languageId!); + const hasLanguage = TreeSitterTokenizationRegistry.get(languageId); + if (!hasLanguage) { + return []; + } + const model = this.modelService.getModel(resource) ?? this.modelService.createModel(content, { languageId, onDidChange: Event.None }, resource); + let textModelTreeSitter = this.treeSitterParserService.getParseResult(model); + let tree = textModelTreeSitter?.parseResult?.tree; + if (!textModelTreeSitter) { + return []; + } + if (!tree) { + let e = await Event.toPromise(this.treeSitterParserService.onDidUpdateTree); + // Once more for injections + if (e.hasInjections) { + e = await Event.toPromise(this.treeSitterParserService.onDidUpdateTree); + } + textModelTreeSitter = e.tree; + tree = textModelTreeSitter.parseResult?.tree; + } if (!tree) { return []; } - const result = (await this._treeSitterTokenize(tree, languageId)).filter(t => t.c.length > 0); + const result = (await this._treeSitterTokenize(textModelTreeSitter, tree, languageId)).filter(t => t.c.length > 0); const themeTokens = await this._getTreeSitterThemesResult(result, languageId); this._enrichResult(result, themeTokens); return result; @@ -348,7 +404,7 @@ async function captureTokens(accessor: ServicesAccessor, resource: URI | undefin return fileService.readFile(resource).then(content => { if (treeSitter) { - return snapper.captureTreeSitterSyntaxTokens(fileName, content.value.toString()); + return snapper.captureTreeSitterSyntaxTokens(resource, content.value.toString()); } else { return snapper.captureSyntaxTokens(fileName, content.value.toString()); } @@ -377,6 +433,12 @@ CommandsRegistry.registerCommand('_workbench.captureSyntaxTokens', function (acc return captureTokens(accessor, resource); }); -CommandsRegistry.registerCommand('_workbench.captureTreeSitterSyntaxTokens', function (accessor: ServicesAccessor, resource: URI) { +CommandsRegistry.registerCommand('_workbench.captureTreeSitterSyntaxTokens', function (accessor: ServicesAccessor, resource?: URI) { + // If no resource is provided, use the active editor's resource + // This is useful for testing the command + if (!resource) { + const editorService = accessor.get(IEditorService); + resource = editorService.activeEditor?.resource; + } return captureTokens(accessor, resource, true); }); diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index 42193a62..a2ef6a1c 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -5,7 +5,7 @@ import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Emitter } from '../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, IDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider, TimelinePaneId } from './timeline.js'; @@ -16,6 +16,7 @@ import { IContextKey, IContextKeyService, RawContextKey } from '../../../../plat export const TimelineHasProviderContext = new RawContextKey('timelineHasProvider', false); export class TimelineService extends Disposable implements ITimelineService { + declare readonly _serviceBrand: undefined; private readonly _onDidChangeProviders = this._register(new Emitter()); @@ -23,12 +24,13 @@ export class TimelineService extends Disposable implements ITimelineService { private readonly _onDidChangeTimeline = this._register(new Emitter()); readonly onDidChangeTimeline = this._onDidChangeTimeline.event; + private readonly _onDidChangeUri = this._register(new Emitter()); readonly onDidChangeUri = this._onDidChangeUri.event; private readonly hasProviderContext: IContextKey; private readonly providers = new Map(); - private readonly providerSubscriptions = new Map(); + private readonly providerSubscriptions = this._register(new DisposableMap()); constructor( @ILogService private readonly logService: ILogService, @@ -122,8 +124,7 @@ export class TimelineService extends Disposable implements ITimelineService { } this.providers.delete(id); - this.providerSubscriptions.get(id)?.dispose(); - this.providerSubscriptions.delete(id); + this.providerSubscriptions.deleteAndDispose(id); this.updateHasProviderContext(); diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts index df75f745..0af4bb62 100644 --- a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts @@ -97,7 +97,7 @@ export class TypeHierarchyTreePeekWidget extends peekView.PeekViewWidget { this.create(); this._peekViewService.addExclusiveWidget(editor, this); this._applyTheme(themeService.getColorTheme()); - this._disposables.add(themeService.onDidColorThemeChange(e => this._applyTheme(e.theme), this)); + this._disposables.add(themeService.onDidColorThemeChange(this._applyTheme, this)); this._disposables.add(this._previewDisposable); } diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index d8c9fbd6..beedeb01 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -340,7 +340,6 @@ export class ReleaseNotesManager { padding-right: 3px; padding-top: 1px; padding-bottom: 1px; - margin-left: -5px; margin-top: -3px; } .codesetting:hover { @@ -363,7 +362,7 @@ export class ReleaseNotesManager { display: inline-block; background-color: var(--vscode-editor-background); font-size: 12px; - margin-right: 8px; + margin-right: 4px; } header { display: flex; align-items: center; padding-top: 1em; } diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index fd3452f5..3def92a9 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -29,7 +29,7 @@ import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys import { Promises } from '../../../../base/common/async.js'; import { IUserDataSyncWorkbenchService } from '../../../services/userDataSync/common/userDataSync.js'; import { Event } from '../../../../base/common/event.js'; -import { Action } from '../../../../base/common/actions.js'; +import { toAction } from '../../../../base/common/actions.js'; export const CONTEXT_UPDATE_STATE = new RawContextKey('updateState', StateType.Uninitialized); export const MAJOR_MINOR_UPDATE_AVAILABLE = new RawContextKey('majorMinorUpdateAvailable', false); @@ -215,8 +215,10 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu message: nls.localize('update service disabled', "Updates are disabled because you are running the user-scope installation of {0} as Administrator.", this.productService.nameLong), actions: { primary: [ - new Action('', nls.localize('learn more', "Learn More"), undefined, undefined, () => { - this.openerService.open('https://aka.ms/vscode-windows-setup'); + toAction({ + id: '', + label: nls.localize('learn more', "Learn More"), + run: () => this.openerService.open('https://aka.ms/vscode-windows-setup') }) ] }, diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainService.ts b/src/vs/workbench/contrib/url/browser/trustedDomainService.ts index 99cddd18..a5f3100c 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainService.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainService.ts @@ -10,13 +10,14 @@ import { URI } from '../../../../base/common/uri.js'; import { IInstantiationService, createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; import { TRUSTED_DOMAINS_STORAGE_KEY, readStaticTrustedDomains } from './trustedDomains.js'; -import { testUrlMatchesGlob } from '../common/urlGlob.js'; +import { isURLDomainTrusted } from '../common/trustedDomains.js'; +import { Event, Emitter } from '../../../../base/common/event.js'; export const ITrustedDomainService = createDecorator('ITrustedDomainService'); export interface ITrustedDomainService { _serviceBrand: undefined; - + onDidChangeTrustedDomains: Event; isValid(resource: URI): boolean; } @@ -25,6 +26,9 @@ export class TrustedDomainService extends Disposable implements ITrustedDomainSe private _staticTrustedDomainsResult!: WindowIdleValue; + private _onDidChangeTrustedDomains: Emitter = this._register(new Emitter()); + onDidChangeTrustedDomains: Event = this._onDidChangeTrustedDomains.event; + constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @IStorageService private readonly _storageService: IStorageService, @@ -44,6 +48,7 @@ export class TrustedDomainService extends Disposable implements ITrustedDomainSe this._register(this._storageService.onDidChangeValue(StorageScope.APPLICATION, TRUSTED_DOMAINS_STORAGE_KEY, this._store)(() => { this._staticTrustedDomainsResult?.dispose(); this._staticTrustedDomainsResult = initStaticDomainsResult(); + this._onDidChangeTrustedDomains.fire(); })); } @@ -54,54 +59,3 @@ export class TrustedDomainService extends Disposable implements ITrustedDomainSe return isURLDomainTrusted(resource, allTrustedDomains); } } - -const rLocalhost = /^localhost(:\d+)?$/i; -const r127 = /^127.0.0.1(:\d+)?$/; - -function isLocalhostAuthority(authority: string) { - return rLocalhost.test(authority) || r127.test(authority); -} - -/** - * Case-normalize some case-insensitive URLs, such as github. - */ -function normalizeURL(url: string | URI): string { - const caseInsensitiveAuthorities = ['github.com']; - try { - const parsed = typeof url === 'string' ? URI.parse(url, true) : url; - if (caseInsensitiveAuthorities.includes(parsed.authority)) { - return parsed.with({ path: parsed.path.toLowerCase() }).toString(true); - } else { - return parsed.toString(true); - } - } catch { return url.toString(); } -} - -/** - * Check whether a domain like https://www.microsoft.com matches - * the list of trusted domains. - * - * - Schemes must match - * - There's no subdomain matching. For example https://microsoft.com doesn't match https://www.microsoft.com - * - Star matches all subdomains. For example https://*.microsoft.com matches https://www.microsoft.com and https://foo.bar.microsoft.com - */ -export function isURLDomainTrusted(url: URI, trustedDomains: string[]): boolean { - url = URI.parse(normalizeURL(url)); - trustedDomains = trustedDomains.map(normalizeURL); - - if (isLocalhostAuthority(url.authority)) { - return true; - } - - for (let i = 0; i < trustedDomains.length; i++) { - if (trustedDomains[i] === '*') { - return true; - } - - if (testUrlMatchesGlob(url, trustedDomains[i])) { - return true; - } - } - - return false; -} diff --git a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts index 3a4df27b..23f27810 100644 --- a/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts +++ b/src/vs/workbench/contrib/url/browser/trustedDomainsValidator.ts @@ -18,7 +18,8 @@ import { IStorageService } from '../../../../platform/storage/common/storage.js' import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { ITrustedDomainService, isURLDomainTrusted } from './trustedDomainService.js'; +import { ITrustedDomainService } from './trustedDomainService.js'; +import { isURLDomainTrusted } from '../common/trustedDomains.js'; import { configureOpenerTrustedDomainsHandler, readStaticTrustedDomains } from './trustedDomains.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; diff --git a/src/vs/workbench/contrib/url/common/trustedDomains.ts b/src/vs/workbench/contrib/url/common/trustedDomains.ts new file mode 100644 index 00000000..850a8357 --- /dev/null +++ b/src/vs/workbench/contrib/url/common/trustedDomains.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../base/common/uri.js'; +import { testUrlMatchesGlob } from './urlGlob.js'; + + +/** + * Check whether a domain like https://www.microsoft.com matches + * the list of trusted domains. + * + * - Schemes must match + * - There's no subdomain matching. For example https://microsoft.com doesn't match https://www.microsoft.com + * - Star matches all subdomains. For example https://*.microsoft.com matches https://www.microsoft.com and https://foo.bar.microsoft.com + */ + +export function isURLDomainTrusted(url: URI, trustedDomains: string[]): boolean { + url = URI.parse(normalizeURL(url)); + trustedDomains = trustedDomains.map(normalizeURL); + + if (isLocalhostAuthority(url.authority)) { + return true; + } + + for (let i = 0; i < trustedDomains.length; i++) { + if (trustedDomains[i] === '*') { + return true; + } + + if (testUrlMatchesGlob(url, trustedDomains[i])) { + return true; + } + } + + return false; +} +/** + * Case-normalize some case-insensitive URLs, such as github. + */ + +export function normalizeURL(url: string | URI): string { + const caseInsensitiveAuthorities = ['github.com']; + try { + const parsed = typeof url === 'string' ? URI.parse(url, true) : url; + if (caseInsensitiveAuthorities.includes(parsed.authority)) { + return parsed.with({ path: parsed.path.toLowerCase() }).toString(true); + } else { + return parsed.toString(true); + } + } catch { return url.toString(); } +} +const rLocalhost = /^localhost(:\d+)?$/i; +const r127 = /^127.0.0.1(:\d+)?$/; + +export function isLocalhostAuthority(authority: string) { + return rLocalhost.test(authority) || r127.test(authority); +} + diff --git a/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts b/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts index 020ac3a9..d36e2128 100644 --- a/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts +++ b/src/vs/workbench/contrib/url/test/browser/mockTrustedDomainService.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Event } from '../../../../../base/common/event.js'; import { URI } from '../../../../../base/common/uri.js'; -import { isURLDomainTrusted, ITrustedDomainService } from '../../browser/trustedDomainService.js'; +import { ITrustedDomainService } from '../../browser/trustedDomainService.js'; +import { isURLDomainTrusted } from '../../common/trustedDomains.js'; export class MockTrustedDomainService implements ITrustedDomainService { _serviceBrand: undefined; @@ -12,6 +14,8 @@ export class MockTrustedDomainService implements ITrustedDomainService { constructor(private readonly _trustedDomains: string[] = []) { } + onDidChangeTrustedDomains: Event = Event.None; + isValid(resource: URI): boolean { return isURLDomainTrusted(resource, this._trustedDomains); } diff --git a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts index aef79543..50b2af6a 100644 --- a/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts +++ b/src/vs/workbench/contrib/url/test/browser/trustedDomains.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { isURLDomainTrusted } from '../../browser/trustedDomainService.js'; +import { isURLDomainTrusted } from '../../common/trustedDomains.js'; function linkAllowedByRules(link: string, rules: string[]) { assert.ok(isURLDomainTrusted(URI.parse(link), rules), `Link\n${link}\n should be allowed by rules\n${JSON.stringify(rules)}`); diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts index c4a03276..04a69674 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditor.ts @@ -5,7 +5,7 @@ import './media/userDataProfilesEditor.css'; import { $, addDisposableListener, append, clearNode, Dimension, EventHelper, EventType, IDomPosition, trackFocus } from '../../../../base/browser/dom.js'; -import { Action, IAction, IActionChangeEvent, Separator, SubmenuAction } from '../../../../base/common/actions.js'; +import { Action, IAction, IActionChangeEvent, Separator, SubmenuAction, toAction } from '../../../../base/common/actions.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; @@ -225,7 +225,11 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi actions.push(new SubmenuAction('from.template', localize('from template', "From Template"), this.getCreateFromTemplateActions())); actions.push(new Separator()); } - actions.push(new Action('importProfile', localize('importProfile', "Import Profile..."), undefined, true, () => this.importProfile())); + actions.push(toAction({ + id: 'importProfile', + label: localize('importProfile', "Import Profile..."), + run: () => this.importProfile() + })); return actions; } }, @@ -240,12 +244,11 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi private getCreateFromTemplateActions(): IAction[] { return this.templates.map(template => - new Action( - `template:${template.url}`, - template.name, - undefined, - true, - () => this.createNewProfile(URI.parse(template.url)))); + toAction({ + id: `template:${template.url}`, + label: template.name, + run: () => this.createNewProfile(URI.parse(template.url)) + })); } private registerListeners(): void { @@ -282,13 +285,21 @@ export class UserDataProfilesEditor extends EditorPane implements IUserDataProfi private getTreeContextMenuActions(): IAction[] { const actions: IAction[] = []; - actions.push(new Action('newProfile', localize('newProfile', "New Profile"), undefined, true, () => this.createNewProfile())); + actions.push(toAction({ + id: 'newProfile', + label: localize('newProfile', "New Profile"), + run: () => this.createNewProfile() + })); const templateActions = this.getCreateFromTemplateActions(); if (templateActions.length) { actions.push(new SubmenuAction('from.template', localize('new from template', "New Profile From Template"), templateActions)); } actions.push(new Separator()); - actions.push(new Action('importProfile', localize('importProfile', "Import Profile..."), undefined, true, () => this.importProfile())); + actions.push(toAction({ + id: 'importProfile', + label: localize('importProfile', "Import Profile..."), + run: () => this.importProfile() + })); return actions; } @@ -1036,7 +1047,7 @@ class ProfileIconRenderer extends ProfilePropertyRenderer { return; } iconSelectBox.clearInput(); - hoverWidget = this.hoverService.showHover({ + hoverWidget = this.hoverService.showInstantHover({ content: iconSelectBox.domNode, target: iconElement, position: { diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts index 6cd4321e..5ef8a898 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfilesEditorModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action, IAction, Separator } from '../../../../base/common/actions.js'; +import { Action, IAction, Separator, toAction } from '../../../../base/common/actions.js'; import { Emitter } from '../../../../base/common/event.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; @@ -262,11 +262,12 @@ export abstract class AbstractUserDataProfileElement extends Disposable { checkbox: undefined, resourceType: r, openAction: children.length - ? new Action('_open', - localize('open', "Open to the Side"), - ThemeIcon.asClassName(Codicon.goToFile), - true, - () => children[0]?.openAction?.run()) + ? toAction({ + id: '_open', + label: localize('open', "Open to the Side"), + class: ThemeIcon.asClassName(Codicon.goToFile), + run: () => children[0]?.openAction?.run() + }) : undefined }; })); @@ -309,11 +310,16 @@ export abstract class AbstractUserDataProfileElement extends Disposable { description: isString(child.description) ? child.description : undefined, resource: URI.revive(child.resourceUri), icon: child.themeIcon, - openAction: new Action('_openChild', localize('open', "Open to the Side"), ThemeIcon.asClassName(Codicon.goToFile), true, async () => { - if (child.parent.type === ProfileResourceType.Extensions) { - await this.commandService.executeCommand('extension.open', child.handle, undefined, true, undefined, true); - } else if (child.resourceUri) { - await this.commandService.executeCommand(API_OPEN_EDITOR_COMMAND_ID, child.resourceUri, [SIDE_GROUP], undefined); + openAction: toAction({ + id: '_openChild', + label: localize('open', "Open to the Side"), + class: ThemeIcon.asClassName(Codicon.goToFile), + run: async () => { + if (child.parent.type === ProfileResourceType.Extensions) { + await this.commandService.executeCommand('extension.open', child.handle, undefined, true, undefined, true); + } else if (child.resourceUri) { + await this.commandService.executeCommand(API_OPEN_EDITOR_COMMAND_ID, child.resourceUri, [SIDE_GROUP], undefined); + } } }), actions: { @@ -1036,13 +1042,13 @@ export class UserDataProfilesEditorModel extends EditorModel { )); primaryActions.push(createAction); if (isWeb && copyFrom instanceof URI && isProfileURL(copyFrom)) { - primaryActions.push(new Action( + primaryActions.push(disposables.add(new Action( 'userDataProfile.createInDesktop', localize('import in desktop', "Create in {0}", this.productService.nameLong), undefined, true, () => this.openerService.open(copyFrom, { openExternal: true }) - )); + ))); } const cancelAction = disposables.add(new Action( 'userDataProfile.cancel', diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index 3da71ce1..10d8716c 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -13,7 +13,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { localize } from '../../../../nls.js'; import { isWeb } from '../../../../base/common/platform.js'; import { UserDataSyncTrigger } from './userDataSyncTrigger.js'; -import { Action } from '../../../../base/common/actions.js'; +import { toAction } from '../../../../base/common/actions.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IHostService } from '../../../services/host/browser/host.js'; @@ -42,8 +42,16 @@ class UserDataSyncReportIssueContribution extends Disposable implements IWorkben message, actions: { primary: [ - new Action('Show Sync Logs', localize('show sync logs', "Show Log"), undefined, true, () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID)), - new Action('Restart', isWeb ? localize('reload', "Reload") : localize('restart', "Restart"), undefined, true, () => this.hostService.restart()) + toAction({ + id: 'Show Sync Logs', + label: localize('show sync logs', "Show Log"), + run: () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID) + }), + toAction({ + id: 'Restart', + label: isWeb ? localize('reload', "Reload") : localize('restart', "Restart"), + run: () => this.hostService.restart() + }) ] } }); @@ -58,7 +66,11 @@ class UserDataSyncReportIssueContribution extends Disposable implements IWorkben source: error.operationId ? localize('settings sync', "Settings Sync. Operation Id: {0}", error.operationId) : undefined, actions: { primary: [ - new Action('Show Sync Logs', localize('show sync logs', "Show Log"), undefined, true, () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID)), + toAction({ + id: 'Show Sync Logs', + label: localize('show sync logs', "Show Log"), + run: () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID) + }) ] } }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 8f2a4d37..21d1c9b2 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from '../../../../base/common/actions.js'; +import { toAction } from '../../../../base/common/actions.js'; import { getErrorMessage, isCancellationError } from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable, IDisposable } from '../../../../base/common/lifecycle.js'; @@ -249,7 +249,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo severity: Severity.Info, message: localize('session expired', "Settings sync was turned off because current session is expired, please sign in again to turn on sync."), actions: { - primary: [new Action('turn on sync', localize('turn on sync', "Turn on Settings Sync..."), undefined, true, () => this.turnOn())] + primary: [toAction({ + id: 'turn on sync', + label: localize('turn on sync', "Turn on Settings Sync..."), + run: () => this.turnOn() + })] } }); break; @@ -258,7 +262,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo severity: Severity.Info, message: localize('turned off', "Settings sync was turned off from another device, please turn on sync again."), actions: { - primary: [new Action('turn on sync', localize('turn on sync', "Turn on Settings Sync..."), undefined, true, () => this.turnOn())] + primary: [toAction({ + id: 'turn on sync', + label: localize('turn on sync', "Turn on Settings Sync..."), + run: () => this.turnOn() + })] } }); break; @@ -292,8 +300,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo message: operationId ? `${message} ${operationId}` : message, actions: { primary: [ - new Action('Show Sync Logs', localize('show sync logs', "Show Log"), undefined, true, () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID)), - new Action('Report Issue', localize('report issue', "Report Issue"), undefined, true, () => this.workbenchIssueService.openReporter()) + toAction({ + id: 'Show Sync Logs', + label: localize('show sync logs', "Show Log"), + run: () => this.commandService.executeCommand(SHOW_SYNC_LOG_COMMAND_ID) + }), + toAction({ + id: 'Report Issue', + label: localize('report issue', "Report Issue"), + run: () => this.workbenchIssueService.openReporter() + }) ] } }); @@ -305,8 +321,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo message: localize('error reset required', "Settings sync is disabled because your data in the cloud is older than that of the client. Please clear your data in the cloud before turning on sync."), actions: { primary: [ - new Action('reset', localize('reset', "Clear Data in Cloud..."), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), - new Action('show synced data', localize('show synced data action', "Show Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.showSyncActivity()) + toAction({ + id: 'reset', + label: localize('reset', "Clear Data in Cloud..."), + run: () => this.userDataSyncWorkbenchService.resetSyncedData() + }), + toAction({ + id: 'show synced data', + label: localize('show synced data action', "Show Synced Data"), + run: () => this.userDataSyncWorkbenchService.showSyncActivity() + }) ] } }); @@ -337,7 +361,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo severity: Severity.Info, message: localize('service changed and turned off', "Settings sync was turned off because {0} now uses a separate service. Please turn on sync again.", this.productService.nameLong), actions: { - primary: [new Action('turn on sync', localize('turn on sync', "Turn on Settings Sync..."), undefined, true, () => this.turnOn())] + primary: [toAction({ + id: 'turn on sync', + label: localize('turn on sync', "Turn on Settings Sync..."), + run: () => this.turnOn() + })] } }); } @@ -351,8 +379,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo severity: Severity.Error, message: operationId ? `${message} ${operationId}` : message, actions: { - primary: [new Action('open sync file', localize('open file', "Open {0} File", getSyncAreaLabel(resource)), undefined, true, - () => resource === SyncResource.Settings ? this.preferencesService.openUserSettings({ jsonEditor: true }) : this.preferencesService.openGlobalKeybindingSettings(true))] + primary: [toAction({ + id: 'open sync file', + label: localize('open file', "Open {0} File", getSyncAreaLabel(resource)), + run: () => resource === SyncResource.Settings ? this.preferencesService.openUserSettings({ jsonEditor: true }) : this.preferencesService.openGlobalKeybindingSettings(true) + })] } }); } @@ -408,8 +439,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo severity: Severity.Error, message: localize('errorInvalidConfiguration', "Unable to sync {0} because the content in the file is not valid. Please open the file and correct it.", errorArea.toLowerCase()), actions: { - primary: [new Action('open sync file', localize('open file', "Open {0} File", errorArea), undefined, true, - () => source === SyncResource.Settings ? this.preferencesService.openUserSettings({ jsonEditor: true }) : this.preferencesService.openGlobalKeybindingSettings(true))] + primary: [toAction({ + id: 'open sync file', + label: localize('open file', "Open {0} File", errorArea), + run: () => source === SyncResource.Settings ? this.preferencesService.openUserSettings({ jsonEditor: true }) : this.preferencesService.openGlobalKeybindingSettings(true) + })] } }); this.invalidContentErrorDisposables.set(key, toDisposable(() => { @@ -494,8 +528,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo message: localize('error reset required while starting sync', "Settings sync cannot be turned on because your data in the cloud is older than that of the client. Please clear your data in the cloud before turning on sync."), actions: { primary: [ - new Action('reset', localize('reset', "Clear Data in Cloud..."), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), - new Action('show synced data', localize('show synced data action', "Show Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.showSyncActivity()) + toAction({ + id: 'reset', + label: localize('reset', "Clear Data in Cloud..."), + run: () => this.userDataSyncWorkbenchService.resetSyncedData() + }), + toAction({ + id: 'show synced data', + label: localize('show synced data action', "Show Synced Data"), + run: () => this.userDataSyncWorkbenchService.showSyncActivity() + }) ] } }); @@ -529,7 +571,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const items = this.getConfigureSyncQuickPickItems(); quickPick.items = items; - quickPick.selectedItems = items.filter(item => this.userDataSyncEnablementService.isResourceEnabled(item.id)); + quickPick.selectedItems = items.filter(item => this.userDataSyncEnablementService.isResourceEnabled(item.id, true)); let accepted: boolean = false; disposables.add(Event.any(quickPick.onDidAccept, quickPick.onDidCustom)(() => { accepted = true; @@ -575,8 +617,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo label: getSyncAreaLabel(SyncResource.Profiles), }]; - // if the `reusable prompt` feature is enabled, add appropriate item to the list - if (PromptsConfig.enabled(this.configService)) { + // if the `reusable prompt` feature is enabled and in vscode + // insiders, add the `Prompts` resource item to the list + if (PromptsConfig.enabled(this.configService) === true) { result.push({ id: SyncResource.Prompts, label: getSyncAreaLabel(SyncResource.Prompts) @@ -761,7 +804,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: 'workbench.userData.actions.turningOn', - title: localize('turnin on sync', "Turning on Settings Sync..."), + title: localize('turning on sync', "Turning on Settings Sync..."), precondition: ContextKeyExpr.false(), menu: [{ group: '3_configuration', diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts index 8ced2a6f..eb5f2a8e 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncViews.ts @@ -20,7 +20,7 @@ import { IDialogService, IFileDialogService } from '../../../../platform/dialogs import { Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Action } from '../../../../base/common/actions.js'; +import { toAction } from '../../../../base/common/actions.js'; import { IUserDataSyncWorkbenchService, CONTEXT_SYNC_STATE, getSyncAreaLabel, CONTEXT_ACCOUNT_STATE, AccountStatus, CONTEXT_ENABLE_ACTIVITY_VIEWS, SYNC_TITLE, SYNC_CONFLICTS_VIEW_ID, CONTEXT_ENABLE_SYNC_CONFLICTS_VIEW, CONTEXT_HAS_CONFLICTS } from '../../../services/userDataSync/common/userDataSync.js'; import { IUserDataSyncMachinesService, IUserDataSyncMachine, isWebPlatform } from '../../../../platform/userDataSync/common/userDataSyncMachines.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; @@ -380,7 +380,11 @@ abstract class UserDataSyncActivityViewDataProvider implements ITre message: error.message, actions: { primary: [ - new Action('reset', localize('reset', "Reset Synced Data"), undefined, true, () => this.userDataSyncWorkbenchService.resetSyncedData()), + toAction({ + id: 'reset', + label: localize('reset', "Reset Synced Data"), + run: () => this.userDataSyncWorkbenchService.resetSyncedData() + }), ] } }); diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 12614595..7a9a38c9 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -34,6 +34,7 @@ import { THREAD_STORAGE_KEY } from '../common/storageKeys.js'; import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; import { timeout } from '../../../../base/common/async.js'; import { deepClone } from '../../../../base/common/objects.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; // related to retrying when LLM message has error @@ -136,19 +137,50 @@ export type IsRunningType = | 'LLM' // the LLM is currently streaming | 'tool' // whether a tool is currently running | 'awaiting_user' // awaiting user call + | 'idle' // nothing is running now, but the chat should still appear like it's going (used in-between calls) | undefined export type ThreadStreamState = { [threadId: string]: undefined | { - // state related to streaming (not just when streaming) - isRunning?: IsRunningType; // whether or not actually running the agent loop (can be running and not streaming, like if it's calling a tool and awaiting user response) + isRunning: undefined; error?: { message: string, fullError: Error | null, }; - - // streaming related - when streaming message - streamingToken?: string; - displayContentSoFar?: string; - reasoningSoFar?: string; - toolCallSoFar?: RawToolCallObj; + llmInfo?: undefined; + toolInfo?: undefined; + interrupt?: undefined; + } | { // an assistant message is being written + isRunning: 'LLM'; + error?: undefined; + llmInfo: { + displayContentSoFar: string; + reasoningSoFar: string; + toolCallSoFar: RawToolCallObj | null; + }; + toolInfo?: undefined; + interrupt: Promise<() => void>; // calling this should have no effect on state - would be too confusing. it just cancels the tool + } | { // a tool is being run + isRunning: 'tool'; + error?: undefined; + llmInfo?: undefined; + toolInfo: { + toolName: ToolName; + toolParams: ToolCallParams[ToolName]; + id: string; + content: string; + rawParams: RawToolParamsObj; + }; + interrupt: Promise<() => void>; + } | { + isRunning: 'awaiting_user'; + error?: undefined; + llmInfo?: undefined; + toolInfo?: undefined; + interrupt?: undefined; + } | { + isRunning: 'idle'; + error?: undefined; + llmInfo?: undefined; + toolInfo?: undefined; + interrupt?: undefined; } } @@ -215,10 +247,11 @@ export interface IChatThreadService { // codespan links (link to symbols in the markdown) getCodespanLink(opts: { codespanStr: string, messageIdx: number, threadId: string }): CodespanLocationLink | undefined; addCodespanLink(opts: { newLinkText: string, newLinkLocation: CodespanLocationLink, messageIdx: number, threadId: string }): void; - generateCodespanLink(opts: { codespanStr: string, threadId: string }): Promise + generateCodespanLink(opts: { codespanStr: string, threadId: string }): Promise; + getRelativeStr(uri: URI): string | undefined // entry pts - stopRunning(threadId: string): void; + abortRunning(threadId: string): Promise; dismissStreamError(threadId: string): void; // call to edit a message @@ -265,6 +298,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IEditCodeService private readonly _editCodeService: IEditCodeService, @INotificationService private readonly _notificationService: INotificationService, @IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -343,32 +377,41 @@ class ChatThreadService extends Disposable implements IChatThreadService { // this should be the only place this.state = ... appears besides constructor private _setState(state: Partial, affectsCurrent: boolean) { - this.state = { + const newState = { ...this.state, ...state } + + this.state = newState + if (affectsCurrent) this._onDidChangeCurrentThread.fire() + + + // if we just switched to a thread, update its current stream state if it's not streaming to possibly streaming + const threadId = newState.currentThreadId + const streamState = this.streamState[threadId] + if (streamState?.isRunning === undefined && !streamState?.error) { + + // set streamState + const messages = newState.allThreads[threadId]?.messages + const lastMessage = messages && messages[messages.length - 1] + // if awaiting user but stream state doesn't indicate it (happens if restart Void) + if (lastMessage && lastMessage.role === 'tool' && lastMessage.type === 'tool_request') + this._setStreamState(threadId, { isRunning: 'awaiting_user', }) + + // if running now but stream state doesn't indicate it (happens if restart Void), cancel that last tool + if (lastMessage && lastMessage.role === 'tool' && lastMessage.type === 'running_now') { + this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params }) + } + + } + } - private _setStreamState(threadId: string, state: Partial>, behavior: 'set' | 'merge') { - if (state === undefined) - delete this.streamState[threadId] - - else { - if (behavior === 'merge') { - this.streamState[threadId] = { - ...this.streamState[threadId], - ...state - } - } - else if (behavior === 'set') { - this.streamState[threadId] = state - } - else throw new Error(`setStreamState`) - } - + private _setStreamState(threadId: string, state: ThreadStreamState[string]) { + this.streamState[threadId] = state this._onDidChangeStreamState.fire({ threadId }) } @@ -435,40 +478,35 @@ class ChatThreadService extends Disposable implements IChatThreadService { const errorMessage = this.errMsgs.rejected this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams }) - this._setStreamState(threadId, {}, 'set') + this._setStreamState(threadId, undefined) } - stopRunning(threadId: string) { + async abortRunning(threadId: string) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen - // reject the tool for the user if relevant - this.rejectLatestToolRequest(threadId) - - // interrupt the tool if relevant - this._currentlyRunningToolInterruptor[threadId]?.() - - // interrupt assistant message - const isRunning = this.streamState[threadId]?.isRunning - if (isRunning === 'LLM') { - // abort the stream first so it doesn't change any state - const displayContentSoFar = this.streamState[threadId]?.displayContentSoFar ?? '' - const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar - - const llmCancelToken = this.streamState[threadId]?.streamingToken - if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } + // interrupt any effects + const interrupt = await this.streamState[threadId]?.interrupt + interrupt?.() + // add assistant message + if (this.streamState[threadId]?.isRunning === 'LLM') { + const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - - if (toolCallSoFar) { - this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) - } - + if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) this._addUserCheckpoint({ threadId }) } + // add tool that's running + else if (this.streamState[threadId]?.isRunning === 'tool') { + const { toolName, toolParams, id, content, rawParams } = this.streamState[threadId].toolInfo + this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null }) + } + // reject the tool for the user if relevant + else if (this.streamState[threadId]?.isRunning === 'awaiting_user') { + this.rejectLatestToolRequest(threadId) + } - this._setStreamState(threadId, {}, 'set') + this._setStreamState(threadId, undefined) } @@ -479,7 +517,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {} + // private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {} // returns true when the tool call is waiting for user approval @@ -498,7 +536,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (!opts.preapproved) { // skip this if pre-approved // 1. validate tool params try { - const params = await this._toolsService.validateParams[toolName](opts.unvalidatedToolParams) + const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams) toolParams = params } catch (error) { const errorMessage = getErrorMessage(error) @@ -529,30 +567,38 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 3. call the tool - this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams }) + // this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') + const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams } as const + this._updateLatestTool(threadId, runningTool) + let interrupted = false + let resolveInterruptor: (r: () => void) => void = () => { } + const interruptorPromise = new Promise<() => void>(res => { resolveInterruptor = res }) try { + + // set stream state + this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams } }) + const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) - this._currentlyRunningToolInterruptor[threadId] = () => { - interrupted = true; - interruptTool?.(); - delete this._currentlyRunningToolInterruptor[threadId]; - } - toolResult = await result // ts is bad... await is needed + const interruptor = () => { interrupted = true; interruptTool?.() } + resolveInterruptor(interruptor) + + toolResult = await result if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here } catch (error) { - delete this._currentlyRunningToolInterruptor[threadId] + resolveInterruptor(() => { }) // resolve for the sake of it if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here - const errorMessage = getErrorMessage(error) this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) return {} } + finally { + this._setStreamState(threadId, undefined) + } // 4. stringify the result to give to the LLM try { @@ -565,8 +611,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 5. add to history and keep going this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams }) - delete this._currentlyRunningToolInterruptor[threadId] - return {} }; @@ -590,8 +634,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { // above just defines helpers, below starts the actual function const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here - // clear any previous error - this._setStreamState(threadId, { error: undefined }, 'set') + // not running at start, clear state + this._setStreamState(threadId, { isRunning: 'idle' }) let nMessagesSent = 0 let shouldSendAnotherMessage = true @@ -600,7 +644,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { // before enter loop, call tool if (callThisToolFirst) { const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params }) - if (interrupted) return + this._setStreamState(threadId, undefined) + if (interrupted) { return } } // tool use loop @@ -617,18 +662,20 @@ class ChatThreadService extends Disposable implements IChatThreadService { chatMode }) - let aborted = false - let shouldRetry = true + let shouldRetryLLM = true let nAttempts = 0 - while (shouldRetry) { - shouldRetry = false + while (shouldRetryLLM) { + // if (this.streamState[threadId]?.isRunning === 'LLM' || ) { + // // if already streaming, stop + // console.log('returning...', this.streamState[threadId]) + // return + // } + shouldRetryLLM = false let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval) const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) - // send llm message - this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge') - + let aborted = false const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', chatMode, @@ -638,84 +685,83 @@ class ChatThreadService extends Disposable implements IChatThreadService { logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, separateSystemMessage: separateSystemMessage, onText: ({ fullText, fullReasoning, toolCall }) => { - this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge') + this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall ?? null }, interrupt: Promise.resolve(() => { if (llmCancelToken) this._llmMessageService.abort(llmCancelToken) }) }) }, onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => { this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning }) - this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge') + this._setStreamState(threadId, undefined) resMessageIsDonePromise(toolCall) // resolve with tool calls }, onError: async (error) => { - const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? '' - const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + if (this.streamState[threadId]?.isRunning !== 'LLM') { + console.log('Unexpected onError when', this.streamState[threadId]?.isRunning) + return + } - this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge') if (nAttempts < CHAT_RETRIES) { nAttempts += 1 - shouldRetry = true - await timeout(RETRY_DELAY) + shouldRetryLLM = true + this._setStreamState(threadId, undefined) // clear later so can be interrupted resMessageIsDonePromise() } else { - // const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar // add assistant's message to chat history, and clear selection - this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - this._setStreamState(threadId, { error }, 'set') + const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo + this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) + this._setStreamState(threadId, { isRunning: undefined, error }) resMessageIsDonePromise() } }, onAbort: () => { // stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it) aborted = true + this._setStreamState(threadId, { isRunning: 'idle' }) resMessageIsDonePromise() this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode }) }, }) - // should never happen, just for safety - if (llmCancelToken === null) { - this._setStreamState(threadId, { - error: { message: 'There was an unexpected error when sending your chat message.', fullError: null } - }, 'set') + // mark as streaming + if (!llmCancelToken) { + this._setStreamState(threadId, { isRunning: undefined, error: { message: 'There was an unexpected error when sending your chat message.', fullError: null } }) break } - this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message + + this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: '', reasoningSoFar: '', toolCallSoFar: null }, interrupt: Promise.resolve(() => this._llmMessageService.abort(llmCancelToken)) }) const toolCall = await messageIsDonePromise // wait for message to complete - this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done - // this is a complete hack to make it so if an error loop was aborted, we stop (because onAbort does not get called if error happens instantly) - // maybe we should remove all the abort stuff and just make it so that we only go by state? - if (!this.streamState[threadId]?.isRunning) { return } - - if (aborted) { return } - if (shouldRetry) { continue } + if (aborted) { + this._setStreamState(threadId, undefined) + return + } + if (shouldRetryLLM) { + this._setStreamState(threadId, { isRunning: 'idle' }) + await timeout(RETRY_DELAY) + continue + } + this._setStreamState(threadId, { isRunning: 'idle' }) // call tool if there is one - const tool: RawToolCallObj | undefined = toolCall - if (tool) { - const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams }) + if (toolCall) { + const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams }) - // stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools. - // just detect tool interruption which is the same as chat interruption right now - if (!this.streamState[threadId]?.isRunning) { return } - if (aborted) { return } - if (interrupted) { return } + if (interrupted) { + this._setStreamState(threadId, undefined) + return + } + this._setStreamState(threadId, { isRunning: 'idle' }) - if (awaitingUserApproval) { - console.log('awaiting...') - isRunningWhenEnd = 'awaiting_user' - } - else { - shouldSendAnotherMessage = true - } + if (awaitingUserApproval) { isRunningWhenEnd = 'awaiting_user' } + else { shouldSendAnotherMessage = true } } } // end while (attempts) } // end while (send message) // if awaiting user approval, keep isRunning true, else end isRunning - this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge') + this._setStreamState(threadId, { isRunning: isRunningWhenEnd }) // add checkpoint before the next user message if (!isRunningWhenEnd) @@ -1039,7 +1085,7 @@ We only need to do it for files that were edited since `from`, ie files between } dismissStreamError(threadId: string): void { - this._setStreamState(threadId, { error: undefined }, 'merge') + this._setStreamState(threadId, undefined) } @@ -1047,13 +1093,11 @@ We only need to do it for files that were edited since `from`, ie files between const thread = this.state.allThreads[threadId] if (!thread) return // should never happen - const llmCancelToken = this.streamState[threadId]?.streamingToken // currently streaming LLM on this thread - if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning) { - // if about to call the other LLM, just wait for and stop now - return + // interrupt existing stream + if (this.streamState[threadId]?.isRunning) { + console.log('stopping....') + await this.abortRunning(threadId) } - // stop it (this simply resolves the promise to free up space) - if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) // add dummy before this message to keep checkpoint before user message idea consistent if (thread.messages.length === 0) { @@ -1166,6 +1210,20 @@ We only need to do it for files that were edited since `from`, ie files between } + + getRelativeStr = (uri: URI) => { + const isInside = this._workspaceContextService.isInsideWorkspace(uri) + if (isInside) { + const f = this._workspaceContextService.getWorkspace().folders.find(f => uri.fsPath.startsWith(f.uri.fsPath)) + if (f) { return uri.fsPath.replace(f.uri.fsPath, '') } + else { return undefined } + } + else { + return undefined + } + } + + // gets the location of codespan link so the user can click on it generateCodespanLink: IChatThreadService['generateCodespanLink'] = async ({ codespanStr: _codespanStr, threadId }) => { @@ -1270,7 +1328,7 @@ We only need to do it for files that were edited since `from`, ie files between false, // searchOnlyEditableRange false, // isRegex true, // matchCase - ' ', // wordSeparators + null, //' ', // wordSeparators true // captureMatches ); diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 9810f4bf..65b45d33 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -483,7 +483,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess }) const includeXMLToolDefinitions = !specialToolFormat - const persistentTerminalIDs = this.terminalToolService.listTerminalIds() + const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds() const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, includeXMLToolDefinitions }) return systemMessage } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index e3fadf98..c7e972b8 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -14,6 +14,7 @@ import { URI } from '../../../../../../../base/common/uri.js' import { isAbsolute } from '../../../../../../../base/common/path.js' import { separateOutFirstLine } from '../../../../common/helpers/util.js' import { BlockCode } from '../util/inputs.js' +import { CodespanLocationLink } from '../../../../common/chatThreadServiceTypes.js' export type ChatMessageLocation = { @@ -113,7 +114,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string const [didComputeCodespanLink, setDidComputeCodespanLink] = useState(false) - let link = undefined + let link: CodespanLocationLink | undefined = undefined if (rawText.endsWith('`')) { // if codespan was completed // get link from cache @@ -121,12 +122,12 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string if (link === undefined) { // if no link, generate link and add to cache - (chatThreadService.generateCodespanLink({ codespanStr: text, threadId }) + chatThreadService.generateCodespanLink({ codespanStr: text, threadId }) .then(link => { chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId }) setDidComputeCodespanLink(true) // rerender }) - ) + } } @@ -543,6 +544,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation, ...options }: { string: string, inPTag?: boolean, codeURI?: URI, chatMessageLocation: ChatMessageLocation | undefined } & RenderTokenOptions) => { + string = string.replaceAll('\n•', '\n\n•') const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer return ( <> diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 2a531815..191b7212 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -12,6 +12,9 @@ import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js'; import { useRefState } from '../util/helpers.js'; import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; + + + export const QuickEditChat = ({ diffareaid, onChangeHeight, diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index e71ef004..d0188f37 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -30,8 +30,8 @@ import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames } import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; import ErrorBoundary from './ErrorBoundary.js'; import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js'; -import { terminalNameOfId } from '../../../terminalToolService.js'; +import { persistentTerminalNameOfId } from '../../../terminalToolService.js'; export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps) => { @@ -352,14 +352,13 @@ export const VoidChatArea: React.FC = ({
-
+
{featureName === 'Chat' && }
)} -
{isStreaming && loadingIcon} @@ -382,7 +381,6 @@ export const VoidChatArea: React.FC = ({ - type ButtonProps = ButtonHTMLAttributes const DEFAULT_BUTTON_SIZE = 22; export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required>) => { @@ -501,6 +499,7 @@ export const getBasename = (pathStr: string, parts: number = 1) => { return allParts.slice(-parts).join('/') } + export const SelectedFiles = ( { type, selections, setSelections, showProspectiveSelections, messageIdx, }: | { type: 'past', selections: StagingSelectionItem[]; setSelections?: undefined, showProspectiveSelections?: undefined, messageIdx: number, } @@ -681,8 +680,6 @@ export const SelectedFiles = ( } - - type ToolHeaderParams = { icon?: React.ReactNode; title: React.ReactNode; @@ -1018,7 +1015,8 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr // cancel any streams on this thread const threadId = chatThreadsService.state.currentThreadId - chatThreadsService.stopRunning(threadId) + + await chatThreadsService.abortRunning(threadId) // update state setIsBeingEdited(false) @@ -1035,9 +1033,9 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr requestAnimationFrame(() => _scrollToBottom?.()) } - const onAbort = () => { + const onAbort = async () => { const threadId = chatThreadsService.state.currentThreadId - chatThreadsService.stopRunning(threadId) + await chatThreadsService.abortRunning(threadId) } const onKeyDown = (e: KeyboardEvent) => { @@ -1065,244 +1063,244 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr setSelections={setStagingSelections} > setIsDisabled(!text)} - onFocus={() => { - setIsFocused(true) - chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); - }} - onBlur={() => { - setIsFocused(false) - }} - onKeyDown={onKeyDown} - fnsRef={textAreaFnsRef} - multiline={true} - /> - - } + enableAtToMention + ref={setTextAreaRef} + className='min-h-[81px] max-h-[500px] px-0.5' + placeholder="Edit your message..." + onChangeText={(text) => setIsDisabled(!text)} + onFocus={() => { + setIsFocused(true) + chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); + }} + onBlur={() => { + setIsFocused(false) + }} + onKeyDown={onKeyDown} + fnsRef={textAreaFnsRef} + multiline={true} + /> + +} - const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 +const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 - return
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > -
{ if (mode === 'display') { onOpenEdit() } }} - > - {chatbubbleContents} -
+ ${isCheckpointGhost && !isMsgAfterCheckpoint ? 'opacity-50 pointer-events-none' : ''} + `} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} +> +
{ if (mode === 'display') { onOpenEdit() } }} + > + {chatbubbleContents} +
-
- { - if (mode === 'display') { - onOpenEdit() - } else if (mode === 'edit') { - onCloseEdit() - } - }} - /> -
+
+ { + if (mode === 'display') { + onOpenEdit() + } else if (mode === 'edit') { + onCloseEdit() + } + }} + /> +
-
+
} const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => { - return
:first-child]:!mt-0 - [&>:last-child]:!mb-0 +[&>:first-child]:!mt-0 +[&>:last-child]:!mb-0 - prose-h1:text-[14px] - prose-h1:my-4 +prose-h1:text-[14px] +prose-h1:my-4 - prose-h2:text-[13px] - prose-h2:my-4 +prose-h2:text-[13px] +prose-h2:my-4 - prose-h3:text-[13px] - prose-h3:my-3 +prose-h3:text-[13px] +prose-h3:my-3 - prose-h4:text-[13px] - prose-h4:my-2 +prose-h4:text-[13px] +prose-h4:my-2 - prose-p:my-2 - prose-p:leading-snug - prose-hr:my-2 +prose-p:my-2 +prose-p:leading-snug +prose-hr:my-2 - prose-ul:my-2 - prose-ul:pl-4 - prose-ul:list-outside - prose-ul:list-disc - prose-ul:leading-snug +prose-ul:my-2 +prose-ul:pl-4 +prose-ul:list-outside +prose-ul:list-disc +prose-ul:leading-snug - prose-ol:my-2 - prose-ol:pl-4 - prose-ol:list-outside - prose-ol:list-decimal - prose-ol:leading-snug +prose-ol:my-2 +prose-ol:pl-4 +prose-ol:list-outside +prose-ol:list-decimal +prose-ol:leading-snug - marker:text-inherit +marker:text-inherit - prose-blockquote:pl-2 - prose-blockquote:my-2 +prose-blockquote:pl-2 +prose-blockquote:my-2 - prose-code:text-void-fg-3 - prose-code:text-[12px] - prose-code:before:content-none - prose-code:after:content-none +prose-code:text-void-fg-3 +prose-code:text-[12px] +prose-code:before:content-none +prose-code:after:content-none - prose-pre:text-[12px] - prose-pre:p-2 - prose-pre:my-2 +prose-pre:text-[12px] +prose-pre:p-2 +prose-pre:my-2 - prose-table:text-[13px] - '> - {children} -
+prose-table:text-[13px] +'> + {children} +
} const ProseWrapper = ({ children }: { children: React.ReactNode }) => { - return
- {children} -
+> + {children} + } const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted, messageIdx }: { chatMessage: ChatMessage & { role: 'assistant' }, isCheckpointGhost: boolean, messageIdx: number, isCommitted: boolean }) => { - const accessor = useAccessor() - const chatThreadsService = accessor.get('IChatThreadService') +const accessor = useAccessor() +const chatThreadsService = accessor.get('IChatThreadService') - const reasoningStr = chatMessage.reasoning?.trim() || null - const hasReasoning = !!reasoningStr - const isDoneReasoning = !!chatMessage.displayContent - const thread = chatThreadsService.getCurrentThread() +const reasoningStr = chatMessage.reasoning?.trim() || null +const hasReasoning = !!reasoningStr +const isDoneReasoning = !!chatMessage.displayContent +const thread = chatThreadsService.getCurrentThread() - const chatMessageLocation: ChatMessageLocation = { - threadId: thread.id, - messageIdx: messageIdx, - } +const chatMessageLocation: ChatMessageLocation = { + threadId: thread.id, + messageIdx: messageIdx, +} - const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning - if (isEmpty) return null +const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning +if (isEmpty) return null - return <> - {/* reasoning token */} - {hasReasoning && -
- - - - - -
- } +return <> + {/* reasoning token */} + {hasReasoning && +
+ + + + + +
+ } - {/* assistant message */} - {chatMessage.displayContent && -
- - - -
- } - + {/* assistant message */} + {chatMessage.displayContent && +
+ + + +
+ } + } const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneReasoning: boolean, isStreaming: boolean, children: React.ReactNode }) => { - const isDone = isDoneReasoning || !isStreaming - const isWriting = !isDone - const [isOpen, setIsOpen] = useState(isWriting) - useEffect(() => { - if (!isWriting) setIsOpen(false) // if just finished reasoning, close - }, [isWriting]) - return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> - -
- {children} -
-
-
+const isDone = isDoneReasoning || !isStreaming +const isWriting = !isDone +const [isOpen, setIsOpen] = useState(isWriting) +useEffect(() => { + if (!isWriting) setIsOpen(false) // if just finished reasoning, close +}, [isWriting]) +return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> + +
+ {children} +
+
+
} @@ -1311,11 +1309,12 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe // should either be past or "-ing" tense, not present tense. Eg. when the LLM searches for something, the user expects it to say "I searched for X" or "I am searching for X". Not "I search X". const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => { - return - {item} - - +return + {item} + + } + const titleOfToolName = { 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, 'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, @@ -1327,12 +1326,16 @@ const titleOfToolName = { 'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, 'rewrite_file': { done: `Wrote file`, proposed: 'Write file', running: loadingTitleWrapper('Writing file') }, 'run_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, + 'run_persistent_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, + 'open_persistent_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') }, 'kill_persistent_terminal': { done: `Killed terminal`, proposed: 'Kill terminal', running: loadingTitleWrapper('Killing terminal') }, + 'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') }, 'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') }, } as const satisfies Record + const getTitle = (toolMessage: Pick): React.ReactNode => { const t = toolMessage if (!toolNames.includes(t.name as ToolName)) return t.name // good measure @@ -1419,7 +1422,12 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName const toolParams = _toolParams as ToolCallParams['run_command'] return { desc1: `"${toolParams.command}"`, - desc1Info: toolParams.persistentTerminalId + } + }, + 'run_persistent_command': () => { + const toolParams = _toolParams as ToolCallParams['run_persistent_command'] + return { + desc1: `"${toolParams.command}"`, } }, 'open_persistent_terminal': () => { @@ -1638,6 +1646,105 @@ const CanceledTool = ({ toolName }: { toolName: ToolName }) => { } +const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({ + toolMessage: Exclude, { type: 'invalid_params' }> + type: 'run_command' +} | { + toolMessage: Exclude, { type: 'invalid_params' }> + type: | 'run_persistent_command' +})) => { + const accessor = useAccessor() + + const commandService = accessor.get('ICommandService') + const terminalToolsService = accessor.get('ITerminalToolService') + const toolsService = accessor.get('IToolsService') + const terminalService = accessor.get('ITerminalService') + const isError = toolMessage.type === 'tool_error' + const title = getTitle(toolMessage) + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) + const icon = null + const streamState = useChatThreadsStreamState(threadId) + + const divRef = useRef(null) + + const isRejected = toolMessage.type === 'rejected' + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + + + const effect = async () => { + if (streamState?.isRunning !== 'tool') return + if (type !== 'run_command' || toolMessage.type !== 'running_now') return; + + // wait for the interruptor so we know it's running + + await streamState?.interrupt + const container = divRef.current; + if (!container) return; + + const terminal = terminalToolsService.getTemporaryTerminal(toolMessage.params.terminalId); + if (!terminal) return; + + terminal.detachFromElement(); + terminal.attachToElement(container); + + // Listen for size changes + const resizeObserver = new ResizeObserver((entries) => { + const height = entries[0].borderBoxSize[0].blockSize + const width = entries[0].borderBoxSize[0].inlineSize + // Layout terminal to fit container dimensions + if (typeof terminal.layout === 'function') { + terminalService.setActiveInstance(terminal) + terminal.attachToElement(container); + terminal.layout({ width, height }); + + } + }) + + resizeObserver.observe(container); + return () => { terminal.detachFromElement(); resizeObserver?.disconnect(); } + } + + useEffect(() => { + effect() + }, [terminalToolsService, toolMessage, toolMessage.type, type]); + + if (toolMessage.type === 'success') { + const { result } = toolMessage + + // it's unclear that this is a button and not an icon. + // componentParams.desc2 = { terminalToolsService.openTerminal(terminalId) }} + // /> + + let msg: string + if (type === 'run_command') msg = toolsService.stringOfResult['run_command'](toolMessage.params, result) + else msg = toolsService.stringOfResult['run_persistent_command'](toolMessage.params, result) + + componentParams.children = +
+ +
+
+ } + else if (toolMessage.type === 'tool_error') { + const { result } = toolMessage + componentParams.children = + {result} + + } + else if (toolMessage.type === 'running_now') { + componentParams.children =
+ } + else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_request') { + } + + return <> + + +} + + type ResultWrapper = (props: { toolMessage: Exclude, { type: 'invalid_params' }>, messageIdx: number, threadId: string }) => React.ReactNode const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } } = { 'read_file': { @@ -1650,13 +1757,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor); const icon = null - if (toolMessage.type === 'tool_request') return null - if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) { const start = toolMessage.params.startLine === null ? `1` : `${toolMessage.params.startLine}` @@ -1695,17 +1802,17 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null - if (toolMessage.type === 'tool_request') return null - if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } if (params.uri) { const rel = getRelative(params.uri, accessor) - if (rel) componentParams.info = `Searches inside ${rel}` + if (rel) componentParams.info = `Only search in ${rel}` } if (toolMessage.type === 'success') { @@ -1743,17 +1850,17 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null - if (toolMessage.type === 'tool_request') return null - if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } if (params.uri) { const rel = getRelative(params.uri, accessor) - if (rel) componentParams.info = `Searches inside ${rel}` + if (rel) componentParams.info = `Only search in ${rel}` } if (toolMessage.type === 'success') { @@ -1793,19 +1900,19 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const accessor = useAccessor() const commandService = accessor.get('ICommandService') const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' const title = getTitle(toolMessage) const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null - if (toolMessage.type === 'tool_request') return null - if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } if (params.includePattern) { - componentParams.info = `Searches inside ${params.includePattern}` + componentParams.info = `Only search in ${params.includePattern}` } if (toolMessage.type === 'success') { @@ -1842,26 +1949,24 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const accessor = useAccessor() const commandService = accessor.get('ICommandService') const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' const title = getTitle(toolMessage) const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null - if (toolMessage.type === 'tool_request') return null - if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } if (params.searchInFolder || params.isRegex) { let info: string[] = [] if (params.searchInFolder) { const rel = getRelative(params.searchInFolder, accessor) - if (rel) info.push(`Searches inside ${rel}`) - } - if (params.isRegex) { - info.push(`Uses regex search`) + if (rel) info.push(`Only search in ${rel}`) } + if (params.isRegex) { info.push(`Uses regex search`) } componentParams.info = info.join('; ') } @@ -1899,15 +2004,22 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const accessor = useAccessor(); const toolsService = accessor.get('IToolsService'); const title = getTitle(toolMessage); + const isError = toolMessage.type === 'tool_error'; + const isRejected = toolMessage.type === 'rejected' const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor); const icon = null; - if (toolMessage.type === 'tool_request' || toolMessage.type === 'rejected' || toolMessage.type === 'running_now') return null; + if (toolMessage.type === 'tool_request') return null // do not show past requests + if (toolMessage.type === 'running_now') return null // do not show running - const isError = toolMessage.type === 'tool_error'; const { rawParams, params } = toolMessage; - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon }; - if (params.isRegex) componentParams.info = 'Uses regex search' + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected }; + + const infoarr: string[] = [] + const uriStr = getRelative(params.uri, accessor) + if (uriStr) infoarr.push(uriStr) + if (params.isRegex) infoarr.push('Uses regex search') + componentParams.info = infoarr.join('; ') if (toolMessage.type === 'success') { const { result } = toolMessage; // result is array of snippets @@ -1945,13 +2057,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null - if (toolMessage.type === 'tool_request') return null - if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } componentParams.info = getRelative(uri, accessor) // full path @@ -2066,7 +2178,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, - 'rewrite_file': { + 'rewrite_file': { resultWrapper: (params) => { return } @@ -2080,82 +2192,39 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, // --- 'run_command': { + resultWrapper: (params) => { + return + } + }, + + 'run_persistent_command': { + resultWrapper: (params) => { + return + } + }, + 'open_persistent_terminal': { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() - const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') - const toolsService = accessor.get('IToolsService') - const isError = toolMessage.type === 'tool_error' - const title = getTitle(toolMessage) + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) + const title = getTitle(toolMessage) const icon = null + if (toolMessage.type === 'tool_request') return null // do not show past requests + if (toolMessage.type === 'running_now') return null // do not show running + + const isError = toolMessage.type === 'tool_error' const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } - const { command, persistentTerminalId } = params - if (persistentTerminalId) { - componentParams.desc2 = terminalNameOfId(persistentTerminalId) - componentParams.desc2OnClick = () => terminalToolsService.focusTerminal(persistentTerminalId) - } - - - if (toolMessage.type === 'success') { - const { result } = toolMessage - - // it's unclear that this is a button and not an icon. - // componentParams.desc2 = { terminalToolsService.openTerminal(terminalId) }} - // /> - const msg = toolsService.stringOfResult['run_command'](params, result) - - componentParams.children = -
-
- {`Ran command: `} - {command} -
- {msg.length &&
- {`Result: `} - {msg} -
} -
-
- } - else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_error' || toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { - if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.children = {result} - } - } - - return - } - }, - - 'open_persistent_terminal': { - resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const terminalToolsService = accessor.get('ITerminalToolService') - - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const title = getTitle(toolMessage) - const icon = null - - if (toolMessage.type === 'tool_request') return null - if (toolMessage.type === 'rejected') return null // will never happen, not rejectable - if (toolMessage.type === 'running_now') return null // do not show running - - const isError = toolMessage.type === 'tool_error' - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } - + componentParams.info = params.cwd ? `Running in ${getRelative(URI.file(params.cwd), accessor)}` : '' if (toolMessage.type === 'success') { const { result } = toolMessage const { persistentTerminalId } = result - componentParams.desc1 = terminalNameOfId(persistentTerminalId) - componentParams.onClick = () => terminalToolsService.focusTerminal(persistentTerminalId) + componentParams.desc1 = persistentTerminalNameOfId(persistentTerminalId) + componentParams.onClick = () => terminalToolsService.focusPersistentTerminal(persistentTerminalId) } else if (toolMessage.type === 'tool_error') { const { result } = toolMessage @@ -2179,18 +2248,18 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const title = getTitle(toolMessage) const icon = null - if (toolMessage.type === 'tool_request') return null - if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, } + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } if (toolMessage.type === 'success') { const { persistentTerminalId } = params - componentParams.desc1 = terminalNameOfId(persistentTerminalId) - componentParams.onClick = () => terminalToolsService.focusTerminal(persistentTerminalId) + componentParams.desc1 = persistentTerminalNameOfId(persistentTerminalId) + componentParams.onClick = () => terminalToolsService.focusPersistentTerminal(persistentTerminalId) } else if (toolMessage.type === 'tool_error') { const { result } = toolMessage @@ -2642,6 +2711,7 @@ const CommandBarInChat = () => { } + const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => { const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined @@ -2673,6 +2743,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => } + export const SidebarChat = () => { const textAreaRef = useRef(null) const textAreaFnsRef = useRef(null) @@ -2709,9 +2780,7 @@ export const SidebarChat = () => { const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) const isRunning = currThreadStreamState?.isRunning const latestError = currThreadStreamState?.error - const displayContentSoFar = currThreadStreamState?.displayContentSoFar - const toolCallSoFar = currThreadStreamState?.toolCallSoFar - const reasoningSoFar = currThreadStreamState?.reasoningSoFar + const { displayContentSoFar, toolCallSoFar, reasoningSoFar } = currThreadStreamState?.llmInfo ?? {} // this is just if it's currently being generated, NOT if it's currently running const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit) @@ -2726,8 +2795,7 @@ export const SidebarChat = () => { const sidebarRef = useRef(null) const scrollContainerRef = useRef(null) - - const onSubmit = useCallback(async (_forceSubmit?: string) => { + const onSubmit = useCallback(async (_forceSubmit?: string) => { if (isDisabled && !_forceSubmit) return if (isRunning) return @@ -2749,9 +2817,9 @@ export const SidebarChat = () => { }, [chatThreadsService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState]) - const onAbort = () => { + const onAbort = async () => { const threadId = currentThread.id - chatThreadsService.stopRunning(threadId) + await chatThreadsService.abortRunning(threadId) } const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_L_ACTION_ID)?.getLabel() @@ -2866,7 +2934,6 @@ export const SidebarChat = () => { - const inputChatArea = { ) } - - - diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 6bd07485..fdeddf76 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -49,6 +49,7 @@ import { INativeHostService } from '../../../../../../../platform/native/common/ import { IEditCodeService } from '../../../editCodeServiceInterface.js' import { IToolsService } from '../../../toolsService.js' import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.js' +import { ITerminalService } from '../../../../../terminal/browser/terminal.js' import { ISearchService } from '../../../../../../services/search/common/search.js' @@ -146,8 +147,8 @@ export const _registerServices = (accessor: ServicesAccessor) => { colorThemeState = themeService.getColorTheme().type disposables.push( - themeService.onDidColorThemeChange(({ theme }) => { - colorThemeState = theme.type + themeService.onDidColorThemeChange(({ type }) => { + colorThemeState = type colorThemeStateListeners.forEach(l => l(colorThemeState)) }) ) @@ -221,6 +222,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => { INativeHostService: accessor.get(INativeHostService), IToolsService: accessor.get(IToolsService), IConvertToLLMMessageService: accessor.get(IConvertToLLMMessageService), + ITerminalService: accessor.get(ITerminalService), } as const return reactAccessor diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index cc1b3e21..a52e1516 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -5,10 +5,12 @@ import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js'; +import { URI } from '../../../../base/common/uri.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { TerminalExitReason, TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; -import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; +import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { ITerminalService, ITerminalInstance, ICreateTerminalOptions } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js'; import { TerminalResolveReason } from '../common/toolsServiceTypes.js'; @@ -17,13 +19,16 @@ import { TerminalResolveReason } from '../common/toolsServiceTypes.js'; export interface ITerminalToolService { readonly _serviceBrand: undefined; - listTerminalIds(): string[]; - runCommand(command: string, bgTerminalId: string | null): Promise<{ terminalId: string, resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>; - focusTerminal(terminalId: string): Promise - terminalExists(terminalId: string): boolean + listPersistentTerminalIds(): string[]; + runCommand(command: string, opts: { type: 'persistent', persistentTerminalId: string } | { type: 'ephemeral', cwd: string | null, terminalId: string }): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>; + focusPersistentTerminal(terminalId: string): Promise + persistentTerminalExists(terminalId: string): boolean - createTerminal(): Promise - killTerminal(terminalId: string): Promise + createPersistentTerminal(opts: { cwd: string | null }): Promise + killPersistentTerminal(terminalId: string): Promise + + getPersistentTerminal(terminalId: string): ITerminalInstance | undefined + getTemporaryTerminal(terminalId: string): ITerminalInstance | undefined } export const ITerminalToolService = createDecorator('TerminalToolService'); @@ -39,11 +44,11 @@ function isCommandComplete(output: string) { } -export const terminalNameOfId = (id: string) => { +export const persistentTerminalNameOfId = (id: string) => { if (id === '1') return 'Void Agent' return `Void Agent (${id})` } -export const idOfTerminalName = (name: string) => { +export const idOfPersistentTerminalName = (name: string) => { if (name === 'Void Agent') return '1' const match = name.match(/Void Agent \((\d+)\)/) @@ -55,10 +60,12 @@ export const idOfTerminalName = (name: string) => { export class TerminalToolService extends Disposable implements ITerminalToolService { readonly _serviceBrand: undefined; - private terminalInstanceOfId: Record = {} + private persistentTerminalInstanceOfId: Record = {} + private temporaryTerminalInstanceOfId: Record = {} constructor( @ITerminalService private readonly terminalService: ITerminalService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, ) { super(); @@ -66,8 +73,8 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ const initializeTerminal = (terminal: ITerminalInstance) => { // when exit, remove const d = terminal.onExit(() => { - const terminalId = idOfTerminalName(terminal.title) - if (terminalId !== null && (terminalId in this.terminalInstanceOfId)) delete this.terminalInstanceOfId[terminalId] + const terminalId = idOfPersistentTerminalName(terminal.title) + if (terminalId !== null && (terminalId in this.persistentTerminalInstanceOfId)) delete this.persistentTerminalInstanceOfId[terminalId] d.dispose() }) } @@ -75,8 +82,8 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ // initialize any terminals that are already open for (const terminal of terminalService.instances) { - const proposedTerminalId = idOfTerminalName(terminal.title) - if (proposedTerminalId) this.terminalInstanceOfId[proposedTerminalId] = terminal + const proposedTerminalId = idOfPersistentTerminalName(terminal.title) + if (proposedTerminalId) this.persistentTerminalInstanceOfId[proposedTerminalId] = terminal initializeTerminal(terminal) } @@ -88,32 +95,36 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ } - listTerminalIds() { - return Object.keys(this.terminalInstanceOfId) + listPersistentTerminalIds() { + return Object.keys(this.persistentTerminalInstanceOfId) } getValidNewTerminalId(): string { // {1 2 3} # size 3, new=4 // {1 3 4} # size 3, new=2 // 1 <= newTerminalId <= n + 1 - const n = Object.keys(this.terminalInstanceOfId).length; + const n = Object.keys(this.persistentTerminalInstanceOfId).length; if (n === 0) return '1' for (let i = 1; i <= n + 1; i++) { const potentialId = i + ''; - if (!(potentialId in this.terminalInstanceOfId)) return potentialId; + if (!(potentialId in this.persistentTerminalInstanceOfId)) return potentialId; } throw new Error('This should never be reached by pigeonhole principle'); } - async createTerminal() { - // create new terminal and return its ID - const terminalId = this.getValidNewTerminalId(); - const terminal = await this.terminalService.createTerminal({ - location: TerminalLocation.Panel, - config: { name: terminalNameOfId(terminalId), title: terminalNameOfId(terminalId) }, - }) + private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'] }) { + const { cwd: override_cwd, config } = props + + const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri + + // create new terminal and return its ID + const terminal = await this.terminalService.createTerminal({ + cwd: cwd, + location: TerminalLocation.Panel, + config: config, + }) // when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning const disposables: IDisposable[] = [] @@ -130,26 +141,49 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ await Promise.any([waitForMount, waitForTimeout,]) disposables.forEach(d => d.dispose()) - this.terminalInstanceOfId[terminalId] = terminal + return terminal + + } + + createPersistentTerminal: ITerminalToolService['createPersistentTerminal'] = async ({ cwd }) => { + const terminalId = this.getValidNewTerminalId(); + const config = { name: persistentTerminalNameOfId(terminalId), title: persistentTerminalNameOfId(terminalId) } + const terminal = await this._createTerminal({ cwd, config, }) + this.persistentTerminalInstanceOfId[terminalId] = terminal return terminalId } - async killTerminal(terminalId: string) { - const terminal = this.terminalInstanceOfId[terminalId] - if (!terminal) throw new Error(`Kill Terminal: Terminal with ID ${terminalId} does not exist.`); - terminal.dispose(TerminalExitReason.Extension) - delete this.terminalInstanceOfId[terminalId] + async killPersistentTerminal(terminalId: string) { + const terminal = this.persistentTerminalInstanceOfId[terminalId] + if (!terminal) throw new Error(`Kill Terminal: Terminal with ID ${terminalId} did not exist.`); + terminal.dispose() + delete this.persistentTerminalInstanceOfId[terminalId] return } - terminalExists(terminalId: string): boolean { - return terminalId in this.terminalInstanceOfId + persistentTerminalExists(terminalId: string): boolean { + return terminalId in this.persistentTerminalInstanceOfId } - focusTerminal: ITerminalToolService['focusTerminal'] = async (terminalId) => { + getTemporaryTerminal(terminalId: string): ITerminalInstance | undefined { if (!terminalId) return - const terminal = this.terminalInstanceOfId[terminalId] + const terminal = this.temporaryTerminalInstanceOfId[terminalId] + if (!terminal) return // should never happen + return terminal + } + + getPersistentTerminal(terminalId: string): ITerminalInstance | undefined { + if (!terminalId) return + const terminal = this.persistentTerminalInstanceOfId[terminalId] + if (!terminal) return // should never happen + return terminal + } + + + focusPersistentTerminal: ITerminalToolService['focusPersistentTerminal'] = async (terminalId) => { + if (!terminalId) return + const terminal = this.persistentTerminalInstanceOfId[terminalId] if (!terminal) return // should never happen this.terminalService.setActiveInstance(terminal) await this.terminalService.focusActiveInstance() @@ -158,30 +192,38 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ - runCommand: ITerminalToolService['runCommand'] = async (command, bgTerminalId) => { + runCommand: ITerminalToolService['runCommand'] = async (command, params) => { + const { type } = params await this.terminalService.whenConnected; let terminal: ITerminalInstance const disposables: IDisposable[] = [] - const isBG = bgTerminalId !== null - let terminalId: string - if (isBG) { // BG process - terminal = this.terminalInstanceOfId[bgTerminalId]; - if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${bgTerminalId} does not exist.`); - terminalId = bgTerminalId + const isPersistent = type === 'persistent' + + if (isPersistent) { // BG process + const { persistentTerminalId } = params + terminal = this.persistentTerminalInstanceOfId[persistentTerminalId]; + if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${persistentTerminalId} did not exist.`); } else { - terminalId = await this.createTerminal() - terminal = this.terminalInstanceOfId[terminalId] - if (!terminal) throw new Error(`Unexpected error: Terminal could not be created.`) + const { cwd } = params + terminal = await this._createTerminal({ cwd: cwd, config: { name: 'Void Temporary Terminal', title: 'Void Temporary Terminal' } }) + this.temporaryTerminalInstanceOfId[params.terminalId] = terminal } + const interrupt = () => { + terminal.dispose() + if (!isPersistent) + delete this.temporaryTerminalInstanceOfId[params.terminalId] + } const waitForResult = async () => { - // focus the terminal about to run - this.terminalService.setActiveInstance(terminal) - await this.terminalService.focusActiveInstance() + if (isPersistent) { + // focus the terminal about to run + this.terminalService.setActiveInstance(terminal) + await this.terminalService.focusActiveInstance() + } let result: string = '' let resolveReason: TerminalResolveReason | undefined = undefined @@ -207,7 +249,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ // send the command here await terminal.sendText(command, true) - const waitUntilInterrupt = isBG ? + const waitUntilInterrupt = isPersistent ? // timeout after X seconds new Promise((res) => { setTimeout(() => { @@ -237,8 +279,8 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ await Promise.any([waitUntilDone, waitUntilInterrupt]) disposables.forEach(d => d.dispose()) - if (!isBG) { - await this.killTerminal(terminalId) + if (!isPersistent) { + interrupt() } if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.') @@ -259,7 +301,10 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ } const resPromise = waitForResult() - return { terminalId, resPromise } + return { + interrupt, + resPromise, + } } diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index fa4b6787..188bb658 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -18,6 +18,7 @@ import { timeout } from '../../../../base/common/async.js' import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js' import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js' import { IVoidSettingsService } from '../common/voidSettingsService.js' +import { generateUuid } from '../../../../base/common/uuid.js' // tool use for AI @@ -84,14 +85,6 @@ const validateNumber = (numStr: unknown, opts: { default: number | null }) => { return opts.default } -const validateRecursiveParamStr = (paramsUnknown: unknown) => { - if (!paramsUnknown) return false - if (typeof paramsUnknown !== 'string') throw new Error('Invalid LLM output format: Error calling tool: provided params must be a string.') - const params = paramsUnknown - const isRecursive = params.includes('r') - return isRecursive -} - const validateProposedTerminalId = (terminalIdUnknown: unknown) => { if (!terminalIdUnknown) throw new Error(`A value for terminalID must be specified, but the value was "${terminalIdUnknown}"`) const terminalId = terminalIdUnknown + '' @@ -234,9 +227,9 @@ export class ToolsService implements IToolsService { }, delete_file_or_folder: (params: RawToolParamsObj) => { - const { uri: uriUnknown, params: paramsStr } = params + const { uri: uriUnknown, is_recursive: isRecursiveUnknown } = params const uri = validateURI(uriUnknown) - const isRecursive = validateRecursiveParamStr(paramsStr) + const isRecursive = validateBoolean(isRecursiveUnknown, { default: false }) const uriStr = validateStr('uri', uriUnknown) const isFolder = checkIfIsFolder(uriStr) return { uri, isRecursive, isFolder } @@ -259,14 +252,23 @@ export class ToolsService implements IToolsService { // --- run_command: (params: RawToolParamsObj) => { - const { command: commandUnknown, persistent_terminal_id: terminalIdUnknown } = params; + const { command: commandUnknown, cwd: cwdUnknown } = params + const command = validateStr('command', commandUnknown) + const cwd = validateOptionalStr('cwd', cwdUnknown) + const terminalId = generateUuid() + return { command, cwd, terminalId } + }, + run_persistent_command: (params: RawToolParamsObj) => { + const { command: commandUnknown, persistent_terminal_id: persistentTerminalIdUnknown } = params; const command = validateStr('command', commandUnknown); - const persistentTerminalId = terminalIdUnknown ? validateProposedTerminalId(terminalIdUnknown) : null; + const persistentTerminalId = validateProposedTerminalId(persistentTerminalIdUnknown) return { command, persistentTerminalId }; }, - open_persistent_terminal: (_params: RawToolParamsObj) => { + open_persistent_terminal: (params: RawToolParamsObj) => { + const { cwd: cwdUnknown } = params; + const cwd = validateOptionalStr('cwd', cwdUnknown) // No parameters needed; will open a new background terminal - return {}; + return { cwd }; }, kill_persistent_terminal: (params: RawToolParamsObj) => { const { persistent_terminal_id: terminalIdUnknown } = params; @@ -425,20 +427,21 @@ export class ToolsService implements IToolsService { return { result: lintErrorsPromise } }, // --- - run_command: async ({ command, persistentTerminalId }) => { - const { terminalId, resPromise } = await this.terminalToolService.runCommand(command, persistentTerminalId) - const interruptTool = () => { - this.terminalToolService.killTerminal(terminalId) - } - return { result: resPromise, interruptTool } + run_command: async ({ command, cwd, terminalId }) => { + const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'ephemeral', cwd, terminalId }) + return { result: resPromise, interruptTool: interrupt } }, - open_persistent_terminal: async () => { - const persistentTerminalId = await this.terminalToolService.createTerminal() + run_persistent_command: async ({ command, persistentTerminalId }) => { + const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'persistent', persistentTerminalId }) + return { result: resPromise, interruptTool: interrupt } + }, + open_persistent_terminal: async ({ cwd }) => { + const persistentTerminalId = await this.terminalToolService.createPersistentTerminal({ cwd }) return { result: { persistentTerminalId } } }, kill_persistent_terminal: async ({ persistentTerminalId }) => { // Close the background terminal by sending exit - await this.terminalToolService.killTerminal(persistentTerminalId) + await this.terminalToolService.killPersistentTerminal(persistentTerminalId) return { result: {} } }, @@ -512,33 +515,32 @@ export class ToolsService implements IToolsService { return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}` }, run_command: (params, result) => { - const { - resolveReason, - result: result_, - } = result - const { persistentTerminalId } = params - + const { resolveReason, result: result_, } = result // success if (resolveReason.type === 'done') { - const desc = persistentTerminalId ? ` in terminal ${persistentTerminalId}` : '' - return `Terminal command executed and finished${desc}. Result (exit code ${resolveReason.exitCode}):\n${result_}` - } - - // bg command - if (persistentTerminalId !== null) { - if (resolveReason.type === 'timeout') { - return `Terminal command is running in terminal ${persistentTerminalId}. Here are the current outputs (after ${MAX_TERMINAL_BG_COMMAND_TIME} seconds):\n${result_}` - } + return `${result_}\n(exit code ${resolveReason.exitCode})` } // normal command - else { - if (resolveReason.type === 'timeout') { - return `Terminal command ran, but was interrupted after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. Full output:\n${result_}` - } + if (resolveReason.type === 'timeout') { + return `${result_}\nTerminal command ran, but was interrupted by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. To try with more time, open a persistent terminal and run the command there.` } - throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`) }, + + run_persistent_command: (params, result) => { + const { resolveReason, result: result_, } = result + const { persistentTerminalId } = params + // success + if (resolveReason.type === 'done') { + return `${result_}\n(exit code ${resolveReason.exitCode})` + } + // bg command + if (resolveReason.type === 'timeout') { + return `${result_}\nTerminal command is running in terminal ${persistentTerminalId}. The given outputs are the results after ${MAX_TERMINAL_BG_COMMAND_TIME} seconds.` + } + throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`) + }, + open_persistent_terminal: (_params, result) => { const { persistentTerminalId } = result; return `Successfully created persistent terminal. persistentTerminalId="${persistentTerminalId}"`; diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 521d74bb..7fad5d59 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -88,7 +88,7 @@ export const defaultModelsOfProvider = { ], gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini 'gemini-2.5-pro-exp-03-25', - 'gemini-2.0-flash', + 'gemini-2.5-flash-preview-04-17', 'gemini-2.0-flash-lite', ], deepseek: [ // https://api-docs.deepseek.com/quick_start/pricing @@ -633,6 +633,15 @@ const xAISettings: VoidStaticProviderInfo = { // ---------------- GEMINI ---------------- const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing + 'gemini-2.5-flash-preview-04-17': { + contextWindow: 1_048_576, + maxOutputTokens: 8_192, + cost: { input: 0.15, output: .60 }, // TODO $3.50 output with thinking not included + downloadable: false, + supportsFIM: false, + supportsSystemMessage: 'system-role', + reasoningCapabilities: false, + }, 'gemini-2.5-pro-exp-03-25': { contextWindow: 1_048_576, maxOutputTokens: 8_192, diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index a69e87a1..f08f41bc 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -7,7 +7,7 @@ import { EndOfLinePreference } from '../../../../../editor/common/model.js'; import { StagingSelectionItem } from '../chatThreadServiceTypes.js'; import { os } from '../helpers/systemInfo.js'; import { RawToolParamsObj } from '../sendLLMMessageTypes.js'; -import { approvalTypeOfToolName, ToolResultType } from '../toolsServiceTypes.js'; +import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js'; import { IVoidModelService } from '../voidModelService.js'; import { ChatMode } from '../voidSettingsTypes.js'; @@ -34,7 +34,6 @@ export const MAX_TERMINAL_BG_COMMAND_TIME = 5 export const MAX_PREFIX_SUFFIX_CHARS = 20_000 - export const ORIGINAL = `<<<<<<< ORIGINAL` export const DIVIDER = `=======` export const FINAL = `>>>>>>> UPDATED` @@ -166,20 +165,25 @@ const paginationParam = { +const terminalDescHelper = `You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use edit_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.` + +const cwdHelper = 'Optional. The directory in which to run the command. Defaults to the first workspace folder.' + +export type SnakeCase = + // exact acronym URI + S extends 'URI' ? 'uri' + // suffix URI: e.g. 'rootURI' -> snakeCase('root') + '_uri' + : S extends `${infer Prefix}URI` ? `${SnakeCase}_uri` + // default: for each char, prefix '_' on uppercase letters + : S extends `${infer C}${infer Rest}` + ? `${C extends Lowercase ? C : `_${Lowercase}`}${SnakeCase}` + : S; + +export type SnakeCaseKeys> = { + [K in keyof T as SnakeCase>]: T[K] +}; -// export type SnakeCase = -// // exact acronym URI -// S extends 'URI' ? 'uri' -// // suffix URI: e.g. 'rootURI' -> snakeCase('root') + '_uri' -// : S extends `${infer Prefix}URI` ? `${SnakeCase}_uri` -// // default: for each char, prefix '_' on uppercase letters -// : S extends `${infer C}${infer Rest}` -// ? `${C extends Lowercase ? C : `_${Lowercase}`}${SnakeCase}` -// : S; -// export type SnakeCaseKeys> = { -// [K in keyof T as SnakeCase>]: T[K] -// }; const applyToolDescription = (type: 'edit tool' | 'chat suggestion') => `\ ${type === 'edit tool' ? 'A' : 'a'} code diff describing the change to make to the file. \ @@ -190,157 +194,169 @@ Use comments like "// ... existing code ..." to condense your writing. \ Here's an example of a good output:\n${type === 'edit tool' ? editToolDescriptionExample : chatSuggestionDiffExample}` -export const voidTools = { - // export const voidTools - // : { - // [T in keyof ToolCallParams]: { - // name: string; - // description: string; - // params: { - // [paramName in keyof SnakeCaseKeys]: { description: string } - // } - // } - // } - // = { - // --- context-gathering (read/search/list) --- - - read_file: { - name: 'read_file', - description: `Returns full contents of a given file.`, - params: { - ...uriParam('file'), - start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' }, - end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' }, - ...paginationParam, - }, - }, - - ls_dir: { - name: 'ls_dir', - description: `Lists all files and folders in the given URI.`, - params: { - uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` }, - ...paginationParam, - }, - }, - - get_dir_tree: { - name: 'get_dir_tree', - description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `, - params: { - ...uriParam('folder') +// export const voidTools = { +export const voidTools + : { + [T in keyof ToolCallParams]: { + name: string; + description: string; + // more params can be generated than exist here, but these params must be a subset of them + params: Partial<{ [paramName in keyof SnakeCaseKeys]: { description: string } }> } - }, - - // pathname_search: { - // name: 'pathname_search', - // description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`, - - search_pathnames_only: { - name: 'search_pathnames_only', - description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`, - params: { - query: { description: `Your query for the search.` }, - include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' }, - ...paginationParam, - }, - }, - - - - search_for_files: { - name: 'search_for_files', - description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`, - params: { - query: { description: `Your query for the search.` }, - search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' }, - is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }, - ...paginationParam, - }, - }, - - // add new search_in_file tool - search_in_file: { - name: 'search_in_file', - description: `Returns an array of all the start line numbers where the content appears in the file.`, - params: { - ...uriParam('file'), - query: { description: 'The string or regex to search for in the file.' }, - is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' } - } - }, - - read_lint_errors: { - name: 'read_lint_errors', - description: `Returns all lint errors on a given file.`, - params: { - ...uriParam('file'), - }, - }, - - // --- editing (create/delete) --- - - create_file_or_folder: { - name: 'create_file_or_folder', - description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`, - params: { - ...uriParam('file or folder'), - }, - }, - - delete_file_or_folder: { - name: 'delete_file_or_folder', - description: `Delete a file or folder at the given path.`, - params: { - ...uriParam('file or folder'), - params: { description: 'Optional. Return -r here to delete recursively.' } - }, - }, - - edit_file: { - name: 'edit_file', - description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`, - params: { - ...uriParam('file'), - search_replace_blocks: { description: replaceTool_description } - }, - }, - - rewrite_file: { - name: 'rewrite_file', - description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`, - params: { - ...uriParam('file'), - new_content: { description: `The new contents of the file. Must be a string.` } - }, - }, - - - run_command: { - name: 'run_command', - description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use edit_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`, - params: { - command: { description: 'The terminal command to run.' }, - persistent_terminal_id: { description: 'Optional. Runs the command in the persistent terminal that you created with open_persistent_terminal.' }, - }, - }, - - open_persistent_terminal: { - name: 'open_persistent_terminal', - description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`, - params: {} - }, - kill_persistent_terminal: { - name: 'kill_persistent_terminal', - description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`, - params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } } } + = { + // --- context-gathering (read/search/list) --- + + read_file: { + name: 'read_file', + description: `Returns full contents of a given file.`, + params: { + ...uriParam('file'), + start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' }, + end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' }, + ...paginationParam, + }, + }, + + ls_dir: { + name: 'ls_dir', + description: `Lists all files and folders in the given URI.`, + params: { + uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` }, + ...paginationParam, + }, + }, + + get_dir_tree: { + name: 'get_dir_tree', + description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `, + params: { + ...uriParam('folder') + } + }, + + // pathname_search: { + // name: 'pathname_search', + // description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`, + + search_pathnames_only: { + name: 'search_pathnames_only', + description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`, + params: { + query: { description: `Your query for the search.` }, + include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' }, + ...paginationParam, + }, + }, - // go_to_definition - // go_to_usages -} satisfies { [T in keyof ToolResultType]: InternalToolInfo } + search_for_files: { + name: 'search_for_files', + description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`, + params: { + query: { description: `Your query for the search.` }, + search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' }, + is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }, + ...paginationParam, + }, + }, + + // add new search_in_file tool + search_in_file: { + name: 'search_in_file', + description: `Returns an array of all the start line numbers where the content appears in the file.`, + params: { + ...uriParam('file'), + query: { description: 'The string or regex to search for in the file.' }, + is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' } + } + }, + + read_lint_errors: { + name: 'read_lint_errors', + description: `Returns all lint errors on a given file.`, + params: { + ...uriParam('file'), + }, + }, + + // --- editing (create/delete) --- + + create_file_or_folder: { + name: 'create_file_or_folder', + description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`, + params: { + ...uriParam('file or folder'), + }, + }, + + delete_file_or_folder: { + name: 'delete_file_or_folder', + description: `Delete a file or folder at the given path.`, + params: { + ...uriParam('file or folder'), + is_recursive: { description: 'Optional. Return true to delete recursively.' } + }, + }, + + edit_file: { + name: 'edit_file', + description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`, + params: { + ...uriParam('file'), + search_replace_blocks: { description: replaceTool_description } + }, + }, + + rewrite_file: { + name: 'rewrite_file', + description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`, + params: { + ...uriParam('file'), + new_content: { description: `The new contents of the file. Must be a string.` } + }, + }, + run_command: { + name: 'run_command', + description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`, + params: { + command: { description: 'The terminal command to run.' }, + cwd: { description: cwdHelper }, + }, + }, + + run_persistent_command: { + name: 'run_persistent_command', + description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`, + params: { + command: { description: 'The terminal command to run.' }, + persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' }, + }, + }, + + + + open_persistent_terminal: { + name: 'open_persistent_terminal', + description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`, + params: { + cwd: { description: cwdHelper }, + } + }, + + + kill_persistent_terminal: { + name: 'kill_persistent_terminal', + description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`, + params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } } + } + + + // go_to_definition + // go_to_usages + + } satisfies { [T in keyof ToolResultType]: InternalToolInfo } export type ToolName = keyof ToolResultType @@ -370,19 +386,19 @@ const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => { return `${tools.map((t, i) => { const params = Object.keys(t.params).map(paramName => `<${paramName}>${t.params[paramName].description}`).join('\n') return `\ -${i + 1}. ${t.name} -Description: ${t.description} -Format: -<${t.name}>${!params ? '' : `\n${params}`} -` + ${i + 1}. ${t.name} + Description: ${t.description} + Format: + <${t.name}>${!params ? '' : `\n${params}`} + ` }).join('\n\n')}` } export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => { const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}`).join('\n') return `\ -<${toolName}>${!params ? '' : `\n${params}`} -` + <${toolName}>${!params ? '' : `\n${params}`} + ` .replace('\t', ' ') } @@ -393,22 +409,22 @@ const systemToolsXMLPrompt = (chatMode: ChatMode) => { if (!tools || tools.length === 0) return null const toolXMLDefinitions = (`\ -Available tools: + Available tools: -${toolCallDefinitionsXMLString(tools)}`) + ${toolCallDefinitionsXMLString(tools)}`) const toolCallXMLGuidelines = (`\ -Tool calling details: -- To call a tool, write its name and parameters in one of the XML formats specified above. -- After you write the tool call, you must STOP and WAIT for the result. -- All parameters are REQUIRED unless noted otherwise. -- You are only allowed to output ONE tool call, and it must be at the END of your response. -- Your tool call will be executed immediately, and the results will appear in the following user message.`) + Tool calling details: + - To call a tool, write its name and parameters in one of the XML formats specified above. + - After you write the tool call, you must STOP and WAIT for the result. + - All parameters are REQUIRED unless noted otherwise. + - You are only allowed to output ONE tool call, and it must be at the END of your response. + - Your tool call will be executed immediately, and the results will appear in the following user message.`) return `\ -${toolXMLDefinitions} + ${toolXMLDefinitions} -${toolCallXMLGuidelines}` + ${toolCallXMLGuidelines}` } // ======================================================== chat (normal, gather, agent) ======================================================== diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index c43f157a..b6c466f4 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -22,6 +22,7 @@ export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'term 'rewrite_file': 'edits', 'edit_file': 'edits', 'run_command': 'terminal', + 'run_persistent_command': 'terminal', } @@ -48,8 +49,9 @@ export type ToolCallParams = { 'create_file_or_folder': { uri: URI, isFolder: boolean }, 'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean }, // --- - 'run_command': { command: string; persistentTerminalId: string | null }, - 'open_persistent_terminal': {}, + 'run_command': { command: string; cwd: string | null, terminalId: string }, + 'open_persistent_terminal': { cwd: string | null }, + 'run_persistent_command': { command: string; persistentTerminalId: string }, 'kill_persistent_terminal': { persistentTerminalId: string }, } @@ -69,6 +71,7 @@ export type ToolResultType = { 'delete_file_or_folder': {}, // --- 'run_command': { result: string; resolveReason: TerminalResolveReason; }, + 'run_persistent_command': { result: string; resolveReason: TerminalResolveReason; }, 'open_persistent_terminal': { persistentTerminalId: string }, 'kill_persistent_terminal': {}, } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts index 3e2031f1..b90dbafa 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/extractGrammar.ts @@ -64,7 +64,7 @@ export const extractReasoningWrapper = ( // until found the second think tag, keep adding to fullReasoning if (!foundTag2) { const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1]) - if (endsWithTag2) { + if (endsWithTag2 && endsWithTag2 !== thinkTags[1]) { // if ends with any partial part (full is fine) // console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar }) // wait until we get the full tag or know more return diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index a7c440d4..070e2f34 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -32,6 +32,15 @@ export const sendLLMMessage = async ({ // only captures number of messages and message "shape", no actual code, instructions, prompts, etc const captureLLMEvent = (eventId: string, extras?: object) => { + + let totalTokens = 0 + if (messagesType === 'chatMessages') { + for (const m of messages_) totalTokens += m.content.length + } + else { + totalTokens = messages_.prefix.length + messages_.suffix.length + } + metricsService.capture(eventId, { providerName, modelName, @@ -40,14 +49,12 @@ export const sendLLMMessage = async ({ ...messagesType === 'chatMessages' ? { numMessages: messages_?.length, messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), - origNumMessages: messages_?.length, - origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), } : messagesType === 'FIMMessage' ? { prefixLength: messages_.prefix.length, suffixLength: messages_.suffix.length, } : {}, - + totalTokens, ...loggingExtras, ...extras, }) @@ -94,10 +101,11 @@ export const sendLLMMessage = async ({ } abortRef_.current = onAbort + if (messagesType === 'chatMessages') - captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messages_?.[messages_.length - 1]?.content.length }) + captureLLMEvent(`${loggingName} - Sending Message`, { userMessageLength: messages_?.[messages_.length - 1]?.content.length }) else if (messagesType === 'FIMMessage') - captureLLMEvent(`${loggingName} - Sending FIM`, { prefixLen: messages_?.prefix?.length, suffixLen: messages_?.suffix?.length }) // TODO!!! add more metrics for FIM + captureLLMEvent(`${loggingName} - Sending FIM`, { prefixLen: messages_?.prefix?.length, suffixLen: messages_?.suffix?.length }) try { diff --git a/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts b/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts index bfc0a8c9..a7d5e559 100644 --- a/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts +++ b/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts @@ -96,7 +96,7 @@ export class MetricsMainService extends Disposable implements IMetricsService { // very important to await whenReady! await this._appStorage.whenReady - const { commit, version, voidVersion, quality } = this._productService + const { commit, version, voidVersion, release, quality } = this._productService const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts @@ -104,7 +104,8 @@ export class MetricsMainService extends Disposable implements IMetricsService { this._initProperties = { commit, vscodeVersion: version, - voidVersion, + voidVersion: voidVersion, + release, os, quality, distinctId: this.distinctId, diff --git a/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts b/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts index c8ef9e64..ab471631 100644 --- a/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts +++ b/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts @@ -94,7 +94,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ const data = await response.json(); const version = data.tag_name; - const myVersion = `${this._productService.voidVersion}.${this._productService.release}` + const myVersion = `${this._productService.version}.${this._productService.release}` const latestVersion = version const isUpToDate = myVersion === latestVersion // only makes sense if response.ok diff --git a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index 88f65d9c..40108569 100644 --- a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts @@ -75,7 +75,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { private _isDisposed = false; private readonly _onDidDispose = this._register(new Emitter()); - public onDidDispose = this._onDidDispose.event; + public readonly onDidDispose = this._onDidDispose.event; override dispose() { this._isDisposed = true; diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index ae571f8c..309df85a 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -230,7 +230,7 @@ return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.')); } - const swPath = encodeURI(`service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`); + const swPath = encodeURI(`service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&id=${ID}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`); navigator.serviceWorker.register(swPath) .then(async registration => { /** diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 6a1f3d45..e123bb92 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ + content="default-src 'none'; script-src 'sha256-nlLyDpnjtftJG2xvXh2vuy77l7xFTjfOz7Jnj1iXNmA=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> @@ -236,7 +236,7 @@ return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.')); } - const swPath = encodeURI(`service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`); + const swPath = encodeURI(`service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&id=${ID}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`); navigator.serviceWorker.register(swPath) .then(async registration => { /** diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index e5fa674e..3d74103e 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -18,6 +18,8 @@ const searchParams = new URL(location.toString()).searchParams; const remoteAuthority = searchParams.get('remoteAuthority'); +const ID = searchParams.get('id'); + /** * Origin used for resources */ @@ -233,12 +235,19 @@ sw.addEventListener('activate', (event) => { */ async function processResourceRequest(event, requestUrlComponents) { const client = await sw.clients.get(event.clientId); + let webviewId; if (!client) { - console.error('Could not find inner client for request'); - return notFound(); + const workerClient = await getWorkerClientForId(event.clientId); + if (!workerClient) { + console.error('Could not find inner client for request'); + return notFound(); + } else { + webviewId = getWebviewIdForClient(workerClient); + } + } else { + webviewId = getWebviewIdForClient(client); } - const webviewId = getWebviewIdForClient(client); if (!webviewId) { console.error('Could not resolve webview id'); return notFound(); @@ -439,6 +448,15 @@ async function processLocalhostRequest(event, requestUrl) { * @returns {string | null} */ function getWebviewIdForClient(client) { + // Refs https://github.com/microsoft/vscode/issues/244143 + // With PlzDedicatedWorker, worker subresources and blob wokers + // will use clients different from the window client. + // Since we cannot different a worker main resource from a worker subresource + // we will use the global webview ID passed in at the time of + // service worker registration. + if (client.type === 'worker' || client.type === 'sharedworker') { + return ID; + } const requesterClientUrl = new URL(client.url); return requesterClientUrl.searchParams.get('id'); } @@ -455,3 +473,16 @@ async function getOuterIframeClient(webviewId) { return hasExpectedPathName && clientUrl.searchParams.get('id') === webviewId; }); } + +/** + * @param {string} clientId + * @returns {Promise} + */ +async function getWorkerClientForId(clientId) { + const allDedicatedWorkerClients = await sw.clients.matchAll({ type: 'worker' }); + const allSharedWorkerClients = await sw.clients.matchAll({ type: 'sharedworker' }); + const allWorkerClients = [...allDedicatedWorkerClients, ...allSharedWorkerClients]; + return allWorkerClients.find(client => { + return client.id === clientId; + }); +} diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index e5b58995..a57f9c15 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -57,13 +57,16 @@ registerAction2(class extends Action2 { public run( accessor: ServicesAccessor, walkthroughID: string | { category: string; step: string } | undefined, - toSide: boolean | undefined + optionsOrToSide: { toSide?: boolean; inactive?: boolean } | boolean | undefined ) { const editorGroupsService = accessor.get(IEditorGroupsService); const instantiationService = accessor.get(IInstantiationService); const editorService = accessor.get(IEditorService); const commandService = accessor.get(ICommandService); + const toSide = typeof optionsOrToSide === 'object' ? optionsOrToSide.toSide : optionsOrToSide; + const inactive = typeof optionsOrToSide === 'object' ? optionsOrToSide.inactive : false; + if (walkthroughID) { const selectedCategory = typeof walkthroughID === 'string' ? walkthroughID : walkthroughID.category; let selectedStep: string | undefined; @@ -77,7 +80,7 @@ registerAction2(class extends Action2 { if (!selectedCategory && !selectedStep) { editorService.openEditor({ resource: GettingStartedInput.RESOURCE, - options: { preserveFocus: toSide ?? false } + options: { preserveFocus: toSide ?? false, inactive } }, toSide ? SIDE_GROUP : undefined); return; } @@ -101,7 +104,7 @@ registerAction2(class extends Action2 { editor.selectedCategory = selectedCategory; editor.selectedStep = selectedStep; editor.showWelcome = false; - group.openEditor(editor, { revealIfOpened: true }); + group.openEditor(editor, { revealIfOpened: true, inactive }); return; } } @@ -124,7 +127,7 @@ registerAction2(class extends Action2 { }]); } else { // else open respecting toSide - const options: GettingStartedEditorOptions = { selectedCategory: selectedCategory, selectedStep: selectedStep, showWelcome: false, preserveFocus: toSide ?? false }; + const options: GettingStartedEditorOptions = { selectedCategory: selectedCategory, selectedStep: selectedStep, showWelcome: false, preserveFocus: toSide ?? false, inactive }; editorService.openEditor({ resource: GettingStartedInput.RESOURCE, options @@ -136,7 +139,7 @@ registerAction2(class extends Action2 { } else { editorService.openEditor({ resource: GettingStartedInput.RESOURCE, - options: { preserveFocus: toSide ?? false } + options: { preserveFocus: toSide ?? false, inactive } }, toSide ? SIDE_GROUP : undefined); } } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 6be205a3..01778b2f 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -280,12 +280,14 @@ export class GettingStartedPage extends EditorPane { badgeelement.parentElement?.setAttribute('aria-checked', 'true'); badgeelement.classList.remove(...ThemeIcon.asClassNameArray(gettingStartedUncheckedCodicon)); badgeelement.classList.add('complete', ...ThemeIcon.asClassNameArray(gettingStartedCheckedCodicon)); + badgeelement.setAttribute('aria-label', localize('stepDone', "Checkbox for Step {0}: Completed", step.title)); } else { badgeelement.setAttribute('aria-checked', 'false'); badgeelement.parentElement?.setAttribute('aria-checked', 'false'); badgeelement.classList.remove('complete', ...ThemeIcon.asClassNameArray(gettingStartedCheckedCodicon)); badgeelement.classList.add(...ThemeIcon.asClassNameArray(gettingStartedUncheckedCodicon)); + badgeelement.setAttribute('aria-label', localize('stepNotDone', "Checkbox for Step {0}: Not completed", step.title)); } }); } @@ -719,8 +721,8 @@ export class GettingStartedPage extends EditorPane { const themeType = this.themeService.getColorTheme().type; const videoPath = media.path[themeType]; const videoPoster = media.poster ? media.poster[themeType] : undefined; - - const rawHTML = await this.detailsRenderer.renderVideo(videoPath, videoPoster); + const altText = media.altText ? media.altText : localize('videoAltText', "Video for {0}", stepToExpand.title); + const rawHTML = await this.detailsRenderer.renderVideo(videoPath, videoPoster, altText); this.webview.setHtml(rawHTML); let isDisposed = false; @@ -731,7 +733,7 @@ export class GettingStartedPage extends EditorPane { const themeType = this.themeService.getColorTheme().type; const videoPath = media.path[themeType]; const videoPoster = media.poster ? media.poster[themeType] : undefined; - const body = await this.detailsRenderer.renderVideo(videoPath, videoPoster); + const body = await this.detailsRenderer.renderVideo(videoPath, videoPoster, altText); if (!isDisposed) { // Make sure we weren't disposed of in the meantime this.webview.setHtml(body); @@ -867,7 +869,7 @@ export class GettingStartedPage extends EditorPane { const header = $('.header', {}, $('h1.product-name.caption', {}, this.productService.nameLong), - $('p.subtitle.description', {}, localize({ key: 'gettingStarted.editingEvolved', comment: ['Shown as subtitle on the Welcome page.'] }, "The Open Source Code Editor.")), // Void + $('p.subtitle.description', {}, localize({ key: 'gettingStarted.editingEvolved', comment: ['Shown as subtitle on the Welcome page.'] }, "Editing evolved")) ); const leftColumn = $('.categories-column.categories-column-left', {},); @@ -1178,7 +1180,7 @@ export class GettingStartedPage extends EditorPane { this.container.classList.toggle('height-constrained', size.height <= 600); this.container.classList.toggle('width-constrained', size.width <= 400); - this.container.classList.toggle('width-semi-constrained', size.width <= 800); + this.container.classList.toggle('width-semi-constrained', size.width <= 950); this.categoriesPageScrollbar?.scanDomNode(); this.detailsPageScrollbar?.scanDomNode(); @@ -1462,7 +1464,10 @@ export class GettingStartedPage extends EditorPane { 'data-done-step-id': step.id, 'x-dispatch': 'toggleStepCompletion:' + step.id, 'role': 'checkbox', - 'aria-checked': step.done ? 'true' : 'false' + 'aria-checked': step.done ? 'true' : 'false', + 'aria-label': step.done + ? localize('stepDone', "Checkbox for Step {0}: Completed", step.title) + : localize('stepNotDone', "Checkbox for Step {0}: Not completed", step.title), }); const container = $('.step-description-container', { 'x-step-description-for': step.id }); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts index 94907761..1ca5db0b 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -201,7 +201,7 @@ export class GettingStartedDetailsRenderer { `; } - async renderVideo(path: URI, poster?: URI): Promise { + async renderVideo(path: URI, poster?: URI, description?: string): Promise { const nonce = generateUuid(); return ` @@ -218,7 +218,7 @@ export class GettingStartedDetailsRenderer { -