From d5b8eae897599c6c16ee93d69a013b13d22ebd4a Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Sun, 26 Nov 2023 15:46:13 -0800 Subject: [PATCH] initial commit --- .config/eslintrc.js | 128 + .config/prettier.config.js | 15 + .config/stylelintrc.json | 32 + .config/tsconfig.base.json | 19 + .gitattributes | 4 + .github/workflows/ci.yml | 95 + .github/workflows/scip-typescript.yml | 48 + .github/workflows/vscode-pre-release.yml | 39 + .github/workflows/vscode-stable-release.yml | 65 + .gitignore | 13 + .npmrc | 17 + .tool-versions | 2 + .vscode/extensions.json | 13 + .vscode/launch.json | 70 + .vscode/settings.json | 37 + .vscode/tasks.json | 38 + LICENSE | 201 + README.md | 38 + client/browser/.gitignore | 1 + client/browser/CHANGELOG.md | 9 + client/browser/CONTRIBUTING.md | 10 + client/browser/README.md | 63 + client/browser/dev/create-icons.sh | 18 + client/browser/manifest.config.ts | 43 + client/browser/package.json | 42 + client/browser/public/icon-128.png | Bin 0 -> 7990 bytes client/browser/public/icon-32.png | Bin 0 -> 1422 bytes client/browser/public/icon-48.png | Bin 0 -> 2287 bytes .../browser/src/background/background.main.ts | 54 + .../ExtensionStorageSubject.ts | 32 + .../web-extension-api/README.md | 3 + .../web-extension-api/fromBrowserEvent.ts | 20 + .../web-extension-api/rpc.ts | 155 + .../web-extension-api/runtime.ts | 9 + .../web-extension-api/storage.ts | 55 + .../web-extension-api/types.ts | 35 + client/browser/src/configuration.ts | 68 + .../src/contentScript/contentScript.main.css | 41 + .../src/contentScript/contentScript.main.ts | 38 + client/browser/src/contentScript/debug.ts | 11 + .../src/contentScript/detectElements.ts | 37 + .../src/contentScript/github/codeView.ts | 227 + .../github/pullRequestFilesView.ts | 212 + .../src/contentScript/locationChanges.ts | 35 + client/browser/src/contentScript/ocgUtil.ts | 28 + client/browser/src/globals.d.ts | 4 + .../src/options/OptionsPage.module.css | 67 + client/browser/src/options/OptionsPage.tsx | 101 + client/browser/src/options/options.html | 17 + client/browser/src/options/options.main.tsx | 15 + client/browser/src/shared/env.ts | 31 + client/browser/src/shared/platform.ts | 6 + client/browser/src/shared/polyfills.ts | 5 + .../shared/util/toLineRangeStrings.test.ts | 14 + .../src/shared/util/toLineRangeStrings.ts | 41 + .../types/webextension-polyfill/index.d.ts | 2028 ++ client/browser/tsconfig.json | 14 + client/browser/vite.config.ts | 30 + client/codemirror/README.md | 99 + client/codemirror/demo/README.md | 7 + client/codemirror/demo/demo.ts | 78 + client/codemirror/demo/globals.d.ts | 4 + client/codemirror/demo/index.html | 12 + client/codemirror/demo/package.json | 18 + client/codemirror/demo/tsconfig.json | 16 + client/codemirror/demo/vite.config.ts | 31 + client/codemirror/package.json | 42 + client/codemirror/src/extension.ts | 92 + client/codemirror/src/index.ts | 2 + client/codemirror/src/itemBlockWidget.ts | 82 + .../src/useOpenCodeGraphExtension.tsx | 45 + client/codemirror/tsconfig.json | 20 + client/monaco-editor/README.md | 51 + client/monaco-editor/demo/README.md | 7 + client/monaco-editor/demo/demo.ts | 32 + client/monaco-editor/demo/globals.d.ts | 4 + client/monaco-editor/demo/index.html | 12 + client/monaco-editor/demo/package.json | 18 + client/monaco-editor/demo/tsconfig.json | 16 + client/monaco-editor/demo/vite.config.ts | 35 + client/monaco-editor/package.json | 33 + client/monaco-editor/src/index.ts | 108 + client/monaco-editor/tsconfig.json | 20 + client/vscode/.vscodeignore | 13 + client/vscode/CHANGELOG.md | 15 + client/vscode/CONTRIBUTING.md | 69 + client/vscode/LICENSE | 201 + client/vscode/README.md | 44 + client/vscode/dev/build.mts | 45 + client/vscode/dev/release.mts | 163 + client/vscode/dev/tsconfig.json | 11 + client/vscode/package.json | 210 + client/vscode/resources/logomark-v0.png | Bin 0 -> 53560 bytes client/vscode/resources/ocg-icons.woff | Bin 0 -> 1484 bytes client/vscode/src/api.ts | 32 + client/vscode/src/authInfo.ts | 76 + client/vscode/src/configuration.test.ts | 49 + client/vscode/src/configuration.ts | 105 + client/vscode/src/controller.ts | 161 + client/vscode/src/dynamicImport.test.ts | 20 + client/vscode/src/dynamicImport.ts | 75 + client/vscode/src/extension.ts | 20 + client/vscode/src/ui/editor/codeLens.ts | 88 + client/vscode/src/ui/editor/hover.ts | 59 + client/vscode/src/ui/fileAnnotationsList.ts | 116 + client/vscode/src/ui/statusBarItem.ts | 33 + client/vscode/src/util.ts | 30 + client/vscode/src/util/errorWaiter.ts | 54 + .../fixtures/workspace/.vscode/settings.json | 44 + .../test/fixtures/workspace/Label.story.tsx | 18 + .../vscode/test/fixtures/workspace/Label.tsx | 7 + .../test/fixtures/workspace/eventLogger.ts | 3 + client/vscode/test/fixtures/workspace/foo.ts | 3 + .../test/fixtures/workspace/globals.d.ts | 3 + .../test/fixtures/workspace/package.json | 4 + .../test/fixtures/workspace/tsconfig.json | 7 + client/vscode/test/integration/api.test.ts | 15 + client/vscode/test/integration/index.ts | 35 + client/vscode/test/integration/main.ts | 35 + client/vscode/test/integration/tsconfig.json | 11 + client/vscode/tsconfig.json | 16 + client/vscode/vite.config.ts | 9 + client/web-playground/README.md | 10 + client/web-playground/index.html | 12 + client/web-playground/package.json | 40 + client/web-playground/src/AnnotatedEditor.tsx | 130 + .../src/EditorHeader.module.css | 25 + client/web-playground/src/EditorHeader.tsx | 20 + client/web-playground/src/SettingsEditor.tsx | 53 + client/web-playground/src/codemirror.ts | 12 + client/web-playground/src/demo.tsx | 10 + .../src/demo/DemoApp.module.css | 69 + client/web-playground/src/demo/DemoApp.tsx | 88 + client/web-playground/src/demo/main.css | 13 + client/web-playground/src/demo/main.tsx | 10 + client/web-playground/src/demo/settings.ts | 58 + client/web-playground/src/globals.d.ts | 4 + client/web-playground/src/index.ts | 2 + client/web-playground/tsconfig.json | 18 + client/web-playground/vite.config.ts | 48 + doc/dev/index.md | 15 + doc/index.md | 4 + doc/resources/logomark-v0.png | Bin 0 -> 6252 bytes doc/resources/logomark-v0.svg | 1 + .../logotext-horiz-color-dark-v0.svg | 1 + .../logotext-horiz-color-light-v0.svg | 1 + lib/client/README.md | 3 + lib/client/package.json | 38 + lib/client/src/api.test.ts | 130 + lib/client/src/api.ts | 109 + lib/client/src/client/client.test.ts | 96 + lib/client/src/client/client.ts | 287 + lib/client/src/client/testdata/simple.js | 13 + lib/client/src/configuration.test.ts | 31 + lib/client/src/configuration.ts | 43 + lib/client/src/index.ts | 7 + lib/client/src/logger.ts | 11 + .../createProviderClient.test.ts | 114 + .../providerClient/createProviderClient.ts | 80 + .../src/providerClient/selector.test.ts | 62 + lib/client/src/providerClient/selector.ts | 39 + .../testdata/annotationsThrow.js | 9 + .../testdata/capabilitiesThrow.js | 9 + .../src/providerClient/testdata/provider.js | 13 + .../providerClient/testdata/topLevelThrow.js | 1 + .../providerClient/testdata/transportReuse.js | 13 + .../src/providerClient/transport/cache.ts | 36 + .../transport/createTransport.test.ts | 153 + .../transport/createTransport.ts | 98 + .../src/providerClient/transport/http.ts | 88 + .../src/providerClient/transport/module.ts | 105 + .../testdata/commonjsExtProvider.cjs | 4 + .../transport/testdata/commonjsProvider.js | 4 + .../transport/testdata/emoji.js | 5 + .../transport/testdata/esmExtProvider.mjs | 4 + .../transport/testdata/esmProvider.js | 4 + .../transport/testdata/esmProvider.ts | 8 + lib/client/tsconfig.json | 11 + lib/client/vitest.config.ts | 5 + lib/protocol/README.md | 3 + lib/protocol/package.json | 32 + lib/protocol/src/index.ts | 1 + .../src/opencodegraph-protocol.schema.json | 122 + .../src/opencodegraph-protocol.schema.ts | 81 + lib/protocol/tsconfig.json | 9 + lib/protocol/vitest.config.ts | 5 + lib/provider/README.md | 3 + lib/provider/package.json | 33 + lib/provider/src/index.ts | 12 + lib/provider/src/provider.ts | 34 + lib/provider/tsconfig.json | 11 + lib/provider/vitest.config.ts | 3 + lib/schema/README.md | 3 + lib/schema/dev/generateJsonSchemaTypes.ts | 56 + .../dev/json-schema-draft-07.schema.json | 151 + lib/schema/dev/tsconfig.json | 9 + lib/schema/package.json | 29 + lib/schema/src/index.ts | 1 + lib/schema/src/opencodegraph.schema.json | 82 + lib/schema/src/opencodegraph.schema.ts | 46 + lib/schema/tsconfig.json | 8 + lib/schema/vitest.config.ts | 5 + lib/ui-react/.storybook/main.ts | 18 + lib/ui-react/README.md | 3 + lib/ui-react/package.json | 45 + lib/ui-react/src/globals.d.ts | 4 + .../IndentationWrapper.story.tsx | 43 + .../indentationWrapper/IndentationWrapper.tsx | 18 + lib/ui-react/src/index.ts | 2 + lib/ui-react/src/itemChip/ItemChip.module.css | 97 + lib/ui-react/src/itemChip/ItemChip.story.tsx | 61 + lib/ui-react/src/itemChip/ItemChip.tsx | 118 + lib/ui-react/tsconfig.json | 17 + lib/ui-standalone/.storybook/main.ts | 10 + lib/ui-standalone/README.md | 5 + lib/ui-standalone/package.json | 34 + lib/ui-standalone/src/globals.d.ts | 4 + .../IndentationWrapper.story.ts | 34 + .../indentationWrapper/IndentationWrapper.ts | 32 + lib/ui-standalone/src/index.ts | 2 + .../src/itemChip/ItemChip.module.css | 108 + .../src/itemChip/ItemChip.story.ts | 52 + lib/ui-standalone/src/itemChip/ItemChip.ts | 164 + lib/ui-standalone/src/itemChip/popover.ts | 50 + lib/ui-standalone/tsconfig.json | 16 + lib/ui-standalone/vite.config.ts | 10 + package.json | 80 + pnpm-lock.yaml | 16870 ++++++++++++++++ pnpm-workspace.yaml | 7 + provider/hello-world/README.md | 24 + provider/hello-world/index.test.ts | 43 + provider/hello-world/index.ts | 48 + provider/hello-world/package.json | 28 + provider/hello-world/tsconfig.json | 12 + provider/hello-world/vitest.config.ts | 3 + provider/links/README.md | 116 + provider/links/index.test.ts | 98 + provider/links/index.ts | 196 + provider/links/package.json | 28 + provider/links/tsconfig.json | 12 + provider/links/vitest.config.ts | 3 + provider/prometheus/README.md | 89 + provider/prometheus/index.test.ts | 57 + provider/prometheus/index.ts | 155 + provider/prometheus/package.json | 28 + provider/prometheus/tsconfig.json | 12 + provider/prometheus/vitest.config.ts | 3 + provider/storybook/README.md | 58 + provider/storybook/index.test.ts | 98 + provider/storybook/index.ts | 255 + provider/storybook/package.json | 31 + provider/storybook/tsconfig.json | 12 + provider/storybook/vitest.config.ts | 3 + tsconfig.json | 33 + vitest.workspace.ts | 13 + web/README.md | 5 + web/components.json | 13 + .../docs/clients/browser-extension.mdx | 9 + web/content/docs/clients/codemirror.mdx | 9 + web/content/docs/clients/cody.mdx | 15 + web/content/docs/clients/github.mdx | 37 + web/content/docs/clients/monaco-editor.mdx | 9 + web/content/docs/clients/sourcegraph.mdx | 10 + web/content/docs/clients/vscode.mdx | 13 + web/content/docs/community.mdx | 18 + web/content/docs/concepts.mdx | 65 + web/content/docs/creating-a-provider.mdx | 21 + web/content/docs/faq.mdx | 34 + web/content/docs/protocol.mdx | 137 + web/content/docs/providers/hello-world.mdx | 9 + web/content/docs/providers/links.mdx | 8 + web/content/docs/providers/prometheus.mdx | 8 + web/content/docs/providers/storybook.mdx | 8 + web/content/docs/start.mdx | 74 + web/content/docs/vision.mdx | 24 + web/netlify.toml | 24 + web/package.json | 67 + web/pages/_error/+Page.tsx | 14 + web/pages/docs/+Page.tsx | 10 + web/pages/docs/+config.h.ts | 8 + web/pages/docs/+onBeforePrerenderStart.ts | 5 + web/pages/docs/+onBeforeRender.ts | 14 + web/pages/docs/components/DocsLayout.tsx | 53 + web/pages/docs/components/NavMenu.tsx | 82 + .../SourcegraphGettingStartedCommon.mdx | 22 + web/pages/docs/content.ts | 13 + web/pages/index/+Page.tsx | 104 + web/pages/npm/+config.h.ts | 7 + web/pages/npm/+onBeforeRender.ts | 27 + web/pages/playground/+Page.tsx | 13 + web/pages/playground/+config.h.ts | 8 + .../playground/FakeEditorWindow.module.css | 40 + web/pages/playground/FakeEditorWindow.tsx | 87 + web/pages/playground/Playground.tsx | 63 + web/pages/playground/Preload.tsx | 19 + web/pages/playground/data.ts | 112 + web/pages/playground/text.mdx | 5 + web/postcss.config.js | 10 + web/public/FiraCode.woff2 | Bin 0 -> 106228 bytes web/public/Inter-italic.var.woff2 | Bin 0 -> 244760 bytes web/public/Inter-roman.var.woff2 | Bin 0 -> 227688 bytes web/public/favicon.ico | Bin 0 -> 4158 bytes web/public/logomark-v0.svg | 22 + web/public/logotext-horiz-color-dark-v0.svg | 23 + web/public/logotext-horiz-color-light-v0.svg | 23 + web/renderer/+config.h.ts | 30 + web/renderer/+title.ts | 45 + web/renderer/types.ts | 17 + web/src/components/ClientOnlyLazy.tsx | 15 + web/src/components/ClientOnlySync.tsx | 15 + web/src/components/Link.tsx | 13 + web/src/components/codemirror/colorTheme.ts | 68 + web/src/components/codemirror/defaults.ts | 15 + .../content/MdxComponents.module.css | 89 + web/src/components/content/MdxComponents.tsx | 13 + .../components/content/github-from-css.css | 63 + web/src/components/ui/button.tsx | 49 + web/src/components/ui/dropdown-menu.tsx | 180 + web/src/components/ui/navigation-menu.tsx | 119 + web/src/components/ui/sheet.tsx | 63 + web/src/content/ContentPage.tsx | 22 + web/src/content/contentPages.tsx | 185 + web/src/globals.css | 122 + web/src/globals.d.ts | 4 + web/src/layout/Head.tsx | 17 + web/src/layout/Layout.tsx | 93 + web/src/layout/config.ts | 2 + web/src/layout/logo.tsx | 11 + web/src/lib/utils.ts | 6 + web/src/server/index.ts | 50 + web/src/server/root.ts | 9 + web/src/server/tsconfig.json | 12 + web/tailwind.config.ts | 81 + web/tsconfig.json | 21 + web/tsconfig.node.json | 10 + web/vite.config.ts | 45 + 336 files changed, 32539 insertions(+) create mode 100644 .config/eslintrc.js create mode 100644 .config/prettier.config.js create mode 100644 .config/stylelintrc.json create mode 100644 .config/tsconfig.base.json create mode 100644 .gitattributes create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/scip-typescript.yml create mode 100644 .github/workflows/vscode-pre-release.yml create mode 100644 .github/workflows/vscode-stable-release.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .tool-versions create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 client/browser/.gitignore create mode 100644 client/browser/CHANGELOG.md create mode 100644 client/browser/CONTRIBUTING.md create mode 100644 client/browser/README.md create mode 100644 client/browser/dev/create-icons.sh create mode 100644 client/browser/manifest.config.ts create mode 100644 client/browser/package.json create mode 100644 client/browser/public/icon-128.png create mode 100644 client/browser/public/icon-32.png create mode 100644 client/browser/public/icon-48.png create mode 100644 client/browser/src/background/background.main.ts create mode 100644 client/browser/src/browser-extension/web-extension-api/ExtensionStorageSubject.ts create mode 100644 client/browser/src/browser-extension/web-extension-api/README.md create mode 100644 client/browser/src/browser-extension/web-extension-api/fromBrowserEvent.ts create mode 100644 client/browser/src/browser-extension/web-extension-api/rpc.ts create mode 100644 client/browser/src/browser-extension/web-extension-api/runtime.ts create mode 100644 client/browser/src/browser-extension/web-extension-api/storage.ts create mode 100644 client/browser/src/browser-extension/web-extension-api/types.ts create mode 100644 client/browser/src/configuration.ts create mode 100644 client/browser/src/contentScript/contentScript.main.css create mode 100644 client/browser/src/contentScript/contentScript.main.ts create mode 100644 client/browser/src/contentScript/debug.ts create mode 100644 client/browser/src/contentScript/detectElements.ts create mode 100644 client/browser/src/contentScript/github/codeView.ts create mode 100644 client/browser/src/contentScript/github/pullRequestFilesView.ts create mode 100644 client/browser/src/contentScript/locationChanges.ts create mode 100644 client/browser/src/contentScript/ocgUtil.ts create mode 100644 client/browser/src/globals.d.ts create mode 100644 client/browser/src/options/OptionsPage.module.css create mode 100644 client/browser/src/options/OptionsPage.tsx create mode 100644 client/browser/src/options/options.html create mode 100644 client/browser/src/options/options.main.tsx create mode 100644 client/browser/src/shared/env.ts create mode 100644 client/browser/src/shared/platform.ts create mode 100644 client/browser/src/shared/polyfills.ts create mode 100644 client/browser/src/shared/util/toLineRangeStrings.test.ts create mode 100644 client/browser/src/shared/util/toLineRangeStrings.ts create mode 100644 client/browser/src/types/webextension-polyfill/index.d.ts create mode 100644 client/browser/tsconfig.json create mode 100644 client/browser/vite.config.ts create mode 100644 client/codemirror/README.md create mode 100644 client/codemirror/demo/README.md create mode 100644 client/codemirror/demo/demo.ts create mode 100644 client/codemirror/demo/globals.d.ts create mode 100644 client/codemirror/demo/index.html create mode 100644 client/codemirror/demo/package.json create mode 100644 client/codemirror/demo/tsconfig.json create mode 100644 client/codemirror/demo/vite.config.ts create mode 100644 client/codemirror/package.json create mode 100644 client/codemirror/src/extension.ts create mode 100644 client/codemirror/src/index.ts create mode 100644 client/codemirror/src/itemBlockWidget.ts create mode 100644 client/codemirror/src/useOpenCodeGraphExtension.tsx create mode 100644 client/codemirror/tsconfig.json create mode 100644 client/monaco-editor/README.md create mode 100644 client/monaco-editor/demo/README.md create mode 100644 client/monaco-editor/demo/demo.ts create mode 100644 client/monaco-editor/demo/globals.d.ts create mode 100644 client/monaco-editor/demo/index.html create mode 100644 client/monaco-editor/demo/package.json create mode 100644 client/monaco-editor/demo/tsconfig.json create mode 100644 client/monaco-editor/demo/vite.config.ts create mode 100644 client/monaco-editor/package.json create mode 100644 client/monaco-editor/src/index.ts create mode 100644 client/monaco-editor/tsconfig.json create mode 100644 client/vscode/.vscodeignore create mode 100644 client/vscode/CHANGELOG.md create mode 100644 client/vscode/CONTRIBUTING.md create mode 100644 client/vscode/LICENSE create mode 100644 client/vscode/README.md create mode 100644 client/vscode/dev/build.mts create mode 100644 client/vscode/dev/release.mts create mode 100644 client/vscode/dev/tsconfig.json create mode 100644 client/vscode/package.json create mode 100644 client/vscode/resources/logomark-v0.png create mode 100644 client/vscode/resources/ocg-icons.woff create mode 100644 client/vscode/src/api.ts create mode 100644 client/vscode/src/authInfo.ts create mode 100644 client/vscode/src/configuration.test.ts create mode 100644 client/vscode/src/configuration.ts create mode 100644 client/vscode/src/controller.ts create mode 100644 client/vscode/src/dynamicImport.test.ts create mode 100644 client/vscode/src/dynamicImport.ts create mode 100644 client/vscode/src/extension.ts create mode 100644 client/vscode/src/ui/editor/codeLens.ts create mode 100644 client/vscode/src/ui/editor/hover.ts create mode 100644 client/vscode/src/ui/fileAnnotationsList.ts create mode 100644 client/vscode/src/ui/statusBarItem.ts create mode 100644 client/vscode/src/util.ts create mode 100644 client/vscode/src/util/errorWaiter.ts create mode 100644 client/vscode/test/fixtures/workspace/.vscode/settings.json create mode 100644 client/vscode/test/fixtures/workspace/Label.story.tsx create mode 100644 client/vscode/test/fixtures/workspace/Label.tsx create mode 100644 client/vscode/test/fixtures/workspace/eventLogger.ts create mode 100644 client/vscode/test/fixtures/workspace/foo.ts create mode 100644 client/vscode/test/fixtures/workspace/globals.d.ts create mode 100644 client/vscode/test/fixtures/workspace/package.json create mode 100644 client/vscode/test/fixtures/workspace/tsconfig.json create mode 100644 client/vscode/test/integration/api.test.ts create mode 100644 client/vscode/test/integration/index.ts create mode 100644 client/vscode/test/integration/main.ts create mode 100644 client/vscode/test/integration/tsconfig.json create mode 100644 client/vscode/tsconfig.json create mode 100644 client/vscode/vite.config.ts create mode 100644 client/web-playground/README.md create mode 100644 client/web-playground/index.html create mode 100644 client/web-playground/package.json create mode 100644 client/web-playground/src/AnnotatedEditor.tsx create mode 100644 client/web-playground/src/EditorHeader.module.css create mode 100644 client/web-playground/src/EditorHeader.tsx create mode 100644 client/web-playground/src/SettingsEditor.tsx create mode 100644 client/web-playground/src/codemirror.ts create mode 100644 client/web-playground/src/demo.tsx create mode 100644 client/web-playground/src/demo/DemoApp.module.css create mode 100644 client/web-playground/src/demo/DemoApp.tsx create mode 100644 client/web-playground/src/demo/main.css create mode 100644 client/web-playground/src/demo/main.tsx create mode 100644 client/web-playground/src/demo/settings.ts create mode 100644 client/web-playground/src/globals.d.ts create mode 100644 client/web-playground/src/index.ts create mode 100644 client/web-playground/tsconfig.json create mode 100644 client/web-playground/vite.config.ts create mode 100644 doc/dev/index.md create mode 100644 doc/index.md create mode 100644 doc/resources/logomark-v0.png create mode 120000 doc/resources/logomark-v0.svg create mode 120000 doc/resources/logotext-horiz-color-dark-v0.svg create mode 120000 doc/resources/logotext-horiz-color-light-v0.svg create mode 100644 lib/client/README.md create mode 100644 lib/client/package.json create mode 100644 lib/client/src/api.test.ts create mode 100644 lib/client/src/api.ts create mode 100644 lib/client/src/client/client.test.ts create mode 100644 lib/client/src/client/client.ts create mode 100644 lib/client/src/client/testdata/simple.js create mode 100644 lib/client/src/configuration.test.ts create mode 100644 lib/client/src/configuration.ts create mode 100644 lib/client/src/index.ts create mode 100644 lib/client/src/logger.ts create mode 100644 lib/client/src/providerClient/createProviderClient.test.ts create mode 100644 lib/client/src/providerClient/createProviderClient.ts create mode 100644 lib/client/src/providerClient/selector.test.ts create mode 100644 lib/client/src/providerClient/selector.ts create mode 100644 lib/client/src/providerClient/testdata/annotationsThrow.js create mode 100644 lib/client/src/providerClient/testdata/capabilitiesThrow.js create mode 100644 lib/client/src/providerClient/testdata/provider.js create mode 100644 lib/client/src/providerClient/testdata/topLevelThrow.js create mode 100644 lib/client/src/providerClient/testdata/transportReuse.js create mode 100644 lib/client/src/providerClient/transport/cache.ts create mode 100644 lib/client/src/providerClient/transport/createTransport.test.ts create mode 100644 lib/client/src/providerClient/transport/createTransport.ts create mode 100644 lib/client/src/providerClient/transport/http.ts create mode 100644 lib/client/src/providerClient/transport/module.ts create mode 100644 lib/client/src/providerClient/transport/testdata/commonjsExtProvider.cjs create mode 100644 lib/client/src/providerClient/transport/testdata/commonjsProvider.js create mode 100644 lib/client/src/providerClient/transport/testdata/emoji.js create mode 100644 lib/client/src/providerClient/transport/testdata/esmExtProvider.mjs create mode 100644 lib/client/src/providerClient/transport/testdata/esmProvider.js create mode 100644 lib/client/src/providerClient/transport/testdata/esmProvider.ts create mode 100644 lib/client/tsconfig.json create mode 100644 lib/client/vitest.config.ts create mode 100644 lib/protocol/README.md create mode 100644 lib/protocol/package.json create mode 100644 lib/protocol/src/index.ts create mode 100644 lib/protocol/src/opencodegraph-protocol.schema.json create mode 100644 lib/protocol/src/opencodegraph-protocol.schema.ts create mode 100644 lib/protocol/tsconfig.json create mode 100644 lib/protocol/vitest.config.ts create mode 100644 lib/provider/README.md create mode 100644 lib/provider/package.json create mode 100644 lib/provider/src/index.ts create mode 100644 lib/provider/src/provider.ts create mode 100644 lib/provider/tsconfig.json create mode 100644 lib/provider/vitest.config.ts create mode 100644 lib/schema/README.md create mode 100644 lib/schema/dev/generateJsonSchemaTypes.ts create mode 100644 lib/schema/dev/json-schema-draft-07.schema.json create mode 100644 lib/schema/dev/tsconfig.json create mode 100644 lib/schema/package.json create mode 100644 lib/schema/src/index.ts create mode 100644 lib/schema/src/opencodegraph.schema.json create mode 100644 lib/schema/src/opencodegraph.schema.ts create mode 100644 lib/schema/tsconfig.json create mode 100644 lib/schema/vitest.config.ts create mode 100644 lib/ui-react/.storybook/main.ts create mode 100644 lib/ui-react/README.md create mode 100644 lib/ui-react/package.json create mode 100644 lib/ui-react/src/globals.d.ts create mode 100644 lib/ui-react/src/indentationWrapper/IndentationWrapper.story.tsx create mode 100644 lib/ui-react/src/indentationWrapper/IndentationWrapper.tsx create mode 100644 lib/ui-react/src/index.ts create mode 100644 lib/ui-react/src/itemChip/ItemChip.module.css create mode 100644 lib/ui-react/src/itemChip/ItemChip.story.tsx create mode 100644 lib/ui-react/src/itemChip/ItemChip.tsx create mode 100644 lib/ui-react/tsconfig.json create mode 100644 lib/ui-standalone/.storybook/main.ts create mode 100644 lib/ui-standalone/README.md create mode 100644 lib/ui-standalone/package.json create mode 100644 lib/ui-standalone/src/globals.d.ts create mode 100644 lib/ui-standalone/src/indentationWrapper/IndentationWrapper.story.ts create mode 100644 lib/ui-standalone/src/indentationWrapper/IndentationWrapper.ts create mode 100644 lib/ui-standalone/src/index.ts create mode 100644 lib/ui-standalone/src/itemChip/ItemChip.module.css create mode 100644 lib/ui-standalone/src/itemChip/ItemChip.story.ts create mode 100644 lib/ui-standalone/src/itemChip/ItemChip.ts create mode 100644 lib/ui-standalone/src/itemChip/popover.ts create mode 100644 lib/ui-standalone/tsconfig.json create mode 100644 lib/ui-standalone/vite.config.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 provider/hello-world/README.md create mode 100644 provider/hello-world/index.test.ts create mode 100644 provider/hello-world/index.ts create mode 100644 provider/hello-world/package.json create mode 100644 provider/hello-world/tsconfig.json create mode 100644 provider/hello-world/vitest.config.ts create mode 100644 provider/links/README.md create mode 100644 provider/links/index.test.ts create mode 100644 provider/links/index.ts create mode 100644 provider/links/package.json create mode 100644 provider/links/tsconfig.json create mode 100644 provider/links/vitest.config.ts create mode 100644 provider/prometheus/README.md create mode 100644 provider/prometheus/index.test.ts create mode 100644 provider/prometheus/index.ts create mode 100644 provider/prometheus/package.json create mode 100644 provider/prometheus/tsconfig.json create mode 100644 provider/prometheus/vitest.config.ts create mode 100644 provider/storybook/README.md create mode 100644 provider/storybook/index.test.ts create mode 100644 provider/storybook/index.ts create mode 100644 provider/storybook/package.json create mode 100644 provider/storybook/tsconfig.json create mode 100644 provider/storybook/vitest.config.ts create mode 100644 tsconfig.json create mode 100644 vitest.workspace.ts create mode 100644 web/README.md create mode 100644 web/components.json create mode 100644 web/content/docs/clients/browser-extension.mdx create mode 100644 web/content/docs/clients/codemirror.mdx create mode 100644 web/content/docs/clients/cody.mdx create mode 100644 web/content/docs/clients/github.mdx create mode 100644 web/content/docs/clients/monaco-editor.mdx create mode 100644 web/content/docs/clients/sourcegraph.mdx create mode 100644 web/content/docs/clients/vscode.mdx create mode 100644 web/content/docs/community.mdx create mode 100644 web/content/docs/concepts.mdx create mode 100644 web/content/docs/creating-a-provider.mdx create mode 100644 web/content/docs/faq.mdx create mode 100644 web/content/docs/protocol.mdx create mode 100644 web/content/docs/providers/hello-world.mdx create mode 100644 web/content/docs/providers/links.mdx create mode 100644 web/content/docs/providers/prometheus.mdx create mode 100644 web/content/docs/providers/storybook.mdx create mode 100644 web/content/docs/start.mdx create mode 100644 web/content/docs/vision.mdx create mode 100644 web/netlify.toml create mode 100644 web/package.json create mode 100644 web/pages/_error/+Page.tsx create mode 100644 web/pages/docs/+Page.tsx create mode 100644 web/pages/docs/+config.h.ts create mode 100644 web/pages/docs/+onBeforePrerenderStart.ts create mode 100644 web/pages/docs/+onBeforeRender.ts create mode 100644 web/pages/docs/components/DocsLayout.tsx create mode 100644 web/pages/docs/components/NavMenu.tsx create mode 100644 web/pages/docs/components/SourcegraphGettingStartedCommon.mdx create mode 100644 web/pages/docs/content.ts create mode 100644 web/pages/index/+Page.tsx create mode 100644 web/pages/npm/+config.h.ts create mode 100644 web/pages/npm/+onBeforeRender.ts create mode 100644 web/pages/playground/+Page.tsx create mode 100644 web/pages/playground/+config.h.ts create mode 100644 web/pages/playground/FakeEditorWindow.module.css create mode 100644 web/pages/playground/FakeEditorWindow.tsx create mode 100644 web/pages/playground/Playground.tsx create mode 100644 web/pages/playground/Preload.tsx create mode 100644 web/pages/playground/data.ts create mode 100644 web/pages/playground/text.mdx create mode 100644 web/postcss.config.js create mode 100644 web/public/FiraCode.woff2 create mode 100644 web/public/Inter-italic.var.woff2 create mode 100644 web/public/Inter-roman.var.woff2 create mode 100644 web/public/favicon.ico create mode 100644 web/public/logomark-v0.svg create mode 100644 web/public/logotext-horiz-color-dark-v0.svg create mode 100644 web/public/logotext-horiz-color-light-v0.svg create mode 100644 web/renderer/+config.h.ts create mode 100644 web/renderer/+title.ts create mode 100644 web/renderer/types.ts create mode 100644 web/src/components/ClientOnlyLazy.tsx create mode 100644 web/src/components/ClientOnlySync.tsx create mode 100644 web/src/components/Link.tsx create mode 100644 web/src/components/codemirror/colorTheme.ts create mode 100644 web/src/components/codemirror/defaults.ts create mode 100644 web/src/components/content/MdxComponents.module.css create mode 100644 web/src/components/content/MdxComponents.tsx create mode 100644 web/src/components/content/github-from-css.css create mode 100644 web/src/components/ui/button.tsx create mode 100644 web/src/components/ui/dropdown-menu.tsx create mode 100644 web/src/components/ui/navigation-menu.tsx create mode 100644 web/src/components/ui/sheet.tsx create mode 100644 web/src/content/ContentPage.tsx create mode 100644 web/src/content/contentPages.tsx create mode 100644 web/src/globals.css create mode 100644 web/src/globals.d.ts create mode 100644 web/src/layout/Head.tsx create mode 100644 web/src/layout/Layout.tsx create mode 100644 web/src/layout/config.ts create mode 100644 web/src/layout/logo.tsx create mode 100644 web/src/lib/utils.ts create mode 100644 web/src/server/index.ts create mode 100644 web/src/server/root.ts create mode 100644 web/src/server/tsconfig.json create mode 100644 web/tailwind.config.ts create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/vite.config.ts diff --git a/.config/eslintrc.js b/.config/eslintrc.js new file mode 100644 index 00000000..5f0f08f7 --- /dev/null +++ b/.config/eslintrc.js @@ -0,0 +1,128 @@ +// @ts-check + +/** @type {import('eslint/lib/linter/linter').ConfigData} */ +const config = { + env: { + browser: true, + node: true, + es6: true, + }, + root: true, + parserOptions: { + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + // @ts-ignore + EXPERIMENTAL_useProjectService: true, + project: true, + }, + settings: { + react: { + version: '18', + }, + }, + overrides: [ + { + files: '*.{js,ts,tsx}', + extends: ['@sourcegraph/eslint-config', 'plugin:storybook/recommended', 'plugin:react/jsx-runtime'], + rules: { + 'import/order': 'off', + 'import/export': 'off', + 'etc/no-deprecated': 'off', // slow + 'no-restricted-imports': 'off', + 'unicorn/switch-case-braces': 'off', + 'unicorn/prefer-event-target': 'off', + 'unicorn/prefer-dom-node-remove': 'off', + 'ban/ban': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'jsx-a11y/anchor-has-content': 'off', + 'jsx-a11y/click-events-have-key-events': 'off', + 'jsx-a11y/no-static-element-interactions': 'off', + 'arrow-body-style': ['error', 'as-needed'], + '@typescript-eslint/consistent-type-exports': [ + 'error', + { + fixMixedExportsWithInlineTypeSpecifier: true, + }, + ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + fixStyle: 'inline-type-imports', + disallowTypeAnnotations: false, + }, + ], + 'jsdoc/tag-lines': ['error', 'always', { count: 0, startLines: 1, endLines: 0 }], + }, + }, + { + files: '*.story.ts?(x)', + rules: { + 'react/forbid-dom-props': 'off', + 'import/no-default-export': 'off', + 'no-console': 'off', + }, + }, + { + files: ['vitest.workspace.ts', 'vite.config.ts', 'vitest.config.ts'], + rules: { + 'import/no-default-export': 'off', + }, + }, + { + files: ['provider/**/index.ts'], + rules: { + 'import/no-default-export': 'off', + }, + }, + { + files: ['provider/hello-world/*.ts', 'provider/storybook/*.ts'], + rules: { + '@typescript-eslint/no-unused-vars': 'off', // makes for cleaner sample code + }, + }, + { + files: ['web/**/*.ts?(x)'], + plugins: ['react-refresh'], + rules: { + 'import/no-default-export': 'off', + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'import/extensions': ['error', 'ignorePackages'], + }, + }, + { + files: ['**/*.mdx'], + extends: ['plugin:mdx/recommended'], + }, + { + files: ['web/**/*.{mdx,ts,tsx}'], + extends: ['plugin:tailwindcss/recommended'], + rules: { + 'tailwindcss/classnames-order': 'warn', + }, + settings: { + tailwindcss: { + config: 'web/tailwind.config.ts', + }, + }, + }, + ], + ignorePatterns: [ + 'out/', + 'dist/', + '*.schema.ts', + '.eslintrc.js', + 'postcss.config.js', + 'vitest.config.ts', + 'vitest.workspace.ts', + 'vite.config.ts', + 'client/vscode/src/entrypoint/*', + 'client/vscode/test/fixtures/', + '/coverage/', + 'testdata/', + 'web/src/components/ui', // shadcn components + '*.mts', + ], +} +module.exports = config diff --git a/.config/prettier.config.js b/.config/prettier.config.js new file mode 100644 index 00000000..e8bb5a12 --- /dev/null +++ b/.config/prettier.config.js @@ -0,0 +1,15 @@ +// @ts-check + +const baseConfig = require('@sourcegraph/prettierrc') + +/** @type {import('prettier').Config} */ +module.exports = { + ...baseConfig, + plugins: [...(baseConfig.plugins || []), '@ianvs/prettier-plugin-sort-imports'], + overrides: [ + ...baseConfig.overrides, + // In *.mdx files, printWidth wrapping breaks up elements so that there are nested HTML + // tags, which means that client-side hydration fails. + { files: '**/*.mdx', options: { proseWrap: 'preserve', printWidth: Number.MAX_SAFE_INTEGER, tabWidth: 2 } }, + ], +} diff --git a/.config/stylelintrc.json b/.config/stylelintrc.json new file mode 100644 index 00000000..471ce5de --- /dev/null +++ b/.config/stylelintrc.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json.schemastore.org/stylelintrc.json", + "extends": ["stylelint-config-standard", "stylelint-config-prettier"], + "rules": { + "color-named": "never", + "color-hex-length": "long", + "function-disallowed-list": ["rgb", "hsl"], + "declaration-block-no-duplicate-properties": [ + true, + { + "ignore": ["consecutive-duplicates-with-different-values"] + } + ], + "no-duplicate-selectors": true, + "custom-property-pattern": null, + "value-keyword-case": null, + "selector-pseudo-class-no-unknown": [ + true, + { + "ignorePseudoClasses": ["global"] + } + ], + "at-rule-no-unknown": [ + true, + { + "ignoreAtRules": ["tailwind"] + } + ] + }, + "defaultSeverity": "warning", + "ignoreFiles": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.svg", "node_modules/", "__mocks__/", "dist/"] +} diff --git a/.config/tsconfig.base.json b/.config/tsconfig.base.json new file mode 100644 index 00000000..0412052c --- /dev/null +++ b/.config/tsconfig.base.json @@ -0,0 +1,19 @@ +{ + "extends": "@sourcegraph/tsconfig", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "strict": true, + "lib": ["ESNext"], + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "noErrorTruncation": true, + "resolveJsonModule": true, + "composite": true, + "isolatedModules": true, + "outDir": "dist" + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1d86bc20 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Ensure everything is checked out using \n even on Windows, because some test +# code and snapshots assume this. +* text=auto eol=lf +CHANGELOG.md merge=union diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..79e30737 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +name: ci + +on: + push: + branches: + - main + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: .tool-versions + - uses: pnpm/action-setup@v2 + - run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + shell: bash + id: pnpm-cache + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-store- + - run: pnpm install + - run: pnpm run build + - run: pnpm run lint + - run: pnpm run format:check + + test-unit: + strategy: + fail-fast: false + matrix: + runner: [ubuntu, macos] + # Run on the most recently supported version of node for all bots. + node: [20] + include: + # Additionally, run the oldest supported version on Ubuntu. We don't + # need to run this on all platforms as we're only verifying we don't + # call any APIs not available in this version. + - runner: ubuntu + node: 18.15.0 # Supported by VS Code 1.85.0 (November 2023). + runs-on: ${{ matrix.runner }}-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + - uses: pnpm/action-setup@v2 + - run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + shell: bash + id: pnpm-cache + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-${{ matrix.node }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-${{ matrix.node }}-pnpm-store- + - run: pnpm install + - run: pnpm build + - run: pnpm run test:unit + + test-integration: + strategy: + fail-fast: false + matrix: + runner: [ubuntu, macos] + runs-on: ${{ matrix.runner }}-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: .tool-versions + - uses: pnpm/action-setup@v2 + - run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + shell: bash + id: pnpm-cache + - name: Cache pnpm store + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-pnpm-store- + - run: pnpm install + - run: xvfb-run -a pnpm -C client/vscode run test:integration + if: matrix.runner == 'ubuntu' + - run: pnpm -C client/vscode run test:integration + if: github.ref == 'refs/heads/main' && (matrix.runner == 'windows' || matrix.runner == 'macos') diff --git a/.github/workflows/scip-typescript.yml b/.github/workflows/scip-typescript.yml new file mode 100644 index 00000000..7be3e5df --- /dev/null +++ b/.github/workflows/scip-typescript.yml @@ -0,0 +1,48 @@ +name: scip-typescript +on: + push: + paths: + - '**.ts' + - '**.tsx' + - '**.js' + +jobs: + scip-typescript: + if: github.repository == 'sourcegraph/opencodegraph' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + id: pnpm-install + with: + run_install: false + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + name: Setup pnpm cache + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + - run: pnpm install --frozen-lockfile + - run: pnpm dlx @sourcegraph/scip-typescript index --pnpm-workspaces --no-global-caches + - name: Upload SCIP to Cloud + run: pnpm dlx @sourcegraph/src lsif upload -github-token='${{ secrets.GITHUB_TOKEN }}' -no-progress + env: + SRC_ENDPOINT: https://sourcegraph.com/ + - name: Upload SCIP to S2 + run: pnpm dlx @sourcegraph/src lsif upload -github-token='${{ secrets.GITHUB_TOKEN }}' -no-progress + env: + SRC_ENDPOINT: https://sourcegraph.sourcegraph.com/ + - name: Upload lsif to Dogfood + run: pnpm dlx @sourcegraph/src lsif upload -github-token='${{ secrets.GITHUB_TOKEN }}' -no-progress || true + env: + SRC_ENDPOINT: https://k8s.sgdev.org/ + - name: Upload lsif to Demo + run: pnpm dlx @sourcegraph/src lsif upload -github-token='${{ secrets.GITHUB_TOKEN }}' -no-progress || true + env: + SRC_ENDPOINT: https://demo.sourcegraph.com/ diff --git a/.github/workflows/vscode-pre-release.yml b/.github/workflows/vscode-pre-release.yml new file mode 100644 index 00000000..d0bbcfd6 --- /dev/null +++ b/.github/workflows/vscode-pre-release.yml @@ -0,0 +1,39 @@ +name: vscode-pre-release + +on: + schedule: + - cron: '0 15 * * *' # daily at 1500 UTC + workflow_dispatch: + +jobs: + release: + if: github.ref == 'refs/heads/main' && github.repository == 'sourcegraph/opencodegraph' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: .tool-versions + - uses: pnpm/action-setup@v2 + with: + run_install: true + - run: pnpm build + - run: pnpm run test:unit + - run: xvfb-run -a pnpm -C client/vscode run test:integration + - run: RELEASE_TYPE=pre pnpm -C client/vscode run release + if: github.ref == 'refs/heads/main' && github.repository == 'sourcegraph/opencodegraph' + env: + VSCODE_MARKETPLACE_TOKEN: ${{ secrets.VSCODE_MARKETPLACE_TOKEN }} + VSCODE_OPENVSX_TOKEN: ${{ secrets.VSCODE_OPENVSX_TOKEN }} + - name: Slack Notification + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_CHANNEL: opencodegraph + SLACK_ICON: https://github.com/sourcegraph.png?size=48 + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_MESSAGE: Pre-release build failed + SLACK_COLOR: danger + SLACK_FOOTER: '' + MSG_MINIMAL: actions url diff --git a/.github/workflows/vscode-stable-release.yml b/.github/workflows/vscode-stable-release.yml new file mode 100644 index 00000000..b3e51038 --- /dev/null +++ b/.github/workflows/vscode-stable-release.yml @@ -0,0 +1,65 @@ +name: vscode-stable-release + +on: + push: + tags: + - vscode-v* + +jobs: + release: + if: github.repository == 'sourcegraph/opencodegraph' + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: write # for publishing the release + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: .tool-versions + - uses: pnpm/action-setup@v2 + with: + run_install: true + - name: get release version + id: release_version + run: | + TAGGED_VERSION="${GITHUB_REF/refs\/tags\/vscode-v/}" + + if [[ ! "${TAGGED_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then + echo "Invalid version tag '${TAGGED_VERSION}'" + exit 1 + fi + + echo "EXT_VERSION=${TAGGED_VERSION}" >> $GITHUB_ENV + WRITTEN_VERSION="$(cat client/vscode/package.json | jq '.version' -r)" + + if [[ "${TAGGED_VERSION}" != "${WRITTEN_VERSION}" ]]; then + echo "Release tag and version in client/vscode/package.json do not match: '${TAGGED_VERSION}' vs. '${WRITTEN_VERSION}'" + exit 1 + fi + - run: pnpm build + - run: pnpm run test + - run: xvfb-run -a pnpm -C client/vscode run test:integration + - run: RELEASE_TYPE=stable pnpm -C client/vscode run release + if: github.repository == 'sourcegraph/opencodegraph' + env: + VSCODE_MARKETPLACE_TOKEN: ${{ secrets.VSCODE_MARKETPLACE_TOKEN }} + VSCODE_OPENVSX_TOKEN: ${{ secrets.VSCODE_OPENVSX_TOKEN }} + - name: create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: OpenCodeGraph for VS Code ${{ env.EXT_VERSION }} + draft: false + - name: upload release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: client/vscode/dist/opencodegraph.vsix + asset_name: opencodegraph-vscode-${{ env.EXT_VERSION }}.vsix + asset_content_type: application/zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..045b3f7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +.eslintcache +.stylelintcache +*.tsbuildinfo +.DS_Store +.env +.idea/ +out/ +dist/ +dist-ssr/ +/coverage/ +.vscode-test/ +.vscode-test-web/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..93f5656a --- /dev/null +++ b/.npmrc @@ -0,0 +1,17 @@ +hoist=false + +# needed by vike-react to fix the following error on `pnpm -C web dev`: +# +# 3:11:31 AM [vike][request(1)] Following error was thrown by the onRenderHtml() hook defined at vike-react/renderer/onRenderHtml +# Error: Cannot find package 'react-dom' imported from /home/sqs/src/github.com/sourcegraph/opencodegraph/node_modules/.pnpm/@brillout+import@0.2.3/node_modules/@brillout/import/dist/index.js +public-hoist-pattern[]=*react-dom* + +# Needed to use eslint plugins in the `.eslintrc` config. +public-hoist-pattern[]=*eslint* + +# Needed to import package README.md files in the web content pages. +public-hoist-pattern[]=@mdx-js/react # needed for client/codemirror/README.md +public-hoist-pattern[]=@code-hike/mdx # needed for client/codemirror/README.md +public-hoist-pattern[]=react # needed for client/vscode/README.md +public-hoist-pattern[]=@storybook/html # needed to avoid: [vite] Internal server error: Failed to resolve import "@storybook/html/dist/entry-preview.mjs" from "../../../../../../../../virtual:/@storybook/builder-vite/vite-app.js". Does the file exist? +public-hoist-pattern[]=@swc/wasm # fix `Could not resolve "@swc/wasm"` from @swc/core in vscode desktop build diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..d2420543 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +nodejs 20.10.0 +pnpm 8.12.1 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..e783609e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + "recommendations": [ + "sourcegraph.opencodegraph", + "sourcegraph.cody-ai", + "esbenp.prettier-vscode", + "ecmel.vscode-html-css", + "dbaeumer.vscode-eslint", + "stylelint.vscode-stylelint", + "tamasfe.even-better-toml", + "unifiedjs.vscode-mdx", + "bradlc.vscode-tailwindcss" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..8f3025c0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,70 @@ +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Launch VS Code Extension (Desktop)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "preLaunchTask": "Build VS Code Extension (Desktop)", + "args": ["--extensionDevelopmentPath=${workspaceRoot}/client/vscode"], + "sourceMaps": true, + "outFiles": ["${workspaceRoot}/client/vscode/dist/**/*.js"], + "internalConsoleOptions": "openOnSessionStart", + "env": { + "NODE_ENV": "development", + "NODE_TLS_REJECT_UNAUTHORIZED": "0" + // Enable the Node debug protocol for the TypeScript server: + // "TSS_DEBUG": "5859" + } + }, + { + "name": "Launch VS Code Extension (Desktop; Separate Instance)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "preLaunchTask": "Build VS Code Extension (Desktop)", + "args": [ + "--user-data-dir=/tmp/vscode-opencodegraph-extension-dev-host", + "--profile-temp", + "--extensionDevelopmentPath=${workspaceRoot}/client/vscode" + ], + "sourceMaps": true, + "outFiles": ["${workspaceRoot}/client/vscode/dist/**/*.js"], + "env": { + "NODE_ENV": "development" + } + }, + { + "name": "Launch VS Code Extension (Web, in Browser)", + "type": "node", + "request": "launch", + "preLaunchTask": "Build VS Code Extension (Web)", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["-C", "${workspaceFolder}/client/vscode", "run", "--silent", "_dev:vscode-test-web"], + "outFiles": ["${workspaceFolder}/client/vscode/dist/**/*.js"] + }, + { + "name": "Launch VS Code Extension (Web Extension Host)", + "type": "extensionHost", + "debugWebWorkerHost": true, + "request": "launch", + "preLaunchTask": "Build VS Code Extension (Web)", + "outFiles": ["${workspaceFolder}/client/vscode/dist/**/*.js"], + "args": ["--extensionDevelopmentPath=${workspaceRoot}/client/vscode", "--extensionDevelopmentKind=web"] + }, + { + "type": "node", + "request": "launch", + "name": "Debug Current File with Vitest", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", + // ${relativeFile} will guarantee the "current" file, but fileBaseNameNoExtension + // can be convenient because running with a file like "graph-section-observer.ts" + // (the implementation, not the test) will run the correct tests. + "args": ["run", "${fileBasenameNoExtension}"], + "smartStep": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..cf572229 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,37 @@ +{ + "search.exclude": { + "**/node_modules": true, + "**/dist": true + }, + "files.exclude": { + "**/.eslintcache": true, + "**/dist": true, + "**/.vscode-test": true, + "**/.vscode-test-web": true, + "coverage/": true, + "pnpm-lock.yaml": true, + ".stylelintcache": true + }, + "editor.formatOnSave": true, + "npm.packageManager": "pnpm", + "npm.runSilent": true, + "typescript.preferences.quoteStyle": "single", + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.format.semicolons": "remove", + "typescript.tsc.autoDetect": "off", + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.preferences.importModuleSpecifierEnding": "auto", + "task.allowAutomaticTasks": "on", + "eslint.lintTask.enable": false, + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "mdx"], + "[typescriptreact][typescript][jsonc][css][javascript][mdx]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "css.lint.unknownAtRules": "ignore", + "[mdx][markdown]": { + "editor.insertSpaces": true + }, + "opencodegraph.providers": { + // "https://sourcegraph.test:3443/.api/opencodegraph": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..08449bd6 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,38 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "typescript", + "tsconfig": "tsconfig.json", + "problemMatcher": ["$tsc-watch"], + "group": { + "kind": "build", + "isDefault": true + }, + "option": "watch", + "runOptions": { "runOn": "folderOpen", "instanceLimit": 1 }, + "isBackground": true, + "presentation": { + "reveal": "never" + } + }, + { + "label": "Build VS Code Extension (Desktop)", + "type": "npm", + "path": "client/vscode", + "script": "build:dev:desktop", + "problemMatcher": "$tsc-watch", + "options": { "cwd": "client/vscode" }, + "isBackground": true + }, + { + "label": "Build VS Code Extension (Web)", + "type": "npm", + "path": "client/vscode", + "script": "build:dev:web", + "problemMatcher": "$tsc-watch", + "options": { "cwd": "client/vscode" }, + "isBackground": true + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..c3ad6a17 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Sourcegraph, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + 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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..23caed1f --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# OpenCodeGraph logo OpenCodeGraph + + + +See contextual info about code from your dev tools, in your editor and anywhere else you read code. + +OpenCodeGraph is an open standard for annotating code with info from other dev tools (logs, docs, UI storybooks, service catalogs, analytics, etc.). + +[**Get started**](https://opencodegraph.org/docs/start) in VS Code, GitHub (via browser extension), Sourcegraph, or in the playground. + +See [opencodegraph.org](https://opencodegraph.org) for more info. + +_Status: alpha_ + +--- + +## Screenshots + +#### Hover over a UI component in your editor to see what it looks like + +Screenshot of OpenCodeGraph annotations in VS Code +

+ +#### Jump from a GitHub PR to what a Prometheus metric is doing in prod + +Screenshot of OpenCodeGraph anotations in a GitHub pull request +

+ +#### See links to internal docs when reviewing code on GitHub + +Screenshot of OpenCodeGraph annotations in a GitHub code file +

+ +## Development + +- [Source code](https://github.com/sourcegraph/opencodegraph) +- [Docs](https://opencodegraph.org) +- License: [Apache 2.0](LICENSE) diff --git a/client/browser/.gitignore b/client/browser/.gitignore new file mode 100644 index 00000000..c4c4ffc6 --- /dev/null +++ b/client/browser/.gitignore @@ -0,0 +1 @@ +*.zip diff --git a/client/browser/CHANGELOG.md b/client/browser/CHANGELOG.md new file mode 100644 index 00000000..7ab1e44f --- /dev/null +++ b/client/browser/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to the OpenCodeGraph browser extension will be documented in this file. + +## (Unreleased) + +## 0.0.0 + +- Initial release. diff --git a/client/browser/CONTRIBUTING.md b/client/browser/CONTRIBUTING.md new file mode 100644 index 00000000..8e2f867e --- /dev/null +++ b/client/browser/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# Contributing to the OpenCodeGraph browser extension + +## Building and running locally + +1. Run `pnpm run dev` in this directory. +1. Go to [chrome://extensions](chrome://extensions). +1. If you already have the OpenCodeGraph extension installed from the [Chrome Web Store](https://chromewebstore.google.com/detail/opencodegraph/indllinbfleghfhhaglfgohfceffendm), disable it using the toggle. +1. Enable **Developer mode**. +1. Click [Load unpacked extensions](https://developer.chrome.com/extensions/getstarted#unpacked) and open the `opencodegraph/client/browser/dist` directory. +1. Browse to any public repository on GitHub to confirm it is working. See [README.md](README.md) for some examples. diff --git a/client/browser/README.md b/client/browser/README.md new file mode 100644 index 00000000..37f8a0d1 --- /dev/null +++ b/client/browser/README.md @@ -0,0 +1,63 @@ +# OpenCodeGraph browser extension + +The [OpenCodeGraph](https://opencodegraph.org) browser extension enhances your code host's UI with contextual info from your other dev tools. + +## Usage + +**Install it for:** + +- [Google Chrome](https://chromewebstore.google.com/detail/indllinbfleghfhhaglfgohfceffendm) +- Coming soon: Firefox, Safari, Brave, Arc, Edge, and any other browser that supports the [WebExtensions API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API) + +Supported code hosts: + +- [GitHub](https://opencodegraph.org/docs/clients/github) (code view, pull request diff view) +- Coming soon: GitLab, Bitbucket Cloud & Server + +### Setup + +After installing it in Chrome, visit the following pages to see it in action: + +- [Example code file on GitHub](https://github.com/sourcegraph/sourcegraph/blob/main/internal/repos/conf.go) +- [Example pull request on GitHub](https://github.com/sourcegraph/sourcegraph/pull/58878/files) + +Click the extension's icon to change your [configuration](https://opencodegraph.org/docs/concepts#user-configuration). + +## Screenshots + +![Screenshot of OpenCodeGraph annotations in a GitHub pull request](https://storage.googleapis.com/sourcegraph-assets/opencodegraph/screenshot-browser-github-pr-v0.png) + +_See relevant docs when reviewing a GitHub PR_ + +![Screenshot of OpenCodeGraph annotations in the GitHub code view](https://storage.googleapis.com/sourcegraph-assets/opencodegraph/screenshot-browser-github-codeview-v0.png) + +_And when viewing files on GitHub._ + +## Known issues + +### No remotely hosted `.js` OpenCodeGraph providers + +Because of the [restriction on remotely hosted code](https://developer.chrome.com/docs/extensions/develop/migrate/remote-hosted-code) in Chrome extensions, the browser extension does not support all OpenCodeGraph context providers. + +Supported providers: + +- All providers that are implemented as an [HTTP endpoint](https://opencodegraph.org/docs/protocol) +- The following additional providers: + - [Links](https://opencodegraph.org/docs/providers/links) + - [Storybook](https://opencodegraph.org/docs/providers/storybook) + - [Prometheus](https://opencodegraph.org/docs/providers/prometheus) + - [Hello World](https://opencodegraph.org/docs/providers/hello-world) + +Unsupported providers: + +- Any other provider that is implemented as a JavaScript bundle executed on the client + +### GitHub pull request files are annotated with only partial file contents + +Only the displayed portion of the diffs of GitHub pull request files are sent to the OpenCodeGraph provider. Providers do not receive the full contents of the changed files, nor do they receive the diff markers to let them know whether each line was an addition, removal, edit, or context. + +## Development + +- [Source code](https://sourcegraph.com/github.com/sourcegraph/opencodegraph/-/tree/client/browser) +- [Docs](https://opencodegraph.org/docs/clients/browser-extension) +- License: Apache 2.0 diff --git a/client/browser/dev/create-icons.sh b/client/browser/dev/create-icons.sh new file mode 100644 index 00000000..b7d0b142 --- /dev/null +++ b/client/browser/dev/create-icons.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -eu +INIT_CWD=$PWD +cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null + +INPUT_SVG=../../../web/public/logomark-v0.svg + +SIZES=( + 32 + 48 + 128 +) +for size in "${SIZES[@]}"; do + out=$(realpath ../public/icon-${size}.png) + pnpx svgexport@0.4.2 "$INPUT_SVG" "$out" png 100% ${size}:${size} + echo ${out/$INIT_CWD\//} +done diff --git a/client/browser/manifest.config.ts b/client/browser/manifest.config.ts new file mode 100644 index 00000000..3e3eb3f1 --- /dev/null +++ b/client/browser/manifest.config.ts @@ -0,0 +1,43 @@ +import { defineManifest } from '@crxjs/vite-plugin' +import packageJson from './package.json' + +const { version } = packageJson + +// eslint-disable-next-line import/no-default-export +export default defineManifest(env => ({ + manifest_version: 3, + name: env.mode === 'development' ? 'OpenCodeGraph [dev]' : 'OpenCodeGraph', + description: "Enhance your code host's UI with contextual info from your other dev tools.", + version, + + icons: { + '32': 'icon-32.png', + '48': 'icon-48.png', + '128': 'icon-128.png', + }, + action: { + default_title: 'OpenCodeGraph', + default_icon: { + '32': 'icon-32.png', + '48': 'icon-48.png', + '128': 'icon-128.png', + }, + default_popup: 'src/options/options.html', + }, + permissions: ['storage'], + options_ui: { + page: 'src/options/options.html', + open_in_tab: true, + }, + background: { + service_worker: 'src/background/background.main.ts', + type: 'module' as const, + }, + content_scripts: [ + { + matches: ['https://github.com/*'], + js: ['src/contentScript/contentScript.main.ts'], + run_at: 'document_idle', + }, + ], +})) diff --git a/client/browser/package.json b/client/browser/package.json new file mode 100644 index 00000000..91e48100 --- /dev/null +++ b/client/browser/package.json @@ -0,0 +1,42 @@ +{ + "private": true, + "name": "@opencodegraph/browser-extension", + "version": "0.0.3", + "license": "Apache-2.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "release": "pnpm release:chrome", + "release:chrome": "rm -rf dist && pnpm run -s build && cd dist && zip -r opencodegraph-chrome-extension.zip * && mv opencodegraph-chrome-extension.zip ..", + "test": "vitest", + "dev:create-icons": "bash dev/create-icons.sh" + }, + "type": "module", + "dependencies": { + "@opencodegraph/client": "workspace:*", + "@opencodegraph/provider": "workspace:*", + "@opencodegraph/provider-hello-world": "workspace:*", + "@opencodegraph/provider-links": "workspace:*", + "@opencodegraph/provider-storybook": "workspace:*", + "@opencodegraph/provider-prometheus": "workspace:*", + "@opencodegraph/ui-standalone": "workspace:*", + "clsx": "^2.0.0", + "deep-equal": "^2.2.3", + "jsonc-parser": "^3.2.0", + "observable-hooks": "^4.2.3", + "react": "^18", + "react-dom": "^18", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^1.0.14", + "@types/chrome": "^0.0.254", + "@types/deep-equal": "^1.0.4", + "@types/react": "18.2.37", + "@types/react-dom": "18.2.15", + "@vitejs/plugin-react": "^4.0.2", + "svgexport": "^0.4.2", + "vite-plugin-monaco-editor": "^1.1.0", + "webextension-polyfill": "^0.10.0" + } +} diff --git a/client/browser/public/icon-128.png b/client/browser/public/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..1ee97363a9805add17628148d39484e33a3401ab GIT binary patch literal 7990 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 literal 0 HcmV?d00001 diff --git a/client/browser/public/icon-48.png b/client/browser/public/icon-48.png new file mode 100644 index 0000000000000000000000000000000000000000..f8d1feac7ccc0389a2ae2368c7cf559c57f760c8 GIT binary patch literal 2287 zcmVPx-sYygZRA_;NfpOGuX{W*c4B+-V}ptKVeb+Y1c(`l3tA*5e?h!jR$?~_ zv8CO^YLQ|~h!8GuBv4>4FMY7d%LXNYtR%wBttl0t4h(1Y;Wjwy zdtWS2$0)C11FZjp0m~Cw7PyFjs!?9OF;zi?D^EY-!-0`>16&7MJDkQ-)<-~TBOn6m zumUhw(yCZoKp6#Usw_;_qDHHAn29mpIJ~kfr8zv#3 zfb)d+=a1;32;=6%V4U&!qdGtFNG6l|?;gaS%T+zk`_w04}&K z^XxjNP94DE@!x*M_XFi)A~5lrk|r`(wgF1b*IJ}KeV~m10@{MD4Wm?8oIPl5d5U^U z6Ea4<--9&@B4pC2D_q>a0XFlh)Vm>oKx+rK??#*N)jUs!5pX&YsQsNnrXE0=ftd!J znU@;ER5t|t_l?K!^uO))yY~q^o`yi94!aM!sSm2ulj)n<2{Irs3!0I)vin0iiw4kg|9(oC$ z+~tYeUTai3I{RJq9X=&@5A1so|Lan)=WYemmC=HBiSpV*ibwUJNE*Ey+7zcz-@vy zX)jWch(P^L=QvgY`j12L1PqMuWbh?!=ZdVZiTVLJugX%FsemImXSsX_AI)ug(CIqUh*R@iamYN>}J#Q#- zMJODFrq2lp2sF05>NEtzd_VR|MA>%0`%a-fEyao$F-n73V8S%dvyw(rNz!-7Fb&WV z1I=x~Y4dzy3^-urz2=%+;?J0HDedEGC`T}sfjR{<<*h(@Wf`IBvp|^UiF<@LTL?zE*0y1$#GJ=5-?|Ru zhoEl&@}*F2rLq3P{l}017AP=UnU-3BfG9HxrEre3-?HH;?+@+@9Y~2~cpl6K$ZeV| zHux@%v}If@@F(E+l^@AZh=~Ba|Eg*SA1Cp%bUynuu=}yMsgcloV*S`K@(oWZ@3;F1 z+=huE$${AIcO9Z^s4!i*)ZSuR%i~&t5M_0V{v^KEreUcd?{}g(k9s??!jVoEDI7EP zxkF~9w{j`U8+VTiEUsQ_@6N0s-R5GPkHf|`wmn09WkdWQE(=E!c-%6n_kPX@ z#p3$vb>n!;(|f5GPsjP%H=tYd$tRspd8ey-4J=AqZgF_5CLZa;1njy?*UJV8Bpvk1`q6i*Or&ci#czzDB?;4|$NHf)2cnMSH+Dyfrx z!VwXdHOQEGb0sZ_u-xsmU~tUjMB(E?jj@p34=1aDWt0uGkl02GD^%{)4aKI3!e-@? zj6%w$zE|a~yy+o4*WH8|7##B`lH?c&R#>mhmuN`Jk(+l)5}Y3;8emyWbyFS;^(C=Q zgoPAJr(s|Ousc~Ol5TjV2>IeM{wg-_sPZ1>)uzG%O1bib_VzN-PF2pv2TGE@0WZEo*dpW?Ms=jBY64rxoqSU2VEq6fQXBXGPDYLhpM zR#=h{2ubzMjpQT6OalZGPXHnf^E5inIul}G@Wp7mT))X&eMyReuqesrrP&53nfFzy z)N^_ETyKtdAmFdO$DojKP}6tFTYc$@0IAnkSq6N!Ci8e3hEDDIpfJRbU6;XB1ibR- zSJDU04}n;G)C~dR&%GD`aN&_mh|46(O|PR(;Od1>q>S**z|$EB44=v#ylXMx{Y9+!0v{HRa5}4hu{bHhga|9D42M|=W6#2=bJ^Fg z2fJoF8rMIBaDxsZWC zefT851TJ;yst)j_w`GGaY?CGjLcrc-28Vho0xf1={{Ga0ZSSzpvrYnO5kHpX5*_> = { + ['https://opencodegraph.org/npm/@opencodegraph/provider-hello-world']: helloWorldProvider, + ['https://opencodegraph.org/npm/@opencodegraph/provider-links']: linksProvider, + ['https://opencodegraph.org/npm/@opencodegraph/provider-storybook']: storybookProvider, + ['https://opencodegraph.org/npm/@opencodegraph/provider-prometheus']: prometheusProvider, +} + +function getBuiltinProvider(uri: string): OpenCodeGraphProvider { + const mod = BUILTIN_PROVIDER_MODULES[uri] + if (!mod) { + throw new Error( + `Only HTTP endpoint providers and the following built-in providers are supported: ${Object.keys( + BUILTIN_PROVIDER_MODULES + ).join(', ')}. See https://opencodegraph.org/docs/clients/browser-extension#known-issues.` + ) + } + return mod +} + +function main(): void { + const subscriptions = new Subscription() + + const client = createClient({ + configuration: () => configurationChanges, + logger: console.error, + makeRange: r => r, + dynamicImportFromUri: uri => Promise.resolve({ default: getBuiltinProvider(uri) }), + }) + subscriptions.add(() => client.dispose()) + + subscriptions.add( + addMessageListenersForBackgroundApi({ + annotationsChanges: (...args) => client.annotationsChanges(...args), + }) + ) + + self.addEventListener('unload', () => subscriptions.unsubscribe(), { once: true }) +} + +// Browsers log an unhandled Promise here automatically with a nice stack trace, so we don't need to +// `.catch(...)` it. +main() diff --git a/client/browser/src/browser-extension/web-extension-api/ExtensionStorageSubject.ts b/client/browser/src/browser-extension/web-extension-api/ExtensionStorageSubject.ts new file mode 100644 index 00000000..83c8e619 --- /dev/null +++ b/client/browser/src/browser-extension/web-extension-api/ExtensionStorageSubject.ts @@ -0,0 +1,32 @@ +import { Observable, type BehaviorSubject, type NextObserver } from 'rxjs' +import { observeStorageKey, storage } from './storage' +import type { LocalStorageItems } from './types' + +/** + * An RxJS subject that is backed by an extension storage item. + */ +export class ExtensionStorageSubject + // eslint-disable-next-line rxjs/no-subclass + extends Observable + implements NextObserver, Pick, 'value'> +{ + constructor( + private key: T, + defaultValue: LocalStorageItems[T] + ) { + super(subscriber => { + subscriber.next(this.value) + return observeStorageKey('local', this.key).subscribe((item = defaultValue) => { + this.value = item + subscriber.next(item) + }) + }) + this.value = defaultValue + } + + public async next(value: LocalStorageItems[T]): Promise { + await storage.local.set({ [this.key]: value }) + } + + public value: LocalStorageItems[T] +} diff --git a/client/browser/src/browser-extension/web-extension-api/README.md b/client/browser/src/browser-extension/web-extension-api/README.md new file mode 100644 index 00000000..6a4ae050 --- /dev/null +++ b/client/browser/src/browser-extension/web-extension-api/README.md @@ -0,0 +1,3 @@ +# Browser APIs + +This directory contains a set of helper functions that wrap the [APIs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API) exposed by the browser to browser extensions. This originally came about while implementing support for Safari. Rather than write an entirely new extension, we wrapped the APIs and implemented pseudo support for these APIs in Safari. At this time, we no longer support Safari, however we have kept these because there are still differences in the APIs between Chrome and Firefox. This abstraction also allowed us to have a more type safe interaction with the APIs in actual application code. diff --git a/client/browser/src/browser-extension/web-extension-api/fromBrowserEvent.ts b/client/browser/src/browser-extension/web-extension-api/fromBrowserEvent.ts new file mode 100644 index 00000000..0dff31dc --- /dev/null +++ b/client/browser/src/browser-extension/web-extension-api/fromBrowserEvent.ts @@ -0,0 +1,20 @@ +import { Observable } from 'rxjs' + +/** + * Returns an Observable for a WebExtension API event listener. + * The handler will always return `void`. + */ +export const fromBrowserEvent = void>( + emitter: browser.CallbackEventEmitter +): Observable> => + // Do not use fromEventPattern() because of https://github.com/ReactiveX/rxjs/issues/4736 + new Observable(subscriber => { + const handler: any = (...args: any) => subscriber.next(args) + try { + emitter.addListener(handler) + } catch (error) { + subscriber.error(error) + return undefined + } + return () => emitter.removeListener(handler) + }) diff --git a/client/browser/src/browser-extension/web-extension-api/rpc.ts b/client/browser/src/browser-extension/web-extension-api/rpc.ts new file mode 100644 index 00000000..4b223855 --- /dev/null +++ b/client/browser/src/browser-extension/web-extension-api/rpc.ts @@ -0,0 +1,155 @@ +import { Observable, Subscription, type Unsubscribable } from 'rxjs' +import { isBackground } from '../../shared/env' +import { type BackgroundApi } from './types' + +interface RequestMessage { + /** + * If defined, this request is expecting an Observable (multiple emitted values) in response. + * The streamId is a unique and opaque identifier that all responses will be associated with so + * that the caller can associate them with this request. + * + * If `undefined`, the request is expecting a Promise (single emitted value), and no stream is + * needed. + */ + streamId?: string + + /** + * The name of the method to invoke. + */ + method: string + + /** + * The method arguments. + */ + args: any[] +} + +interface ResponseMessage { + /** + * If defined, this response is an emitted value (or error/completion event) from a request that + * expects an Observable (multiple emitted values). All responses to that request use the same + * `streamId` as the request so they can be associated with it. + * + * If `undefined`, this response is a single value. + */ + streamId?: string + + streamEvent?: 'next' | 'error' | 'complete' + + /** + * For non-stream responses or for `next`/`error` stream events, the data. + */ + data?: any +} + +// This function generates a unique ID for each message stream. +function generateStreamId(): string { + return Math.random().toString(36).slice(2) + Date.now().toString(36) +} + +// This function sends a message and returns an Observable that will emit the responses +function callBackgroundMethodReturningObservable(method: string, args: unknown[]): Observable { + const streamId = generateStreamId() + const request: RequestMessage = { streamId, method, args } + + return new Observable(observer => { + // Set up a listener for messages from the background. + const messageListener = (response: ResponseMessage): void => { + // If the message is on the stream for this call, emit it. + if (response.streamId === streamId) { + switch (response.streamEvent) { + case 'next': + observer.next(response.data) + break + case 'error': + observer.error(response.data) + break + case 'complete': + observer.complete() + break + } + } + } + + chrome.runtime.onMessage.addListener(messageListener) + + chrome.runtime.sendMessage(request).catch(console.error) + + return () => { + chrome.runtime.onMessage.removeListener(messageListener) + } + }) +} + +/** + * Create a proxy for an Observable-returning background API method. + */ +export function proxyBackgroundMethodReturningObservable(method: M): BackgroundApi[M] { + if (isBackground) { + throw new Error('tried to call background service worker function from background service worker itself') + } + return (...args: any[]) => callBackgroundMethodReturningObservable(method, args) as ReturnType +} + +/** + * Set up the background service worker to handle API requests from the content script. + */ +export function addMessageListenersForBackgroundApi(api: BackgroundApi): Unsubscribable { + if (!isBackground) { + throw new Error('must be called from background') + } + + const subscriptions = new Subscription() + + const handler = ( + { streamId, method, args }: RequestMessage, + sender: browser.runtime.MessageSender + ): Promise => { + if (streamId === undefined) { + throw new Error('non-Observable-returning RPC calls are not yet implemented') + } + + const handler = api[method as keyof BackgroundApi] + if (!handler) { + throw new Error(`Invalid RPC call for method ${JSON.stringify(method)}`) + } + + const senderTabId = sender.tab?.id + if (!senderTabId) { + throw new Error('no sender tab ID') + } + + let subscription: Subscription | undefined + // eslint-disable-next-line prefer-const + subscription = handler.apply(api, args as any).subscribe({ + next: value => { + browser.tabs + .sendMessage(senderTabId, { streamId, streamEvent: 'next', data: value }) + .catch(console.error) + }, + error: error => { + browser.tabs + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + .sendMessage(senderTabId, { streamId, streamEvent: 'error', data: error.toString() }) + .catch(console.error) + if (subscription) { + subscriptions.remove(subscription) + } + }, + complete: () => { + browser.tabs.sendMessage(senderTabId, { streamId, streamEvent: 'complete' }).catch(console.error) + if (subscription) { + subscriptions.remove(subscription) + } + }, + }) + subscriptions.add(subscription) + return Promise.resolve() + } + + browser.runtime.onMessage.addListener(handler) + + subscriptions.add(() => browser.runtime.onMessage.removeListener(handler)) + + return subscriptions +} diff --git a/client/browser/src/browser-extension/web-extension-api/runtime.ts b/client/browser/src/browser-extension/web-extension-api/runtime.ts new file mode 100644 index 00000000..d2ce5d65 --- /dev/null +++ b/client/browser/src/browser-extension/web-extension-api/runtime.ts @@ -0,0 +1,9 @@ +import { proxyBackgroundMethodReturningObservable } from './rpc' +import type { BackgroundApi } from './types' + +/** + * Functions invoked from content scripts that will be executed in the background service worker. + */ +export const background: BackgroundApi = { + annotationsChanges: proxyBackgroundMethodReturningObservable('annotationsChanges'), +} diff --git a/client/browser/src/browser-extension/web-extension-api/storage.ts b/client/browser/src/browser-extension/web-extension-api/storage.ts new file mode 100644 index 00000000..c0ff24db --- /dev/null +++ b/client/browser/src/browser-extension/web-extension-api/storage.ts @@ -0,0 +1,55 @@ +import { concat, defer, filter, from, map, of, type Observable } from 'rxjs' +import { platform } from '../../shared/platform' +import { fromBrowserEvent } from './fromBrowserEvent' +import type { LocalStorageItems, ManagedStorageItems, SyncStorageItems } from './types' + +interface ExtensionStorageItems { + local: LocalStorageItems + sync: SyncStorageItems + managed: ManagedStorageItems +} + +/** + * Type-safe access to browser extension storage. + */ +export const storage: { + [K in browser.storage.AreaName]: browser.storage.StorageArea +} & { + onChanged: browser.CallbackEventEmitter< + ( + changes: browser.storage.ChangeDict, + areaName: browser.storage.AreaName + ) => void + > +} = globalThis.browser && browser.storage + +export const observeStorageKey = ( + areaName: A, + key: K +): Observable => { + if (platform !== 'chrome-extension' && areaName === 'managed') { + // Accessing managed storage throws an error on Firefox and on Safari. + return of(undefined) + } + return concat( + // Start with current value of the item + defer(() => + from((storage[areaName] as browser.storage.StorageArea).get(key)).pipe( + map(items => (items as ExtensionStorageItems[A])[key]) + ) + ), + // Emit every new value from change events that affect that item + fromBrowserEvent(storage.onChanged).pipe( + filter(([, name]) => areaName === name), + map(([changes]) => changes), + filter( + ( + changes + ): changes is { + [k in K]: browser.storage.StorageChange + } => Object.prototype.hasOwnProperty.call(changes, key) + ), + map(changes => changes[key].newValue) + ) + ) +} diff --git a/client/browser/src/browser-extension/web-extension-api/types.ts b/client/browser/src/browser-extension/web-extension-api/types.ts new file mode 100644 index 00000000..cc629bb0 --- /dev/null +++ b/client/browser/src/browser-extension/web-extension-api/types.ts @@ -0,0 +1,35 @@ +import { type Client, type OpenCodeGraphRange } from '@opencodegraph/client' + +/** + * Wrapper type for a string of JSONC (JSON with comments and trailing commas). + * + * A wrapper type is used to reduce the likelihood that it is accidentally parsed as (less-tolerant) + * JSON. + */ +interface JSONCString { + jsonc: string +} + +export interface LocalStorageItems {} + +export interface SyncStorageItems { + configuration: JSONCString +} + +export interface ManagedStorageItems {} + +/** + * Functions in the background page that can be invoked from content scripts. + */ +export interface BackgroundApi extends Pick, 'annotationsChanges'> {} + +/** + * Shape of the handler object in the background worker. + * The handlers get access to the sender tab of the message as a parameter. + */ +export type BackgroundApiHandlers = { + [M in keyof BackgroundApi]: ( + args: Parameters, + sender: browser.runtime.MessageSender + ) => ReturnType +} diff --git a/client/browser/src/configuration.ts b/client/browser/src/configuration.ts new file mode 100644 index 00000000..a0cb8f89 --- /dev/null +++ b/client/browser/src/configuration.ts @@ -0,0 +1,68 @@ +import { type ClientConfiguration } from '@opencodegraph/client' +import { parse as parseJSONC, type ParseError } from 'jsonc-parser' +import { map, type Observable } from 'rxjs' +import { observeStorageKey } from './browser-extension/web-extension-api/storage' + +const DEFAULT_CONFIG: ClientConfiguration = { + enable: true, + providers: { + 'https://opencodegraph.org/npm/@opencodegraph/provider-hello-world': true, + 'https://opencodegraph.org/npm/@opencodegraph/provider-links': { + links: [ + { + title: 'Telemetry', + url: 'https://docs.sourcegraph.com/dev/background-information/telemetry#sourcegraph-web-app', + type: 'docs', + preview: true, + path: '**/*.ts?(x)', + pattern: 'eventLogger\\.', + }, + { + title: 'CSS in client/web', + url: 'https://docs.sourcegraph.com/dev/background-information/web/styling#styling-ui', + type: 'docs', + preview: true, + path: '**/*.ts?(x)', + pattern: '^import styles from', + }, + { + title: '🐘 $ table (PostgreSQL console)', + url: 'https://example.com/postgresql?table=$', + description: 'View table schema and data...', + path: '**', + pattern: '(FROM|UPDATE|INSERT INTO|DELETE FROM|ALTER TABLE) (?
    \\w+)', + }, + ], + } satisfies import('@opencodegraph/provider-links').Settings, + 'https://opencodegraph.org/npm/@opencodegraph/provider-prometheus': { + metricRegistrationPatterns: [ + { + path: '**/*.go', + pattern: 'prometheus\\.HistogramOpts{\\s*Name:\\s*"([^"]+)', + urlTemplate: 'https://prometheus.demo.do.prometheus.io/graph?g0.expr=$1&g0.tab=0&g0.stacked=1', + }, + ], + } satisfies import('@opencodegraph/provider-prometheus').Settings, + 'https://opencodegraph.org/npm/@opencodegraph/provider-storybook': { + storybookUrl: 'https://daeeaa811098f52f15a110dbaf76b6c416191c3b--5f0f381c0e50750022dc6bf7.chromatic.com/', // this is a public URL because our storybooks are public + } satisfies import('@opencodegraph/provider-storybook').Settings, + }, +} + +export const configurationStringChanges: Observable = observeStorageKey('sync', 'configuration').pipe( + map(c => c ?? { jsonc: JSON.stringify(DEFAULT_CONFIG, null, 2) }), + map(({ jsonc: jsoncStr }) => jsoncStr) +) + +export const configurationChanges: Observable = configurationStringChanges.pipe( + map(jsoncStr => { + const errors: ParseError[] = [] + const obj = parseJSONC(jsoncStr, errors, { + allowTrailingComma: true, + }) as ClientConfiguration + if (errors.length > 0) { + console.error('Error parsing configuration (as JSONC):', errors) + } + return obj + }) +) diff --git a/client/browser/src/contentScript/contentScript.main.css b/client/browser/src/contentScript/contentScript.main.css new file mode 100644 index 00000000..fa15a50e --- /dev/null +++ b/client/browser/src/contentScript/contentScript.main.css @@ -0,0 +1,41 @@ +.ocg-line-chips { + margin-left: 7px; +} + +.ocg-chip { + font-size: 83%; + line-height: normal; + font-family: system-ui, sans-serif; +} + +html[data-color-mode='light'] { + & .ocg-chip { + background: #00000011; + border: solid 1px #00000019; + color: black; + + &:hover { + background-color: #00000022; + } + } + + & .ocg-chip-popover { + border: solid 1px #00000019; + } +} + +html[data-color-mode='dark'] { + & .ocg-chip { + background: #ffffff3a; + border: solid 1px #ffffff55; + color: white; + + &:hover { + background-color: #ffffff22; + } + } + + & .ocg-chip-popover { + border: solid 1px #ffffff55; + } +} diff --git a/client/browser/src/contentScript/contentScript.main.ts b/client/browser/src/contentScript/contentScript.main.ts new file mode 100644 index 00000000..5376ca08 --- /dev/null +++ b/client/browser/src/contentScript/contentScript.main.ts @@ -0,0 +1,38 @@ +import '../shared/polyfills' +// ^^ import polyfills first +import { type Annotation } from '@opencodegraph/client' +import { type AnnotationsParams } from '@opencodegraph/provider' +import deepEqual from 'deep-equal' +import { combineLatest, distinctUntilChanged, mergeMap, throttleTime, type Observable } from 'rxjs' +import { background } from '../browser-extension/web-extension-api/runtime' +import './contentScript.main.css' +import { debugTap } from './debug' +import { injectOnGitHubCodeView } from './github/codeView' +import { injectOnGitHubPullRequestFilesView } from './github/pullRequestFilesView' +import { locationChanges } from './locationChanges' + +/** + * A function called to inject OpenCodeGraph features on a page. They should just return an empty + * Observable if they are not intended for the current page. + */ +type Injector = (location: URL, annotationsChanges_: typeof annotationsChanges) => Observable + +const INJECTORS: Injector[] = [injectOnGitHubCodeView, injectOnGitHubPullRequestFilesView] + +const subscription = locationChanges + .pipe(mergeMap(location => combineLatest(INJECTORS.map(injector => injector(location, annotationsChanges))))) + .subscribe() +window.addEventListener('unload', () => subscription.unsubscribe()) + +function annotationsChanges(params: AnnotationsParams): Observable { + return background.annotationsChanges(params).pipe( + distinctUntilChanged((a, b) => deepEqual(a, b)), + throttleTime(200, undefined, { leading: true, trailing: true }), + debugTap(annotations => { + console.groupCollapsed('annotationsChanges') + console.count('annotationsChanges count') + console.log(annotations) + console.groupEnd() + }) + ) +} diff --git a/client/browser/src/contentScript/debug.ts b/client/browser/src/contentScript/debug.ts new file mode 100644 index 00000000..729a2c42 --- /dev/null +++ b/client/browser/src/contentScript/debug.ts @@ -0,0 +1,11 @@ +import { tap } from 'rxjs' + +/** + * Additional debug logging. + */ +export const DEBUG = true + +/** + * Like RxJS's {@link tap}, but only run if {@link DEBUG} is true. + */ +export const debugTap: typeof tap = DEBUG ? tap : () => source => source diff --git a/client/browser/src/contentScript/detectElements.ts b/client/browser/src/contentScript/detectElements.ts new file mode 100644 index 00000000..de9b4cd5 --- /dev/null +++ b/client/browser/src/contentScript/detectElements.ts @@ -0,0 +1,37 @@ +import { map, Observable } from 'rxjs' + +/** + * Return an Observable that emits the first DOM element that matches the given selector. If none is + * found, nothing is emitted. Polls briefly in case the page is still loading. + */ +export function withDOMElement(selectors: string): Observable { + return withDOMElements(selectors).pipe(map(els => els[0])) +} + +/** + * Return an Observable that emits all DOM elements matching the given selector. If none is + * found, nothing is emitted. Polls briefly in case the page is still loading. + */ +export function withDOMElements(selectors: string): Observable { + return new Observable(observer => { + const els = document.querySelectorAll(selectors) + if (els.length > 0) { + observer.next(Array.from(els)) + } else { + let calls = 0 + const MAX_CALLS = 10 + const intervalHandle = setInterval(() => { + const els = document.querySelectorAll(selectors) + if (els.length !== 0) { + observer.next(Array.from(els)) + } + + calls++ + if (els.length > 0 || calls >= MAX_CALLS) { + clearInterval(intervalHandle) + } + }, 250) + observer.add(() => clearInterval(intervalHandle)) + } + }) +} diff --git a/client/browser/src/contentScript/github/codeView.ts b/client/browser/src/contentScript/github/codeView.ts new file mode 100644 index 00000000..5907c5e1 --- /dev/null +++ b/client/browser/src/contentScript/github/codeView.ts @@ -0,0 +1,227 @@ +import { type Annotation, type AnnotationsParams, type OpenCodeGraphItem } from '@opencodegraph/client' +import { createItemChipList } from '@opencodegraph/ui-standalone' +import { combineLatest, debounceTime, EMPTY, map, mergeMap, Observable, startWith, tap } from 'rxjs' +import { toLineRangeStrings } from '../../shared/util/toLineRangeStrings' +import { DEBUG, debugTap } from '../debug' +import { withDOMElement } from '../detectElements' +import { annotationsByLine, LINE_CHIPS_CLASSNAME, styledItemChipListParams } from '../ocgUtil' + +/** + * Inject OpenCodeGraph features into the GitHub code view. + * + * Good URLs to test on: + * + * - Small file: https://github.com/sourcegraph/sourcegraph/blob/main/internal/repos/conf.go + * - Large file: https://github.com/sourcegraph/sourcegraph/blob/main/internal/repos/github.go#L1300 + */ +export function injectOnGitHubCodeView( + location: URL, + annotationsChanges: (params: AnnotationsParams) => Observable +): Observable { + // All GitHub code view URLs contain `/blob/` in the path. (But not all URLs with `/blob/` are code + // views, so we still need to check for the presence of DOM elements below. For example, there + // could be a repository named `myorg/blob`, which would have URLs containing `/blob/` on + // non-code view pages.) + if (!location.pathname.includes('/blob/')) { + return EMPTY + } + + return combineLatest([ + // If these don't emit, then the page is not recognized as a GitHub code view. This means + // it's a different GitHub page, or the DOM structure has changed significantly. + withDOMElement('#read-only-cursor-text-area'), + withDOMElement('react-app[app-name="react-code-view"]'), + ]).pipe( + mergeMap(([cursorTextArea, reactCodeView]) => { + interface GitHubCodeView { + cursorTextArea: HTMLTextAreaElement + reactCodeView: HTMLElement + } + const view: GitHubCodeView = { cursorTextArea, reactCodeView } + + const content = view.cursorTextArea.value + + const githubInitialPath = view.reactCodeView.getAttribute('initial-path') + if (!githubInitialPath) { + throw new Error('could not find initialPath') + } + const fileUri = `github://github.com/${githubInitialPath}` + + return combineLatest([ + annotationsChanges({ content, file: fileUri }), + significantCodeViewChanges.pipe( + debounceTime(200), + startWith(undefined), + debugTap(viewState => { + console.groupCollapsed('significantCodeViewChanges') + console.count('significantCodeViewChanges count') + console.log(viewState) + console.groupEnd() + }) + ), + ]).pipe( + tap(([annotations]) => { + if (DEBUG) { + console.count('redraw') + console.time('redraw') + } + redraw(annotations) + if (DEBUG) { + console.timeEnd('redraw') + } + }), + map(() => undefined) + ) + }) + ) +} + +function redraw(annotations: Annotation[]): void { + // TODO(sqs): optimize this by only redrawing changed chips + + const oldChips = document.querySelectorAll(`.${LINE_CHIPS_CLASSNAME}`) + for (const oldChip of Array.from(oldChips)) { + oldChip.remove() + } + + const byLine = annotationsByLine(annotations) + + // TODO(sqs): switch instead to looping over byLine so we only do work on lines that have + // annotations on them. + const codeRowEls = document.querySelectorAll('.react-code-line-contents') + for (const el of Array.from(codeRowEls)) { + const fileLineEl = el.querySelector('& > div > .react-file-line') + if (fileLineEl === null || !(fileLineEl instanceof HTMLElement)) { + throw new Error('unable to determine file line element') + } + const lineNumberStr = fileLineEl.dataset.lineNumber + if (!lineNumberStr) { + throw new Error('unable to determine line number') + } + const line = parseInt(lineNumberStr, 10) - 1 + + const lineAnns = byLine.find(a => a.line === line)?.annotations + if (lineAnns !== undefined) { + addChipsToCodeRow( + line, + lineAnns.map(ann => ann.item) + ) + } + } + + function addChipsToCodeRow(line: number, items: OpenCodeGraphItem[]): void { + const lineEl = document.querySelector(`.react-file-line[data-line-number="${line + 1}"]`) + if (lineEl) { + const chipList = createItemChipList( + styledItemChipListParams({ + items, + }) + ) + lineEl.append(chipList) + } + } +} + +interface GitHubCodeViewState { + renderedLineRanges: string[] + visibleLineRanges?: string[] +} + +/** + * An Observable that emits whenever the code view has a significant change to its view state (which + * means that anything rendered on top of it needs to be re-rendered). + */ +const significantCodeViewChanges: Observable = new Observable(observer => { + const intersectionCallback = (): void => { + // Since our scroll position changed, reanalyze the DOM to see which lines are the new + // boundaries and start observing those. + observeBoundaryLines() + + observer.next(getViewState()) + } + + const intersectionObserver = new IntersectionObserver(() => intersectionCallback(), { + root: null, // entire viewport + rootMargin: '40px', + }) + observer.add(() => intersectionObserver.disconnect()) + + function observeBoundaryLines(): void { + for (const line of getRenderedBoundaryLines()) { + intersectionObserver.observe(getReactFileLine(line)) + } + } + + // Set up initial observers. + observeBoundaryLines() + + return observer +}) + +function getViewState(): GitHubCodeViewState { + return { + renderedLineRanges: toLineRangeStrings(getRenderedLines()), + visibleLineRanges: DEBUG ? toLineRangeStrings(getVisibleLines()) : undefined, + } +} + +function getRenderedLines(): number[] { + const els = Array.from(document.querySelectorAll('.react-file-line')) + const lineNumbers = lineNumbersFromReactFileLines(els) + return lineNumbers +} + +function getVisibleLines(): number[] { + const els = Array.from(document.querySelectorAll('.react-file-line')) + const lineNumbers = lineNumbersFromReactFileLines(els.filter(isElementInViewport)) + return lineNumbers +} + +function isElementInViewport(el: HTMLElement): boolean { + const rect = el.getBoundingClientRect() + const viewportWidth = window.innerWidth || document.documentElement.clientWidth + const viewportHeight = window.innerHeight || document.documentElement.clientHeight + return !(rect.right < 0 || rect.bottom < 0 || rect.left > viewportWidth || rect.top > viewportHeight) +} + +function lineNumbersFromReactFileLines(reactFileLineEls: HTMLElement[]): number[] { + return reactFileLineEls + .map(el => (el.dataset.lineNumber ? parseInt(el.dataset.lineNumber, 10) - 1 : null)) + .filter((line): line is number => line !== null) +} + +function getReactFileLine(lineNumber: number): HTMLDivElement { + const el = document.querySelector(`.react-file-line[data-line-number="${lineNumber + 1}"]`) + if (!el) { + throw new Error(`no .react-file-line for line number ${lineNumber}`) + } + return el +} + +/** + * Get the line numbers of the first and last lines (for each sequentially rendered section) that + * are rendered. + */ +function getRenderedBoundaryLines(): number[] { + const renderedLines = getRenderedLines() + + const boundaryLines: number[] = [] + for (const [i, line] of renderedLines.entries()) { + if (i === 0 || line === renderedLines.length - 1) { + boundaryLines.push(line) + continue + } + + const lastLine = renderedLines[i - 1] + if (line - lastLine !== 1) { + boundaryLines.push(lastLine) + boundaryLines.push(line) + } + } + + return sortUnique(boundaryLines) +} + +function sortUnique(array: T[]): T[] { + return array.sort().filter((value, index, array) => value !== array[index - 1]) +} diff --git a/client/browser/src/contentScript/github/pullRequestFilesView.ts b/client/browser/src/contentScript/github/pullRequestFilesView.ts new file mode 100644 index 00000000..56cdd95a --- /dev/null +++ b/client/browser/src/contentScript/github/pullRequestFilesView.ts @@ -0,0 +1,212 @@ +import { type Annotation, type AnnotationsParams } from '@opencodegraph/client' +import { createItemChipList } from '@opencodegraph/ui-standalone' +import { combineLatest, EMPTY, filter, fromEvent, map, mergeMap, startWith, tap, type Observable } from 'rxjs' +import { DEBUG, debugTap } from '../debug' +import { withDOMElements } from '../detectElements' +import { annotationsByLine, LINE_CHIPS_CLASSNAME, styledItemChipListParams } from '../ocgUtil' + +/** + * Inject OpenCodeGraph features into the GitHub pull request files view. + * + * Good URLs to test on: + * + * - Small PR: https://github.com/sourcegraph/sourcegraph/pull/59084/files + * - Medium PR: https://github.com/sourcegraph/sourcegraph/pull/58878/files + * - Large PR: https://github.com/sourcegraph/sourcegraph/pull/58886/files + */ +export function injectOnGitHubPullRequestFilesView( + location: URL, + annotationsChanges: (params: AnnotationsParams) => Observable +): Observable { + // All GitHub PR file view URLs contain `/pull/` and `/files` in the path. + if (!location.pathname.includes('/pull/') && !location.pathname.endsWith('/files')) { + return EMPTY + } + + return combineLatest([ + // If these don't emit, then the page is not recognized as a GitHub PR files view. This means + // it's a different GitHub page, or the DOM structure has changed significantly. + withDOMElements('.diff-view .file'), + clicksThatInvalidateDiffViewData.pipe(startWith(undefined)), + ]).pipe( + mergeMap(([fileEls]) => { + const diffData = getDiffViewData(fileEls) + if (DEBUG) { + console.log('diffData', diffData) + } + return combineLatest( + diffData.files + .flatMap(file => [file.oldFile, file.newFile]) + .map(file => + annotationsChanges({ content: file.content, file: `github://${file.path}` }).pipe( + tap(annotations => { + try { + redraw(file, annotations) + } catch (error) { + console.error(error) + } + }), + map(() => undefined) + ) + ) + ) + }), + map(() => undefined) + ) +} + +function getItemChipListElementsAtEndOfLine(lineEl: HTMLElement): HTMLElement[] { + // There might be 2 of these in a unified (non-split) diff, since one was added by each of the old + // and new file's providers. + return [ + lineEl.childNodes.item(lineEl.childNodes.length - 2) as ChildNode | undefined, + lineEl.childNodes.item(lineEl.childNodes.length - 1) as ChildNode | undefined, + ].filter((el): el is HTMLElement => + Boolean(el instanceof HTMLElement && el.classList.contains(LINE_CHIPS_CLASSNAME)) + ) +} + +function redraw(file: DiffViewFileVersionData, annotations: Annotation[]): void { + // TODO(sqs): use line numbers as though they were in the original file, not just the displayed + // excerpt from the diff. + + const lineEls = file.tableEl.querySelectorAll(file.codeSelector) + for (const { line, annotations: lineAnnotations } of annotationsByLine(annotations)) { + const lineEl = lineEls[line] + if (!lineEl) { + console.error(`could not find lineEl for line ${line} (lineEls.length == ${lineEls.length})`) + continue + } + + for (const chipListEl of getItemChipListElementsAtEndOfLine(lineEl)) { + chipListEl.remove() + } + + const chipList = createItemChipList( + styledItemChipListParams({ + items: lineAnnotations.map(ann => ann.item), + }) + ) + lineEl.append(chipList) + } +} + +/** + * Listen for clicks on elements that, when clicked, invalidate this data. + */ +const clicksThatInvalidateDiffViewData: Observable = fromEvent(document.body, 'click').pipe( + filter(ev => { + let target = ev.target + if (target instanceof SVGSVGElement) { + target = target.parentElement + } + if (!(target instanceof HTMLElement)) { + return false + } + + return target.classList.contains('directional-expander') + }), + + // Wait for it to show up. Mark .blob-expanded elements that we've seen so that this works for + // multiple expansions. + mergeMap(() => + withDOMElements('tr.blob-expanded:not(.ocg-seen)').pipe( + tap(els => { + for (const el of els) { + el.classList.add('ocg-seen') + } + }) + ) + ), + + map(() => undefined), + debugTap(() => console.log('clicksThatInvalidateDiffViewData')) +) + +interface DiffViewData { + files: DiffViewFileData[] +} + +interface DiffViewFileData { + oldFile: DiffViewFileVersionData + newFile: DiffViewFileVersionData +} + +interface DiffViewFileVersionData { + path: string + content: string + tableEl: HTMLTableElement + codeSelector: string +} + +function getDiffViewData(fileEls: HTMLElement[]): DiffViewData { + return { + files: fileEls.map(getFileData), + } + + function getFileData(fileEl: HTMLElement): DiffViewFileData { + const tableEl = fileEl.querySelector('table.diff-table') + if (!tableEl) { + throw new Error('could not find table.diff-table') + } + + const oldFile: DiffViewFileVersionData = { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + path: fileEl.dataset.tagsearchPath!, // TODO(sqs): support renamed files + content: fileContentFromDiffViewSelector(tableEl, codeSelector(tableEl, 'old')), + tableEl, + codeSelector: codeSelector(tableEl, 'old'), + } + + const newFile: DiffViewFileVersionData = { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + path: fileEl.dataset.tagsearchPath!, // TODO(sqs): support renamed files + content: fileContentFromDiffViewSelector(tableEl, codeSelector(tableEl, 'new')), + tableEl, + codeSelector: codeSelector(tableEl, 'new'), + } + + return { oldFile, newFile } + } +} + +function codeSelector(tableEl: HTMLTableElement, version: 'old' | 'new'): string { + const isSplitDiff = tableEl.classList.contains('file-diff-split') + if (isSplitDiff) { + return version === 'old' + ? 'td[data-split-side="left"] .blob-code-inner, td[data-split-side="left"].blob-code-inner' + : 'td[data-split-side="right"] .blob-code-inner, td[data-split-side="right"].blob-code-inner' + } + + // Omit .blob-expanded from the new version of the code to avoid double annotations. + return version === 'old' + ? ':where(.blob-code-context, .blob-code-deletion, .blob-expanded) .blob-code-inner' + : ':where(.blob-code-context, .blob-code-addition) .blob-code-inner' +} + +function fileContentFromDiffViewSelector(fileDiffTableEl: HTMLTableElement, selector: string): string { + const els = Array.from(fileDiffTableEl.querySelectorAll(selector)) + + return els + .map(el => { + // Ignore innerText from the OCG chip. + const chipListEls = getItemChipListElementsAtEndOfLine(el) + for (const chipListEl of chipListEls) { + chipListEl.hidden = true + } + + const innerText = el.innerText + + for (const chipListEl of chipListEls) { + chipListEl.hidden = false + } + + // If the innerText is just `\n`, then treat it as empty so we don't have single empty lines ending up as `\n\n`. + if (innerText === '\n' || innerText === '\r\n') { + return '' + } + + return innerText + }) + .join('\n') +} diff --git a/client/browser/src/contentScript/locationChanges.ts b/client/browser/src/contentScript/locationChanges.ts new file mode 100644 index 00000000..5c24e083 --- /dev/null +++ b/client/browser/src/contentScript/locationChanges.ts @@ -0,0 +1,35 @@ +import { distinctUntilChanged, Observable } from 'rxjs' +import { debugTap } from './debug' + +/** + * An Observable that emits when the page's URL changes. + */ +export const locationChanges: Observable = new Observable(observer => { + const emitCurrentLocation = (): void => { + observer.next(new URL(window.location.href)) + } + + const onLinkClick = (ev: MouseEvent): void => { + if (!(ev.target instanceof HTMLAnchorElement && ev.target.href)) { + return + } + setTimeout(emitCurrentLocation) + } + + // Listen for clicks on elements, which likely cause an immediate location change. + document.addEventListener('click', onLinkClick) + observer.add(() => document.removeEventListener('click', onLinkClick)) + + // Listen for popstate events, which also indicate an immediate location change. + window.addEventListener('popstate', emitCurrentLocation) + observer.add(() => window.removeEventListener('popstate', emitCurrentLocation)) + + // Poll to detect any other location changes. + const intervalHandle = setInterval(emitCurrentLocation, 5000) + observer.add(() => clearInterval(intervalHandle)) + + emitCurrentLocation() +}).pipe( + distinctUntilChanged((a, b) => a.toString() === b.toString()), + debugTap(url => console.log('locationChanges', url.toString())) +) diff --git a/client/browser/src/contentScript/ocgUtil.ts b/client/browser/src/contentScript/ocgUtil.ts new file mode 100644 index 00000000..737c68c2 --- /dev/null +++ b/client/browser/src/contentScript/ocgUtil.ts @@ -0,0 +1,28 @@ +import { type Annotation } from '@opencodegraph/client' +import { type createItemChipList } from '@opencodegraph/ui-standalone' + +export function annotationsByLine(annotations: Annotation[]): { line: number; annotations: Annotation[] }[] { + const byLine: { line: number; annotations: Annotation[] }[] = [] + for (const ann of annotations) { + let cur = byLine.at(-1) + if (!cur || cur.line !== ann.range.start.line) { + cur = { line: ann.range.start.line, annotations: [] } + byLine.push(cur) + } + cur.annotations.push(ann) + } + return byLine +} + +export const LINE_CHIPS_CLASSNAME = 'ocg-line-chips' + +export function styledItemChipListParams( + params: Omit[0], 'className' | 'chipClassName' | 'popoverClassName'> +): Parameters[0] { + return { + ...params, + className: LINE_CHIPS_CLASSNAME, + chipClassName: 'ocg-chip', + popoverClassName: 'ocg-chip-popover', + } +} diff --git a/client/browser/src/globals.d.ts b/client/browser/src/globals.d.ts new file mode 100644 index 00000000..57c46c7c --- /dev/null +++ b/client/browser/src/globals.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: { readonly [key: string]: string } + export default classes +} diff --git a/client/browser/src/options/OptionsPage.module.css b/client/browser/src/options/OptionsPage.module.css new file mode 100644 index 00000000..354eb703 --- /dev/null +++ b/client/browser/src/options/OptionsPage.module.css @@ -0,0 +1,67 @@ +body { + margin: 0; + min-height: 350px; + background-color: Canvas; + color: CanvasText; +} + +* { + box-sizing: border-box; +} + +:global(#root) { + height: 100vh; + width: 100vw; + min-width: 600px; +} + +.container { + height: 100%; + padding: 0.5rem; + + display: flex; + gap: 0.5rem; + flex-direction: column; +} + +.heading { + flex: 0; + margin: 0; + + display: flex; + align-items: center; + justify-content: space-between; + + font-size: unset; +} + +.title { + font-size: 100%; + font-weight: 600; +} + +.docs-link { + font-size: 100%; + font-weight: normal; + text-underline-offset: 2px; +} + +.editor { + display: block; + flex: 1 1 auto; + padding: 0.25rem; + border: none; + white-space: pre-wrap; + overflow: auto; + resize: none; + border: solid 1px ButtonBorder; + font-size: 120%; +} + +.submit { + display: block; + flex: 0 0 auto; + align-self: flex-end; + padding: 0.25rem 0.75rem; + font-size: 100%; +} diff --git a/client/browser/src/options/OptionsPage.tsx b/client/browser/src/options/OptionsPage.tsx new file mode 100644 index 00000000..bcbade8b --- /dev/null +++ b/client/browser/src/options/OptionsPage.tsx @@ -0,0 +1,101 @@ +import clsx from 'clsx' +import { useObservableState } from 'observable-hooks' +import { + useCallback, + useRef, + useState, + type ChangeEventHandler, + type FormEventHandler, + type FunctionComponent, +} from 'react' +import { storage } from '../browser-extension/web-extension-api/storage' +import { configurationStringChanges } from '../configuration' +import styles from './OptionsPage.module.css' + +export const OptionsPage: FunctionComponent = () => { + const configuration = useObservableState(configurationStringChanges) + + const [pendingConfig, setPendingConfig] = useState(configuration) + + const isLoading = configuration === undefined + const [isSaving, setIsSaving] = useState(false) + + const TEXTAREA_ID = 'config-editor' + const textareaRef = useRef(null) + + const onChange = useCallback>(event => { + setPendingConfig(event.currentTarget.value) + }, []) + const onSubmit = useCallback( + event => { + event.preventDefault() + setIsSaving(true) + if (pendingConfig !== undefined) { + storage.sync + .set({ configuration: { jsonc: pendingConfig ?? configuration } }) + .catch(error => { + console.error(error) + alert('Failed to save OpenCodeGraph configuration.') + }) + .finally(() => { + setIsSaving(false) + textareaRef.current?.focus() + }) + } + }, + [configuration, pendingConfig] + ) + + const isDirty = pendingConfig !== undefined && pendingConfig !== configuration + const formDisabled = isLoading || isSaving + + const onKeyDown = useCallback( + event => { + // Ctrl+S saves the configuration. + if ( + !formDisabled && + isDirty && + event.key === 's' && + (event.ctrlKey || event.metaKey) && + !(event.ctrlKey && event.metaKey) && + !event.altKey && + !event.shiftKey + ) { + event.preventDefault() + onSubmit(event) + } + }, + [formDisabled, isDirty, onSubmit] + ) + + return ( +
    +

    + + + Docs + +

    +