From c9439d94569fd9906dc1efe688ee2181c14f56f0 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Wed, 6 Dec 2023 21:39:33 -0800 Subject: [PATCH] OpenCodeGraph prototype (#58675) This adds support for the OpenCodeGraph prototype. Feature-flagged off by default behind the `opencodegraph` feature flag. See https://www.loom.com/share/5549d92a7c244863ac86ce56692ca030 for more information. Also, for our CodeMirror, remove `background:transparent` so that line bg applies to block widgets --- client/shared/BUILD.bazel | 5 + client/shared/package.json | 2 +- .../settings/temporary/TemporarySettings.ts | 4 + client/web/BUILD.bazel | 4 + client/web/dev/esbuild/config.ts | 7 + client/web/package.json | 2 +- client/web/src/featureFlags/featureFlags.ts | 1 + .../global/ToggleOpenCodeGraphVisibility.tsx | 62 ++++++ .../global/useOpenCodeGraphVisibility.ts | 5 + client/web/src/repo/blob/BlobPage.tsx | 23 ++ client/web/src/repo/blob/CodeMirrorBlob.tsx | 83 ++++++- .../src/repo/blob/codemirror/linenumbers.ts | 6 +- cmd/frontend/internal/httpapi/BUILD.bazel | 3 + cmd/frontend/internal/httpapi/httpapi.go | 2 + .../internal/httpapi/opencodegraph.go | 80 +++++++ internal/opencodegraph/BUILD.bazel | 35 +++ internal/opencodegraph/amplitude_provider.go | 57 +++++ internal/opencodegraph/chromatic_util.go | 51 +++++ internal/opencodegraph/doc.go | 2 + internal/opencodegraph/docs_provider.go | 104 +++++++++ internal/opencodegraph/googledocs_provider.go | 62 ++++++ internal/opencodegraph/grafana_provider.go | 54 +++++ internal/opencodegraph/multi.go | 76 +++++++ internal/opencodegraph/protocol.go | 27 +++ internal/opencodegraph/protocol_test.go | 51 +++++ internal/opencodegraph/providers.go | 21 ++ internal/opencodegraph/storybook_provider.go | 210 ++++++++++++++++++ internal/opencodegraph/util.go | 31 +++ package.json | 2 + pnpm-lock.yaml | 147 +++++++++++- schema/BUILD.bazel | 16 ++ schema/opencodegraph-protocol.schema.json | 122 ++++++++++ schema/opencodegraph.schema.json | 82 +++++++ schema/schema.go | 85 +++++++ schema/stringdata.go | 10 + 35 files changed, 1509 insertions(+), 25 deletions(-) create mode 100644 client/web/src/opencodegraph/global/ToggleOpenCodeGraphVisibility.tsx create mode 100644 client/web/src/opencodegraph/global/useOpenCodeGraphVisibility.ts create mode 100644 cmd/frontend/internal/httpapi/opencodegraph.go create mode 100644 internal/opencodegraph/BUILD.bazel create mode 100644 internal/opencodegraph/amplitude_provider.go create mode 100644 internal/opencodegraph/chromatic_util.go create mode 100644 internal/opencodegraph/doc.go create mode 100644 internal/opencodegraph/docs_provider.go create mode 100644 internal/opencodegraph/googledocs_provider.go create mode 100644 internal/opencodegraph/grafana_provider.go create mode 100644 internal/opencodegraph/multi.go create mode 100644 internal/opencodegraph/protocol.go create mode 100644 internal/opencodegraph/protocol_test.go create mode 100644 internal/opencodegraph/providers.go create mode 100644 internal/opencodegraph/storybook_provider.go create mode 100644 internal/opencodegraph/util.go create mode 100644 schema/opencodegraph-protocol.schema.json create mode 100644 schema/opencodegraph.schema.json diff --git a/client/shared/BUILD.bazel b/client/shared/BUILD.bazel index 3676c9e7e3c..95be0631db7 100644 --- a/client/shared/BUILD.bazel +++ b/client/shared/BUILD.bazel @@ -120,6 +120,11 @@ generate_schema( out = "src/schema/batch_spec.schema.d.ts", ) +generate_schema( + name = "opencodegraph", + out = "src/schema/opencodegraph.schema.d.ts", +) + ts_project( name = "shared_lib", srcs = [ diff --git a/client/shared/package.json b/client/shared/package.json index 8ba93e714f8..ce0d862f56e 100644 --- a/client/shared/package.json +++ b/client/shared/package.json @@ -10,7 +10,7 @@ "test": "vitest", "generate": "concurrently -r npm:generate:*", "generate:graphql-operations": "ts-node -T dev/generateGraphQlOperations.ts", - "generate:schema": "ts-node -T dev/generateSchema.ts json-schema-draft-07 settings site batch_spec", + "generate:schema": "ts-node -T dev/generateSchema.ts json-schema-draft-07 settings site batch_spec opencodegraph", "generate:css-modules-types": "ts-node -T dev/generateCssModulesTypes.ts" }, "devDependencies": { diff --git a/client/shared/src/settings/temporary/TemporarySettings.ts b/client/shared/src/settings/temporary/TemporarySettings.ts index 2af60143a57..983e8c52f47 100644 --- a/client/shared/src/settings/temporary/TemporarySettings.ts +++ b/client/shared/src/settings/temporary/TemporarySettings.ts @@ -87,6 +87,9 @@ export interface TemporarySettingsSchema { 'simple.search.toggle': boolean 'cody.onboarding.completed': boolean 'cody.onboarding.step': number + + /** OpenCodeGraph */ + 'openCodeGraph.annotations.visible': boolean } /** @@ -150,6 +153,7 @@ const TEMPORARY_SETTINGS: Record = { 'simple.search.toggle': null, 'cody.onboarding.completed': null, 'cody.onboarding.step': null, + 'openCodeGraph.annotations.visible': null, } export const TEMPORARY_SETTINGS_KEYS = Object.keys(TEMPORARY_SETTINGS) as readonly (keyof TemporarySettings)[] diff --git a/client/web/BUILD.bazel b/client/web/BUILD.bazel index 27186237918..9b755658700 100644 --- a/client/web/BUILD.bazel +++ b/client/web/BUILD.bazel @@ -1206,6 +1206,8 @@ ts_project( "src/open-in-editor/editors.ts", "src/open-in-editor/migrate-legacy-settings.ts", "src/open-in-editor/useOpenCurrentUrlInEditor.ts", + "src/opencodegraph/global/ToggleOpenCodeGraphVisibility.tsx", + "src/opencodegraph/global/useOpenCodeGraphVisibility.ts", "src/org/OrgAvatar.tsx", "src/org/OrgsArea.tsx", "src/org/area/OrgArea.tsx", @@ -1771,6 +1773,8 @@ ts_project( "//:node_modules/@lezer/highlight", "//:node_modules/@mdi/js", "//:node_modules/@microsoft/fetch-event-source", + "//:node_modules/@opencodegraph/client", + "//:node_modules/@opencodegraph/codemirror-extension", "//:node_modules/@opentelemetry/context-zone", "//:node_modules/@opentelemetry/exporter-trace-otlp-http", "//:node_modules/@opentelemetry/instrumentation", diff --git a/client/web/dev/esbuild/config.ts b/client/web/dev/esbuild/config.ts index 9b6fd111409..2968eeb6dfe 100644 --- a/client/web/dev/esbuild/config.ts +++ b/client/web/dev/esbuild/config.ts @@ -62,6 +62,13 @@ export function esbuildBuildOptions(ENVIRONMENT_CONFIG: EnvironmentConfig): esbu // Misc. recharts: '/dev/null', + + // TODO(sqs): force use of same version when developing on opencodegraph because `pnpm link` breaks + '@codemirror/state': path.join(ROOT_PATH, 'node_modules/@codemirror/state'), + '@codemirror/view': path.join(ROOT_PATH, 'node_modules/@codemirror/view'), + react: path.join(ROOT_PATH, 'node_modules/react'), + 'react-dom': path.join(ROOT_PATH, 'node_modules/react-dom'), + 'react-dom/client': path.join(ROOT_PATH, 'node_modules/react-dom/client'), } : null), }), diff --git a/client/web/package.json b/client/web/package.json index 8e099129230..93a3e8dbb73 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -48,4 +48,4 @@ "@sourcegraph/telemetry": "^0.11.0", "@sourcegraph/wildcard": "workspace:*" } -} \ No newline at end of file +} diff --git a/client/web/src/featureFlags/featureFlags.ts b/client/web/src/featureFlags/featureFlags.ts index 025fc32c462..a3e86df85bc 100644 --- a/client/web/src/featureFlags/featureFlags.ts +++ b/client/web/src/featureFlags/featureFlags.ts @@ -34,6 +34,7 @@ export const FEATURE_FLAGS = [ 'cody-chat-mock-test', 'signup-survey-enabled', 'cody-pro', + 'opencodegraph', ] as const export type FeatureFlagName = typeof FEATURE_FLAGS[number] diff --git a/client/web/src/opencodegraph/global/ToggleOpenCodeGraphVisibility.tsx b/client/web/src/opencodegraph/global/ToggleOpenCodeGraphVisibility.tsx new file mode 100644 index 00000000000..e5da6372e16 --- /dev/null +++ b/client/web/src/opencodegraph/global/ToggleOpenCodeGraphVisibility.tsx @@ -0,0 +1,62 @@ +import { useCallback } from 'react' + +import { mdiWeb, mdiWebOff } from '@mdi/js' + +import { SimpleActionItem } from '@sourcegraph/shared/src/actions/SimpleActionItem' +import type { RenderMode } from '@sourcegraph/shared/src/util/url' +import { Button, Icon, Tooltip } from '@sourcegraph/wildcard' + +import { RepoHeaderActionAnchor, RepoHeaderActionMenuLink } from '../../repo/components/RepoHeaderActions' + +import { useOpenCodeGraphVisibility } from './useOpenCodeGraphVisibility' + +interface Props { + source?: 'repoHeader' | 'actionItemsBar' + actionType?: 'nav' | 'dropdown' + renderMode?: RenderMode +} + +export const ToggleOpenCodeGraphVisibilityAction: React.FC = props => { + const [visible, setVisible] = useOpenCodeGraphVisibility() + + const disabled = props.renderMode === 'rendered' + const descriptiveText = disabled + ? 'OpenCodeGraph metadata is not available in rendered files' + : `${visible ? 'Hide' : 'Show'} OpenCodeGraph metadata` + + const onCycle = useCallback(() => { + setVisible(prevVisible => !prevVisible) + // TODO(sqs): telemetry + }, [setVisible]) + + const icon = disabled || !visible ? mdiWebOff : mdiWeb + + if (props.source === 'actionItemsBar') { + return ( + + + + ) + } + + if (props.actionType === 'dropdown') { + return ( + + + {descriptiveText} + + ) + } + + return ( + + + + + + ) +} diff --git a/client/web/src/opencodegraph/global/useOpenCodeGraphVisibility.ts b/client/web/src/opencodegraph/global/useOpenCodeGraphVisibility.ts new file mode 100644 index 00000000000..d9a1b999de8 --- /dev/null +++ b/client/web/src/opencodegraph/global/useOpenCodeGraphVisibility.ts @@ -0,0 +1,5 @@ +import { type UseTemporarySettingsReturnType, useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary' + +export function useOpenCodeGraphVisibility(): UseTemporarySettingsReturnType<'openCodeGraph.annotations.visible'> { + return useTemporarySetting('openCodeGraph.annotations.visible') +} diff --git a/client/web/src/repo/blob/BlobPage.tsx b/client/web/src/repo/blob/BlobPage.tsx index b806457b750..219bef6cc02 100644 --- a/client/web/src/repo/blob/BlobPage.tsx +++ b/client/web/src/repo/blob/BlobPage.tsx @@ -57,6 +57,8 @@ import type { SourcegraphContext } from '../../jscontext' import type { NotebookProps } from '../../notebooks' import { copyNotebook, type CopyNotebookProps } from '../../notebooks/notebook' import { OpenInEditorActionItem } from '../../open-in-editor/OpenInEditorActionItem' +import { ToggleOpenCodeGraphVisibilityAction } from '../../opencodegraph/global/ToggleOpenCodeGraphVisibility' +import { useOpenCodeGraphVisibility } from '../../opencodegraph/global/useOpenCodeGraphVisibility' import type { OwnConfigProps } from '../../own/OwnConfigProps' import type { SearchStreamingProps } from '../../search' import { parseBrowserRepoURL, toTreeURL } from '../../util/url' @@ -302,6 +304,10 @@ export const BlobPage: React.FunctionComponent = ({ className, co const [isBlameVisible] = useBlameVisibility(isPackage) const blameHunks = useBlameHunks({ isPackage, repoName, revision, filePath }, props.platformContext.sourcegraphURL) + // OpenCodeGraph + const [enableOpenCodeGraph] = useFeatureFlag('opencodegraph', false) + const [ocgVisibility] = useOpenCodeGraphVisibility() + const isSearchNotebook = Boolean( blobInfoOrError && !isErrorLike(blobInfoOrError) && @@ -386,6 +392,22 @@ export const BlobPage: React.FunctionComponent = ({ className, co /> )} + {enableOpenCodeGraph && ( + + {({ actionType }) => ( + + )} + + )} = ({ className, co ariaLabel="File blob" isBlameVisible={isBlameVisible} blameHunks={blameHunks} + ocgVisibility={ocgVisibility} overrideBrowserSearchKeybinding={true} /> diff --git a/client/web/src/repo/blob/CodeMirrorBlob.tsx b/client/web/src/repo/blob/CodeMirrorBlob.tsx index 7efce76288b..fd1cd2b14e9 100644 --- a/client/web/src/repo/blob/CodeMirrorBlob.tsx +++ b/client/web/src/repo/blob/CodeMirrorBlob.tsx @@ -2,14 +2,16 @@ * An implementation of the Blob view using CodeMirror */ -import { type MutableRefObject, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject, type RefObject } from 'react' import { openSearchPanel } from '@codemirror/search' import { EditorState, type Extension } from '@codemirror/state' import { EditorView } from '@codemirror/view' +import { createClient, type Annotation } from '@opencodegraph/client' +import { useOpenCodeGraphExtension } from '@opencodegraph/codemirror-extension' import { isEqual } from 'lodash' import { createRoot } from 'react-dom/client' -import { createPath, type NavigateFunction, useLocation, useNavigate, type Location } from 'react-router-dom' +import { createPath, useLocation, useNavigate, type Location, type NavigateFunction } from 'react-router-dom' import { NoopEditor } from '@sourcegraph/cody-shared/dist/editor' import { @@ -24,13 +26,15 @@ import { useKeyboardShortcut } from '@sourcegraph/shared/src/keyboardShortcuts/u import type { PlatformContext, PlatformContextProps } from '@sourcegraph/shared/src/platform/context' import { Shortcut } from '@sourcegraph/shared/src/react-shortcuts' import type { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings' +import type { TemporarySettingsSchema } from '@sourcegraph/shared/src/settings/temporary/TemporarySettings' import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' +import { Theme, useTheme } from '@sourcegraph/shared/src/theme' import { - type AbsoluteRepoFile, - type ModeSpec, parseQueryAndHash, toPrettyBlobURL, + type AbsoluteRepoFile, type BlobViewState, + type ModeSpec, } from '@sourcegraph/shared/src/util/url' import { useLocalStorage } from '@sourcegraph/wildcard' @@ -51,12 +55,12 @@ import { pinnedLocation } from './codemirror/codeintel/pin' import { syncSelection } from './codemirror/codeintel/token-selection' import { hideEmptyLastLine } from './codemirror/eof' import { syntaxHighlight } from './codemirror/highlight' -import { selectableLineNumbers, type SelectedLineRange, selectLines } from './codemirror/linenumbers' +import { selectableLineNumbers, selectLines, type SelectedLineRange } from './codemirror/linenumbers' import { linkify } from './codemirror/links' import { lockFirstVisibleLine } from './codemirror/lock-line' import { navigateToLineOnAnyClickExtension } from './codemirror/navigate-to-any-line-on-click' import { scipSnapshot } from './codemirror/scip-snapshot' -import { search, SearchPanelConfig } from './codemirror/search' +import { search, type SearchPanelConfig } from './codemirror/search' import { sourcegraphExtensions } from './codemirror/sourcegraph-extensions' import { codyWidgetExtension } from './codemirror/tooltips/CodyTooltip' import { HovercardView } from './codemirror/tooltips/HovercardView' @@ -111,6 +115,8 @@ export interface BlobProps isBlameVisible?: boolean blameHunks?: BlameHunkData + ocgVisibility?: TemporarySettingsSchema['openCodeGraph.annotations.visible'] + activeURL?: string searchPanelConfig?: SearchPanelConfig } @@ -203,6 +209,7 @@ export const CodeMirrorBlob: React.FunctionComponent = props => { extensionsController, isBlameVisible, blameHunks, + ocgVisibility, // Reference panel specific props navigateToLineOnAnyClick, @@ -325,6 +332,15 @@ export const CodeMirrorBlob: React.FunctionComponent = props => { editorRef, useMemo(() => pinnedLocation.of(hasPin ? position : null), [hasPin, position]) ) + + const openCodeGraphExtension = useOpenCodeGraphExtensionWithHardcodedConfig( + blobInfo.filePath, + blobInfo.content, + Boolean(ocgVisibility) + ) + + const { theme } = useTheme() + const extensions = useMemo( () => [ staticExtensions, @@ -334,6 +350,7 @@ export const CodeMirrorBlob: React.FunctionComponent = props => { onLineClick: navigateOnClick, }), scipSnapshot(blobInfo.content, blobInfo.snapshotData), + openCodeGraphExtension, codeFoldingExtension(), isCodyEnabled() ? codyWidgetExtension( @@ -370,6 +387,7 @@ export const CodeMirrorBlob: React.FunctionComponent = props => { initialState: searchPanelConfig, navigate, }), + EditorView.theme({}, { dark: theme === Theme.Dark }), ], // A couple of values are not dependencies (hasPin and position) because those are updated in effects // further below. However, they are still needed here because we need to @@ -381,6 +399,7 @@ export const CodeMirrorBlob: React.FunctionComponent = props => { blobInfo, extensionsController, isCodyEnabled, + openCodeGraphExtension, codeIntelExtension, editorRef.current, blameDecorations, @@ -420,7 +439,7 @@ export const CodeMirrorBlob: React.FunctionComponent = props => { // when using macOS VoiceOver. editor.contentDOM.focus({ preventScroll: true }) } - }, [blobInfo, extensions, navigateToLineOnAnyClick, positionRef]) + }, [blobInfo.content, extensions, navigateToLineOnAnyClick, positionRef]) // Update selected lines when URL changes useEffect(() => { @@ -725,6 +744,56 @@ function useBlameDecoration( return useMemo(() => [enabled, data], [enabled, data]) } +function useOpenCodeGraphExtensionWithHardcodedConfig( + filePath: string, + content: string, + visibility: boolean +): Extension { + const client = useMemo( + () => + createClient({ + configuration: () => + Promise.resolve({ + enable: true, + providers: { [`${window.location.origin}/.api/opencodegraph`]: true }, + }), + authInfo: async () => Promise.resolve(null), + makeRange: r => r, + }), + [] + ) + + const [annotations, setAnnotations] = useState() + useEffect(() => { + setAnnotations(undefined) + if (!content || !visibility) { + return + } + const subscription = client.annotations({ file: `sourcegraph:///${filePath}`, content }).subscribe({ + next: setAnnotations, + error: (error: any) => { + // eslint-disable-next-line no-console + console.error('Error getting OpenCodeGraph annotations:', error) + }, + }) + return () => subscription.unsubscribe() + }, [content, visibility, filePath, client]) + + const openCodeGraphExtension = useOpenCodeGraphExtension({ visibility, annotations }) + + const theme = useMemo( + () => + EditorView.baseTheme({ + '.ocg-chip': { + fontSize: '94% !important', + }, + }), + [] + ) + + return useMemo(() => [openCodeGraphExtension, theme], [openCodeGraphExtension, theme]) +} + /** * Returns true when the URL indicates that the hovercard at the URL position * should be shown on load (the hovercard is "pinned"). diff --git a/client/web/src/repo/blob/codemirror/linenumbers.ts b/client/web/src/repo/blob/codemirror/linenumbers.ts index ad8ef420bf1..6da4be18b13 100644 --- a/client/web/src/repo/blob/codemirror/linenumbers.ts +++ b/client/web/src/repo/blob/codemirror/linenumbers.ts @@ -54,11 +54,6 @@ const selectedLinesTheme = EditorView.theme({ minHeight: 'calc(1.5rem + 1px)', }, - // Selected line background is set by adding 'selected-line' class to the layer markers. - '.cm-line.selected-line': { - background: 'transparent', - }, - /** * Rectangle markers `left` position matches the position of the character at the start of range * (for selected lines it is first character of the first line in a range). When line content (`.cm-line`) @@ -270,6 +265,7 @@ const selectedLineNumberTheme = EditorView.theme({ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', + justifyContent: 'flex-end', }, '& .cm-gutterElement:hover': { diff --git a/cmd/frontend/internal/httpapi/BUILD.bazel b/cmd/frontend/internal/httpapi/BUILD.bazel index a48c2bf551e..218c1cd6687 100644 --- a/cmd/frontend/internal/httpapi/BUILD.bazel +++ b/cmd/frontend/internal/httpapi/BUILD.bazel @@ -11,6 +11,7 @@ go_library( "httpapi.go", "internal.go", "metrics.go", + "opencodegraph.go", "repo_shield.go", "search.go", "src_cli.go", @@ -44,10 +45,12 @@ go_library( "//internal/encryption/keyring", "//internal/env", "//internal/errcode", + "//internal/featureflag", "//internal/gitserver", "//internal/gitserver/gitdomain", "//internal/httpcli", "//internal/licensing", + "//internal/opencodegraph", "//internal/search", "//internal/search/backend", "//internal/search/searchcontexts", diff --git a/cmd/frontend/internal/httpapi/httpapi.go b/cmd/frontend/internal/httpapi/httpapi.go index fec13e1995b..bad5d36b727 100644 --- a/cmd/frontend/internal/httpapi/httpapi.go +++ b/cmd/frontend/internal/httpapi/httpapi.go @@ -113,6 +113,8 @@ func NewHandler( m.PathPrefix("/scim/v2").Methods("GET", "POST", "PUT", "PATCH", "DELETE").Handler(trace.Route(handlers.SCIMHandler)) m.Path("/graphql").Methods("POST").Handler(trace.Route(jsonHandler(serveGraphQL(logger, schema, rateLimiter, false)))) + m.Path("/opencodegraph").Methods("POST").Handler(trace.Route(jsonHandler(serveOpenCodeGraph(logger)))) + // Webhooks // // First: register handlers diff --git a/cmd/frontend/internal/httpapi/opencodegraph.go b/cmd/frontend/internal/httpapi/opencodegraph.go new file mode 100644 index 00000000000..b232e3d02da --- /dev/null +++ b/cmd/frontend/internal/httpapi/opencodegraph.go @@ -0,0 +1,80 @@ +package httpapi + +import ( + "compress/gzip" + "encoding/json" + "net/http" + + "github.com/sourcegraph/log" + "github.com/sourcegraph/sourcegraph/cmd/frontend/internal/search" + "github.com/sourcegraph/sourcegraph/internal/featureflag" + "github.com/sourcegraph/sourcegraph/internal/opencodegraph" + "github.com/sourcegraph/sourcegraph/internal/trace" + "github.com/sourcegraph/sourcegraph/lib/errors" + "github.com/sourcegraph/sourcegraph/schema" +) + +// Update OpenCodeGraph JSON Schemas (assuming ../opencodegraph relative to this repository's root +// is the https://github.com/sourcegraph/opencodegraph repository): +// +// cp ../opencodegraph/lib/schema/src/opencodegraph.schema.json schema/ +// cp ../opencodegraph/lib/protocol/src/opencodegraph-protocol.schema.json schema/ +// sed -i 's#../../schema/src/##g' schema/opencodegraph-protocol.schema.json +// bazel run //schema:write_generated_schema + +func serveOpenCodeGraph(logger log.Logger) func(w http.ResponseWriter, r *http.Request) (err error) { + return func(w http.ResponseWriter, r *http.Request) (err error) { + flagSet := featureflag.FromContext(r.Context()) + if !flagSet.GetBoolOr("opencodegraph", false) { + return errors.New("OpenCodeGraph is not enabled (use the 'opencodegraph' feature flag)") + } + + if r.Method != "POST" { + // The URL router should not have routed to this handler if method is not POST, but just + // in case. + return errors.New("method must be POST") + } + + requestSource := search.GuessSource(r) + r = r.WithContext(trace.WithRequestSource(r.Context(), requestSource)) + + if r.Header.Get("Content-Encoding") == "gzip" { + gzipReader, err := gzip.NewReader(r.Body) + if err != nil { + return errors.Wrap(err, "failed to decompress request body") + } + r.Body = gzipReader + defer gzipReader.Close() + } + + method, cap, ann, err := opencodegraph.DecodeRequestMessage(json.NewDecoder(r.Body)) + if err != nil { + return errors.Wrapf(err, "failed to decode request message") + } + + var result any + switch { + case cap != nil: + result, err = opencodegraph.AllProviders.Capabilities(r.Context(), *cap) + case ann != nil: + result, err = opencodegraph.AllProviders.Annotations(r.Context(), *ann) + default: + return errors.Newf("unrecognized OpenCodeGraph request method %q", method) + } + + var respMsg schema.ResponseMessage + if err == nil { + respMsg.Result = result + } else { + respMsg.Error = &schema.ResponseError{ + Code: 1, + Message: err.Error(), + } + logger.Error("error handling OpenCodeGraph method", log.Error(err)) + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(respMsg) + return nil + } +} diff --git a/internal/opencodegraph/BUILD.bazel b/internal/opencodegraph/BUILD.bazel new file mode 100644 index 00000000000..5970e53e5b2 --- /dev/null +++ b/internal/opencodegraph/BUILD.bazel @@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//dev:go_defs.bzl", "go_test") + +go_library( + name = "opencodegraph", + srcs = [ + "amplitude_provider.go", + "chromatic_util.go", + "doc.go", + "docs_provider.go", + "googledocs_provider.go", + "grafana_provider.go", + "multi.go", + "protocol.go", + "providers.go", + "storybook_provider.go", + "util.go", + ], + importpath = "github.com/sourcegraph/sourcegraph/internal/opencodegraph", + visibility = ["//:__subpackages__"], + deps = [ + "//lib/errors", + "//schema", + "@com_github_grafana_regexp//:regexp", + "@org_golang_x_net//context/ctxhttp", + "@org_golang_x_sync//errgroup", + ], +) + +go_test( + name = "opencodegraph_test", + srcs = ["protocol_test.go"], + embed = [":opencodegraph"], + deps = ["//schema"], +) diff --git a/internal/opencodegraph/amplitude_provider.go b/internal/opencodegraph/amplitude_provider.go new file mode 100644 index 00000000000..c6f6fcfcca6 --- /dev/null +++ b/internal/opencodegraph/amplitude_provider.go @@ -0,0 +1,57 @@ +package opencodegraph + +import ( + "context" + "fmt" + + "github.com/grafana/regexp" + "github.com/sourcegraph/sourcegraph/schema" +) + +func init() { + RegisterProvider(amplitudeProvider{}) +} + +type amplitudeProvider struct{} + +func (amplitudeProvider) Name() string { return "amplitude" } + +func (amplitudeProvider) Capabilities(ctx context.Context, params schema.CapabilitiesParams) (*schema.CapabilitiesResult, error) { + return &schema.CapabilitiesResult{ + Selector: []*schema.Selector{ + {Path: "**/*.ts?(x)", ContentContains: "eventLogger.log"}, + }, + }, nil +} + +func (amplitudeProvider) Annotations(ctx context.Context, params schema.AnnotationsParams) (*schema.AnnotationsResult, error) { + var result schema.AnnotationsResult + + events, ranges := telemetryCalls(params.Content) + for i, ev := range events { + id := fmt.Sprintf("%s:%d", ev, i) + item := &schema.OpenCodeGraphItem{ + Id: id, + Title: "📊 Analytics: " + ev, + Url: "https://example.com/#not-yet-implemented", + Preview: true, + PreviewUrl: "https://example.com/#not-yet-implemented", + } + + result.Items = append(result.Items, item) + result.Annotations = append(result.Annotations, &schema.OpenCodeGraphAnnotation{ + Item: schema.OpenCodeGraphItemRef{Id: id}, + Range: ranges[i], + }) + } + + return &result, nil +} + +const anyQuote = "\"'`" + +var logViewEventCall = regexp.MustCompile(`eventLogger.log(?:ViewEvent)?\([` + anyQuote + `]([^` + anyQuote + `]+)[` + anyQuote + `]`) + +func telemetryCalls(content string) (events []string, ranges []schema.OpenCodeGraphRange) { + return firstSubmatchNamesAndRanges(logViewEventCall, content) +} diff --git a/internal/opencodegraph/chromatic_util.go b/internal/opencodegraph/chromatic_util.go new file mode 100644 index 00000000000..a23673cb68f --- /dev/null +++ b/internal/opencodegraph/chromatic_util.go @@ -0,0 +1,51 @@ +package opencodegraph + +import ( + "bytes" + "fmt" + "strings" + "unicode" +) + +// TODO(sqs): dupe from internal/observation/util.go + +// commonAcronyms includes acronyms that malform the expected output of kebabCase +// due to unexpected adjacent upper-case letters. Add items to this list to stop +// kebabCase from transforming `FromLSIF` into `from-l-s-i-f`. +var commonAcronyms = []string{ + "API", + "ID", + "URL", + "JSON", +} + +// acronymsReplacer is a string replacer that normalizes the acronyms above. For +// example, `API` will be transformed into `Api` so that it appears as one word. +var acronymsReplacer *strings.Replacer + +func init() { + var pairs []string + for _, acronym := range commonAcronyms { + pairs = append(pairs, acronym, fmt.Sprintf("%c%s", acronym[0], strings.ToLower(acronym[1:]))) + } + + acronymsReplacer = strings.NewReplacer(pairs...) +} + +// kebab transforms a string into lower-kebab-case. +func kebabCase(s string) string { + // Normalize all acronyms before looking at character transitions + s = acronymsReplacer.Replace(s) + + buf := bytes.NewBufferString("") + for i, c := range s { + // If we've seen a letter and we're going lower -> upper, add a skewer + if i > 0 && unicode.IsLower(rune(s[i-1])) && unicode.IsUpper(c) { + buf.WriteRune('-') + } + + buf.WriteRune(unicode.ToLower(c)) + } + + return buf.String() +} diff --git a/internal/opencodegraph/doc.go b/internal/opencodegraph/doc.go new file mode 100644 index 00000000000..5de348b0045 --- /dev/null +++ b/internal/opencodegraph/doc.go @@ -0,0 +1,2 @@ +// Package opencodegraph provides OpenCodeGraph metadata about code. +package opencodegraph diff --git a/internal/opencodegraph/docs_provider.go b/internal/opencodegraph/docs_provider.go new file mode 100644 index 00000000000..aff2f0b2c7e --- /dev/null +++ b/internal/opencodegraph/docs_provider.go @@ -0,0 +1,104 @@ +package opencodegraph + +import ( + "context" + "go/token" + + "github.com/grafana/regexp" + "github.com/sourcegraph/sourcegraph/schema" +) + +func init() { + RegisterProvider(docsProvider{}) +} + +type docsProvider struct{} + +func (docsProvider) Name() string { return "docs" } + +func (docsProvider) Capabilities(ctx context.Context, params schema.CapabilitiesParams) (*schema.CapabilitiesResult, error) { + return &schema.CapabilitiesResult{}, nil +} + +func (docsProvider) Annotations(ctx context.Context, params schema.AnnotationsParams) (*schema.AnnotationsResult, error) { + var result schema.AnnotationsResult + + for _, p := range docsPatterns { + if p.PathPattern != nil && !p.PathPattern.MatchString(params.File) { + continue + } + + var rs []schema.OpenCodeGraphRange + if p.ContentPattern != nil { + ms := p.ContentPattern.FindAllStringIndex(params.Content, -1) + if len(ms) == 0 { + continue + } + + fset := token.NewFileSet() + f := fset.AddFile(params.File, 1, len(params.Content)) + f.SetLinesForContent([]byte(params.Content)) + + for _, m := range ms { + mstart := m[0] + mend := m[1] + start := f.Position(f.Pos(mstart)) + end := f.Position(f.Pos(mend)) + rs = append(rs, schema.OpenCodeGraphRange{ + Start: schema.OpenCodeGraphPosition{Line: start.Line - 1, Character: start.Column - 1}, + End: schema.OpenCodeGraphPosition{Line: end.Line - 1, Character: end.Column - 1}, + }) + } + } else { + rs = append(rs, schema.OpenCodeGraphRange{ + Start: schema.OpenCodeGraphPosition{Line: 0, Character: 0}, + End: schema.OpenCodeGraphPosition{Line: 0, Character: 0}, + }) + } + + id := p.Id + result.Items = append(result.Items, &schema.OpenCodeGraphItem{ + Id: id, + Title: "📘 Docs: " + p.Title, + Url: p.URL, + Preview: true, + PreviewUrl: p.URL, + }) + for _, r := range rs { + result.Annotations = append(result.Annotations, &schema.OpenCodeGraphAnnotation{ + Item: schema.OpenCodeGraphItemRef{Id: id}, + Range: r, + }) + } + } + + return &result, nil +} + +var docsPatterns = []struct { + Id string + PathPattern, ContentPattern *regexp.Regexp + Title string + URL string +}{ + { + Id: "telemetry", + PathPattern: regexp.MustCompile(`\.tsx?$`), + ContentPattern: regexp.MustCompile(`eventLogger.log(?:ViewEvent)?`), + Title: "Telemetry", + URL: "https://docs.sourcegraph.com/dev/background-information/telemetry#sourcegraph-web-app", + }, + { + Id: "css", + PathPattern: regexp.MustCompile(`\.tsx?$`), + ContentPattern: regexp.MustCompile(`import styles from.*\.module\.s?css'`), + Title: "CSS in client/web", + URL: "https://docs.sourcegraph.com/dev/background-information/web/styling", + }, + { + Id: "bazel", + PathPattern: regexp.MustCompile(`(^|/)BUILD\.bazel$`), + Title: "Bazel at Sourcegraph", + URL: "https://docs.sourcegraph.com/dev/background-information/bazel", + }, +} diff --git a/internal/opencodegraph/googledocs_provider.go b/internal/opencodegraph/googledocs_provider.go new file mode 100644 index 00000000000..2555b1f428f --- /dev/null +++ b/internal/opencodegraph/googledocs_provider.go @@ -0,0 +1,62 @@ +package opencodegraph + +import ( + "context" + "fmt" + + "github.com/grafana/regexp" + "github.com/sourcegraph/sourcegraph/schema" +) + +func init() { + RegisterProvider(googledocsProvider{}) +} + +type googledocsProvider struct{} + +func (googledocsProvider) Name() string { return "googledocs" } + +func (googledocsProvider) Capabilities(ctx context.Context, params schema.CapabilitiesParams) (*schema.CapabilitiesResult, error) { + return &schema.CapabilitiesResult{ + Selector: []*schema.Selector{ + {ContentContains: "https://docs.google.com/document/d/"}, + }, + }, nil +} + +func (googledocsProvider) Annotations(ctx context.Context, params schema.AnnotationsParams) (*schema.AnnotationsResult, error) { + var result schema.AnnotationsResult + + docIDs, ranges := gdocIDs(params.Content) + for i, docID := range docIDs { + id := fmt.Sprintf("%s:%d", docID, i) + item := &schema.OpenCodeGraphItem{ + Id: id, + Title: "📝 GDoc: " + gdocTitles[docID], + Url: gdocURLPrefix + docID, + Preview: true, + PreviewUrl: "https://docs.google.com/document/d/" + docID + "/preview", + } + + result.Items = append(result.Items, item) + result.Annotations = append(result.Annotations, &schema.OpenCodeGraphAnnotation{ + Item: schema.OpenCodeGraphItemRef{Id: id}, + Range: ranges[i], + }) + } + + return &result, nil +} + +// TODO(sqs): fetch titles instead of hardcoding +var gdocTitles = map[string]string{ + "1Z1Yp7G61WYlQ1B4vO5-mIXVtmvzGmD7PqYHNBQV-2Ik": "Telemetry Export (ELE) rollout plan", +} + +const gdocURLPrefix = "https://docs.google.com/document/d/" + +var gdocURL = regexp.MustCompile(regexp.QuoteMeta(gdocURLPrefix) + `([\w-]+)`) + +func gdocIDs(content string) (ids []string, ranges []schema.OpenCodeGraphRange) { + return firstSubmatchNamesAndRanges(gdocURL, content) +} diff --git a/internal/opencodegraph/grafana_provider.go b/internal/opencodegraph/grafana_provider.go new file mode 100644 index 00000000000..e1d6cb757e8 --- /dev/null +++ b/internal/opencodegraph/grafana_provider.go @@ -0,0 +1,54 @@ +package opencodegraph + +import ( + "context" + "fmt" + + "github.com/grafana/regexp" + + "github.com/sourcegraph/sourcegraph/schema" +) + +func init() { + RegisterProvider(grafanaProvider{}) +} + +type grafanaProvider struct{} + +func (grafanaProvider) Name() string { return "grafana" } + +func (grafanaProvider) Capabilities(ctx context.Context, params schema.CapabilitiesParams) (*schema.CapabilitiesResult, error) { + return &schema.CapabilitiesResult{ + Selector: []*schema.Selector{ + {Path: "**/*.go", ContentContains: "prometheus."}, + }, + }, nil +} + +func (grafanaProvider) Annotations(ctx context.Context, params schema.AnnotationsParams) (*schema.AnnotationsResult, error) { + var result schema.AnnotationsResult + + metrics, ranges := prometheusMetricDefs(params.Content) + for i, metric := range metrics { + id := fmt.Sprintf("%s:%d", metric, i) + item := &schema.OpenCodeGraphItem{ + Id: id, + Title: "📟 Prometheus: " + metric, + Url: "https://sourcegraph.sourcegraph.com/-/debug/grafana/explore?orgId=1&left=%5B%22now-6h%22,%22now%22,%22Prometheus%22,%7B%22expr%22:%22" + metric + "%22,%22datasource%22:%22Prometheus%22,%22exemplar%22:true%7D%5D", + } + + result.Items = append(result.Items, item) + result.Annotations = append(result.Annotations, &schema.OpenCodeGraphAnnotation{ + Item: schema.OpenCodeGraphItemRef{Id: id}, + Range: ranges[i], + }) + } + + return &result, nil +} + +var prometheusMetricDef = regexp.MustCompile(`(?ms)prometheus\.(?:Gauge|Counter|Histogram)Opts\{[^}]+\s+Name:\s*"([^"]+)"`) + +func prometheusMetricDefs(content string) (gauges []string, ranges []schema.OpenCodeGraphRange) { + return firstSubmatchNamesAndRanges(prometheusMetricDef, content) +} diff --git a/internal/opencodegraph/multi.go b/internal/opencodegraph/multi.go new file mode 100644 index 00000000000..be4f61c7a4b --- /dev/null +++ b/internal/opencodegraph/multi.go @@ -0,0 +1,76 @@ +package opencodegraph + +import ( + "context" + "sort" + + "github.com/sourcegraph/sourcegraph/schema" + "golang.org/x/sync/errgroup" +) + +// multiProvider implements Provider by calling multiple providers and combining their results. +type multiProvider struct { + providers *[]Provider +} + +func (mp *multiProvider) Name() string { return "multi" } + +func (mp *multiProvider) Capabilities(ctx context.Context, params schema.CapabilitiesParams) (*schema.CapabilitiesResult, error) { + results := make([]*schema.CapabilitiesResult, len(*mp.providers)) + g, ctx := errgroup.WithContext(ctx) + for i, p := range *mp.providers { + i := i + p := p + g.Go(func() (err error) { + results[i], err = p.Capabilities(ctx, params) + return + }) + } + + if err := g.Wait(); err != nil { + return nil, err + } + + var merged schema.CapabilitiesResult + for _, r := range results { + merged.Selector = append(merged.Selector, r.Selector...) + } + return &merged, nil +} + +func (mp *multiProvider) Annotations(ctx context.Context, params schema.AnnotationsParams) (*schema.AnnotationsResult, error) { + results := make([]*schema.AnnotationsResult, len(*mp.providers)) + g, ctx := errgroup.WithContext(ctx) + for i, p := range *mp.providers { + i := i + p := p + g.Go(func() (err error) { + results[i], err = p.Annotations(ctx, params) + return + }) + } + + if err := g.Wait(); err != nil { + return nil, err + } + + var merged schema.AnnotationsResult + for i, res := range results { + providerName := (*mp.providers)[i].Name() + for _, item := range res.Items { + item.Id = providerName + ":" + item.Id + } + for _, ann := range res.Annotations { + ann.Item.Id = providerName + ":" + ann.Item.Id + } + + merged.Items = append(merged.Items, res.Items...) + merged.Annotations = append(merged.Annotations, res.Annotations...) + } + + sort.Slice(merged.Annotations, func(i, j int) bool { + return merged.Annotations[i].Range.Start.Line < merged.Annotations[j].Range.Start.Line + }) + + return &merged, nil +} diff --git a/internal/opencodegraph/protocol.go b/internal/opencodegraph/protocol.go new file mode 100644 index 00000000000..3fa7a414832 --- /dev/null +++ b/internal/opencodegraph/protocol.go @@ -0,0 +1,27 @@ +package opencodegraph + +import ( + "encoding/json" + + "github.com/sourcegraph/sourcegraph/schema" +) + +func DecodeRequestMessage(d *json.Decoder) (method string, cap *schema.CapabilitiesParams, ann *schema.AnnotationsParams, err error) { + var req struct { + schema.RequestMessage + Params json.RawMessage `json:"params"` + } + if err := d.Decode(&req); err != nil { + return "", nil, nil, err + } + + method = req.Method + switch method { + case "capabilities": + err = json.Unmarshal(req.Params, &cap) + case "annotations": + err = json.Unmarshal(req.Params, &ann) + } + + return +} diff --git a/internal/opencodegraph/protocol_test.go b/internal/opencodegraph/protocol_test.go new file mode 100644 index 00000000000..38333adbf89 --- /dev/null +++ b/internal/opencodegraph/protocol_test.go @@ -0,0 +1,51 @@ +package opencodegraph + +import ( + "encoding/json" + "reflect" + "strconv" + "strings" + "testing" + + "github.com/sourcegraph/sourcegraph/schema" +) + +func TestDecodeRequestMessage(t *testing.T) { + tests := []struct { + input string + wantMethod string + wantCapabilities *schema.CapabilitiesParams + wantAnnotations *schema.AnnotationsParams + }{ + { + input: `{"method":"capabilities","params":{}}`, + wantMethod: "capabilities", + wantCapabilities: &schema.CapabilitiesParams{}, + }, + { + input: `{"method":"annotations","params":{"file":"file:///a","content":"c"}}`, + wantMethod: "annotations", + wantAnnotations: &schema.AnnotationsParams{ + File: "file:///a", + Content: "c", + }, + }, + } + for i, test := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + method, capabilities, annotations, err := DecodeRequestMessage(json.NewDecoder(strings.NewReader(test.input))) + if err != nil { + t.Fatal(err) + } + if method != test.wantMethod { + t.Errorf("got method %q, want %q", method, test.wantMethod) + } + if !reflect.DeepEqual(capabilities, test.wantCapabilities) { + t.Errorf("got capabilities %+v, want %+v", capabilities, test.wantCapabilities) + } + if !reflect.DeepEqual(annotations, test.wantAnnotations) { + t.Errorf("got annotations %+v, want %+v", annotations, test.wantAnnotations) + } + }) + } +} diff --git a/internal/opencodegraph/providers.go b/internal/opencodegraph/providers.go new file mode 100644 index 00000000000..b16a420a3e2 --- /dev/null +++ b/internal/opencodegraph/providers.go @@ -0,0 +1,21 @@ +package opencodegraph + +import ( + "context" + + "github.com/sourcegraph/sourcegraph/schema" +) + +type Provider interface { + Name() string + Capabilities(ctx context.Context, params schema.CapabilitiesParams) (*schema.CapabilitiesResult, error) + Annotations(ctx context.Context, params schema.AnnotationsParams) (*schema.AnnotationsResult, error) +} + +var providers []Provider + +func RegisterProvider(provider Provider) { + providers = append(providers, provider) +} + +var AllProviders Provider = &multiProvider{providers: &providers} diff --git a/internal/opencodegraph/storybook_provider.go b/internal/opencodegraph/storybook_provider.go new file mode 100644 index 00000000000..ee8f47649a2 --- /dev/null +++ b/internal/opencodegraph/storybook_provider.go @@ -0,0 +1,210 @@ +package opencodegraph + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/grafana/regexp" + "github.com/sourcegraph/sourcegraph/lib/errors" + "github.com/sourcegraph/sourcegraph/schema" + "golang.org/x/net/context/ctxhttp" +) + +func init() { + RegisterProvider(storybookProvider{}) +} + +type storybookProvider struct{} + +func (storybookProvider) Name() string { return "storybook" } + +func (storybookProvider) Capabilities(ctx context.Context, params schema.CapabilitiesParams) (*schema.CapabilitiesResult, error) { + return &schema.CapabilitiesResult{ + Selector: []*schema.Selector{ + {Path: "**/*.story.(t|j)s?(x)"}, + {Path: "**/*.(t|j)s(x)", ContentContains: "React"}, + }, + }, nil +} + +func (storybookProvider) Annotations(ctx context.Context, params schema.AnnotationsParams) (*schema.AnnotationsResult, error) { + var result schema.AnnotationsResult + + if strings.HasSuffix(params.File, ".story.tsx") { + if component := getStoryTitle(params.Content); component != "" { + stories, ranges := firstSubmatchNamesAndRanges(exportedStory, params.Content) + for i, story := range stories { + id := fmt.Sprintf("%s:%d", story, i) + story = getStoryNameAlias(story, params.Content) + storyURL := chromaticStoryURL(component, story) + item := &schema.OpenCodeGraphItem{ + Id: id, + Title: "🖼️ Storybook: " + component + "/" + story, + Url: storyURL, + Preview: true, + PreviewUrl: chromaticIframeURL(component, story), + } + + info, err := getEmbedInfoForChromaticStorybook(ctx, storyURL) + if err != nil { + return nil, err + } + item.Image = info.image + + result.Items = append(result.Items, item) + result.Annotations = append(result.Annotations, &schema.OpenCodeGraphAnnotation{ + Item: schema.OpenCodeGraphItemRef{Id: id}, + Range: ranges[i], + }) + } + } + } else { + names, ranges := firstSubmatchNamesAndRanges(exportedReactComponentName, params.Content) + for i, name := range names { + id := fmt.Sprintf("%s:%d", name, i) + component := getStoryComponentTitleForReactComponent(params.File, name) + if component == "" { + continue + } + + const story = "Default" + storyURL := chromaticStoryURL(component, story) + item := &schema.OpenCodeGraphItem{ + Id: id, + Title: "🖼️ Storybook: " + component, + Url: storyURL, + Preview: true, + PreviewUrl: chromaticIframeURL(component, story), + } + + info, err := getEmbedInfoForChromaticStorybook(ctx, storyURL) + if err != nil { + return nil, err + } + item.Image = info.image + + result.Items = append(result.Items, item) + result.Annotations = append(result.Annotations, &schema.OpenCodeGraphAnnotation{ + Item: schema.OpenCodeGraphItemRef{Id: id}, + Range: ranges[i], + }) + } + } + + return &result, nil +} + +var storyTitle = regexp.MustCompile(`\btitle: '([^']+)'`) + +func getStoryTitle(content string) string { + m := storyTitle.FindStringSubmatch(content) + if m == nil { + return "" + } + return string(m[1]) +} + +func getStoryNameAlias(story string, content string) string { + // Look for `PlainRequest.storyName = 'plain request'` or similar. + storyNameAlias := regexp.MustCompile(story + `\.storyName = '(\w+)'`) + m := storyNameAlias.FindStringSubmatch(content) + if m != nil { + story = string(m[1]) + } + return story +} + +var ( + exportedStory = regexp.MustCompile(`export const (\w+): Story`) + exportedReactComponentName = regexp.MustCompile(`export const ([A-Z]\w+): React\.`) +) + +func getStoryComponentTitleForReactComponent(path, reactComponentName string) string { + _ = path + _ = reactComponentName + m := map[string]string{ + // TODO(sqs): un-hardcode for sourcegraph + "SignInPage": "web/auth/SignInPage", + } + return m[reactComponentName] +} + +func chromaticStorySlug(component, story string) string { + return strings.ToLower(strings.Replace(component, "/", "-", -1)) + "--" + kebabCase(story) +} + +func chromaticStoryURL(component, story string) string { + return (&url.URL{ + Scheme: "https", + // TODO(sqs): un-hardcode for sourcegraph + Host: "5f0f381c0e50750022dc6bf7-qjtkjsausw.chromatic.com", + Path: "/", + RawQuery: (url.Values{"path": []string{"/story/" + chromaticStorySlug(component, story)}}).Encode(), + }).String() +} + +func chromaticIframeURL(component, story string) string { + return (&url.URL{ + Scheme: "https", + // TODO(sqs): un-hardcode for sourcegraph + Host: "5f0f381c0e50750022dc6bf7-qjtkjsausw.chromatic.com", + Path: "/iframe.html", + RawQuery: (url.Values{ + "id": []string{chromaticStorySlug(component, story)}, + "singleStory": []string{"true"}, + "controls": []string{"false"}, + "embed": []string{"true"}, + "viewMode": []string{"story"}, + }).Encode(), + }).String() +} + +type chromaticEmbedInfo struct { + image *schema.OpenCodeGraphImage +} + +func getEmbedInfoForChromaticStorybook(ctx context.Context, chromaticStoryURL string) (*chromaticEmbedInfo, error) { + oembedURL := &url.URL{ + Scheme: "https", + Host: "www.chromatic.com", + Path: "/oembed", + RawQuery: (url.Values{ + "url": []string{chromaticStoryURL}, + "format": []string{"json"}, + }).Encode(), + } + resp, err := ctxhttp.Get(ctx, http.DefaultClient, oembedURL.String()) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.Errorf("chromatic oembed endpoint %s returned HTTP %d", oembedURL, resp.StatusCode) + } + + var oembedData struct { + Title string `json:"title"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` + ThumbnailWidth int `json:"thumbnail_width,omitempty"` + ThumbnailHeight int `json:"thumbnail_height,omitempty"` + HTML string `json:"html,omitempty"` + } + if err := json.NewDecoder(resp.Body).Decode(&oembedData); err != nil { + return nil, err + } + + var info chromaticEmbedInfo + if oembedData.ThumbnailURL != "" { + info.image = &schema.OpenCodeGraphImage{ + Url: oembedData.ThumbnailURL, + Width: float64(oembedData.ThumbnailWidth), + Height: float64(oembedData.ThumbnailHeight), + Alt: oembedData.Title, + } + } + return &info, nil +} diff --git a/internal/opencodegraph/util.go b/internal/opencodegraph/util.go new file mode 100644 index 00000000000..9da518d378f --- /dev/null +++ b/internal/opencodegraph/util.go @@ -0,0 +1,31 @@ +package opencodegraph + +import ( + "go/token" + + "github.com/grafana/regexp" + "github.com/sourcegraph/sourcegraph/schema" +) + +func firstSubmatchNamesAndRanges(pattern *regexp.Regexp, content string) (names []string, ranges []schema.OpenCodeGraphRange) { + fset := token.NewFileSet() + f := fset.AddFile("x", 1, len(content)) + f.SetLinesForContent([]byte(content)) + + ms := pattern.FindAllStringSubmatchIndex(content, -1) + for _, m := range ms { + mstart := m[2] + mend := m[3] + start := f.Position(f.Pos(mstart)) + end := f.Position(f.Pos(mend)) + + name := string(content[mstart:mend]) + names = append(names, name) + ranges = append(ranges, schema.OpenCodeGraphRange{ + Start: schema.OpenCodeGraphPosition{Line: start.Line - 1, Character: start.Column - 1}, + End: schema.OpenCodeGraphPosition{Line: end.Line - 1, Character: end.Column - 1}, + }) + } + + return names, ranges +} diff --git a/package.json b/package.json index 068f0eaa600..e43161368ab 100644 --- a/package.json +++ b/package.json @@ -287,6 +287,8 @@ "@mdi/js": "7.1.96", "@microsoft/fast-web-utilities": "^6.0.0", "@microsoft/fetch-event-source": "^2.0.1", + "@opencodegraph/client": "^0.0.1", + "@opencodegraph/codemirror-extension": "^0.0.1", "@opentelemetry/api": "^1.4.0", "@opentelemetry/context-zone": "^1.9.1", "@opentelemetry/core": "1.9.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac183d29d10..ba1f63a76f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,6 +67,12 @@ importers: '@microsoft/fetch-event-source': specifier: ^2.0.1 version: 2.0.1 + '@opencodegraph/client': + specifier: ^0.0.1 + version: 0.0.1 + '@opencodegraph/codemirror-extension': + specifier: ^0.0.1 + version: 0.0.1(@codemirror/state@6.2.0)(@codemirror/view@6.7.3)(react-dom@18.1.0)(react@18.1.0) '@opentelemetry/api': specifier: ^1.4.0 version: 1.4.0 @@ -5253,6 +5259,64 @@ packages: resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} dev: true + /@opencodegraph/client@0.0.1: + resolution: {integrity: sha512-XdgDmUG26dfo1eBbmXKaMh8CuavCfCFIVVuM45iCaLQv0L1fw4bLB+na7E3cjFhMoxVFXEjllLpO9G85M72oiQ==} + dependencies: + '@opencodegraph/protocol': 0.0.1 + '@opencodegraph/provider': 0.0.1 + '@opencodegraph/schema': 0.0.1 + lru-cache: 10.1.0 + picomatch: 3.0.1 + rxjs: 7.8.1 + dev: false + + /@opencodegraph/codemirror-extension@0.0.1(@codemirror/state@6.2.0)(@codemirror/view@6.7.3)(react-dom@18.1.0)(react@18.1.0): + resolution: {integrity: sha512-n3WP/88qcgeJboqGI7HOfPujkhnTAJ5TGV9IIs03MAyY/V4/+/J8TclhUzjCX5Pu27eoG4RuZlY7379ejUvYkw==} + peerDependencies: + '@codemirror/state': ^6.2.0 + '@codemirror/view': ^6.7.2 + dependencies: + '@codemirror/state': 6.2.0 + '@codemirror/view': 6.7.3 + '@opencodegraph/client': 0.0.1 + '@opencodegraph/ui-react': 0.0.1(react-dom@18.1.0)(react@18.1.0) + deep-equal: 2.2.3 + transitivePeerDependencies: + - react + - react-dom + dev: false + + /@opencodegraph/protocol@0.0.1: + resolution: {integrity: sha512-IndtaDuY1dm2U66okicW8VNuYilUfJG9mCLGKMXriIbIGyTwqYLKF3EcPeqLUc7xrDwr1KAhl50xlUJGoAxi3Q==} + dependencies: + '@opencodegraph/schema': 0.0.1 + dev: false + + /@opencodegraph/provider@0.0.1: + resolution: {integrity: sha512-nrCCM/lWR6y9hf00s1KI1L48sDiFt0EuyrJAX6PTd/kNqHwg2TxCsiNs4RAx0eTn5bLc+YPeIMRbOWAE4NC/OQ==} + dependencies: + '@opencodegraph/protocol': 0.0.1 + '@opencodegraph/schema': 0.0.1 + picomatch: 3.0.1 + dev: false + + /@opencodegraph/schema@0.0.1: + resolution: {integrity: sha512-v7ulJvSH4yLULEq5dimchQ+Wp0qvOt5Lh/pyhOqxLrMz/mvdKvrY8JBHJDE5S0mnZrYXDSWbq2LYOxdUbhK45g==} + dev: false + + /@opencodegraph/ui-react@0.0.1(react-dom@18.1.0)(react@18.1.0): + resolution: {integrity: sha512-Nw2nTRu7ERoS4TuulQDR2ez85Sypart6jvsb7n27/0DzizcUoQqpNLfs/UAh70yMMPQF9x1h+Xr3yr6zFdwvew==} + peerDependencies: + react: ^16.8.0 ^17 ^18 + react-dom: ^16.8.0 ^17 ^18 + dependencies: + '@opencodegraph/schema': 0.0.1 + '@reach/popover': 0.18.0(react-dom@18.1.0)(react@18.1.0) + classnames: 2.3.2 + react: 18.1.0 + react-dom: 18.1.0(react@18.1.0) + dev: false + /@opentelemetry/api@1.4.0: resolution: {integrity: sha512-IgMK9i3sFGNUqPMbjABm0G26g0QCKCUBfglhQ7rQq6WcxbKfEHRcmwsoER4hZcuYqJgkYn2OeuoJIv7Jsftp7g==} engines: {node: '>=8.0.0'} @@ -6927,6 +6991,14 @@ packages: resolution: {integrity: sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==} dev: false + /@reach/polymorphic@0.18.0(react@18.1.0): + resolution: {integrity: sha512-N9iAjdMbE//6rryZZxAPLRorzDcGBnluf7YQij6XDLiMtfCj1noa7KyLpEc/5XCIB/EwhX3zCluFAwloBKdblA==} + peerDependencies: + react: ^16.8.0 || 17.x + dependencies: + react: 18.1.0 + dev: false + /@reach/popover@0.16.2(react-dom@18.1.0)(react@18.1.0): resolution: {integrity: sha512-IwkRrHM7Vt33BEkSXneovymJv7oIToOfTDwRKpuYEB/BWYMAuNfbsRL7KVe6MjkgchDeQzAk24cYY1ztQj5HQQ==} peerDependencies: @@ -6942,6 +7014,21 @@ packages: tslib: 2.1.0 dev: false + /@reach/popover@0.18.0(react-dom@18.1.0)(react@18.1.0): + resolution: {integrity: sha512-mpnWWn4w74L2U7fcneVdA6Fz3yKWNdZIRMoK8s6H7F8U2dLM/qN7AjzjEBqi6LXKb3Uf1ge4KHSbMixW0BygJQ==} + peerDependencies: + react: ^16.8.0 || 17.x + react-dom: ^16.8.0 || 17.x + dependencies: + '@reach/polymorphic': 0.18.0(react@18.1.0) + '@reach/portal': 0.18.0(react-dom@18.1.0)(react@18.1.0) + '@reach/rect': 0.18.0(react-dom@18.1.0)(react@18.1.0) + '@reach/utils': 0.18.0(react-dom@18.1.0)(react@18.1.0) + react: 18.1.0 + react-dom: 18.1.0(react@18.1.0) + tabbable: 5.3.3 + dev: false + /@reach/portal@0.16.2(react-dom@18.1.0)(react@18.1.0): resolution: {integrity: sha512-9ur/yxNkuVYTIjAcfi46LdKUvH0uYZPfEp4usWcpt6PIp+WDF57F/5deMe/uGi/B/nfDweQu8VVwuMVrCb97JQ==} peerDependencies: @@ -6955,6 +7042,17 @@ packages: tslib: 2.1.0 dev: false + /@reach/portal@0.18.0(react-dom@18.1.0)(react@18.1.0): + resolution: {integrity: sha512-TImozRapd576ofRk30Le2L3lRTFXF1p47B182wnp5eMTdZa74JX138BtNGEPJFOyrMaVmguVF8SSwZ6a0fon1Q==} + peerDependencies: + react: ^16.8.0 || 17.x + react-dom: ^16.8.0 || 17.x + dependencies: + '@reach/utils': 0.18.0(react-dom@18.1.0)(react@18.1.0) + react: 18.1.0 + react-dom: 18.1.0(react@18.1.0) + dev: false + /@reach/rect@0.16.0(react-dom@18.1.0)(react@18.1.0): resolution: {integrity: sha512-/qO9jQDzpOCdrSxVPR6l674mRHNTqfEjkaxZHluwJ/2qGUtYsA0GSZiF/+wX/yOWeBif1ycxJDa6HusAMJZC5Q==} peerDependencies: @@ -6970,6 +7068,18 @@ packages: tslib: 2.1.0 dev: false + /@reach/rect@0.18.0(react-dom@18.1.0)(react@18.1.0): + resolution: {integrity: sha512-Xk8urN4NLn3F70da/DtByMow83qO6DF6vOxpLjuDBqud+kjKgxAU9vZMBSZJyH37+F8mZinRnHyXtlLn5njQOg==} + peerDependencies: + react: ^16.8.0 || 17.x + react-dom: ^16.8.0 || 17.x + dependencies: + '@reach/observe-rect': 1.2.0 + '@reach/utils': 0.18.0(react-dom@18.1.0)(react@18.1.0) + react: 18.1.0 + react-dom: 18.1.0(react@18.1.0) + dev: false + /@reach/tabs@0.16.4(react-dom@18.1.0)(react@18.1.0): resolution: {integrity: sha512-4EK+1U0OoLfg2tJ1BSZf6/tx0hF5vlXKxY7qB//bPWtlIh9Xfp/aSDIdspFf3xS8MjtKeb6IVmo5UAxDMq85ZA==} peerDependencies: @@ -6997,6 +7107,16 @@ packages: tslib: 2.1.0 dev: false + /@reach/utils@0.18.0(react-dom@18.1.0)(react@18.1.0): + resolution: {integrity: sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A==} + peerDependencies: + react: ^16.8.0 || 17.x + react-dom: ^16.8.0 || 17.x + dependencies: + react: 18.1.0 + react-dom: 18.1.0(react@18.1.0) + dev: false + /@reach/visually-hidden@0.16.0(react-dom@18.1.0)(react@18.1.0): resolution: {integrity: sha512-IIayZ3jzJtI5KfcfRVtOMFkw2ef/1dMT8D9BUuFcU2ORZAWLNvnzj1oXNoIfABKl5wtsLjY6SGmkYQ+tMPN8TA==} peerDependencies: @@ -12035,7 +12155,7 @@ packages: /aria-query@5.1.3: resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} dependencies: - deep-equal: 2.2.2 + deep-equal: 2.2.3 dev: true /aria-query@5.3.0: @@ -13933,8 +14053,9 @@ packages: type-detect: 4.0.8 dev: true - /deep-equal@2.2.2: - resolution: {integrity: sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==} + /deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} dependencies: array-buffer-byte-length: 1.0.0 call-bind: 1.0.5 @@ -13954,7 +14075,6 @@ packages: which-boxed-primitive: 1.0.2 which-collection: 1.0.1 which-typed-array: 1.1.13 - dev: true /deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} @@ -14489,7 +14609,6 @@ packages: is-string: 1.0.7 isarray: 2.0.5 stop-iteration-iterator: 1.0.0 - dev: true /es-iterator-helpers@1.0.15: resolution: {integrity: sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==} @@ -18459,10 +18578,9 @@ packages: highlight.js: 10.7.3 dev: true - /lru-cache@10.0.0: - resolution: {integrity: sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==} + /lru-cache@10.1.0: + resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} engines: {node: 14 || >=16.14} - dev: true /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -19693,7 +19811,6 @@ packages: dependencies: call-bind: 1.0.5 define-properties: 1.2.1 - dev: true /object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} @@ -20255,7 +20372,7 @@ packages: resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} engines: {node: '>=16 || 14 >=14.17'} dependencies: - lru-cache: 10.0.0 + lru-cache: 10.1.0 minipass: 7.0.2 dev: true @@ -20341,6 +20458,11 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + /picomatch@3.0.1: + resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==} + engines: {node: '>=10'} + dev: false + /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -22824,7 +22946,6 @@ packages: engines: {node: '>= 0.4'} dependencies: internal-slot: 1.0.6 - dev: true /store2@2.14.2: resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} @@ -23497,6 +23618,10 @@ packages: resolution: {integrity: sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==} dev: false + /tabbable@5.3.3: + resolution: {integrity: sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==} + dev: false + /tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} dev: false diff --git a/schema/BUILD.bazel b/schema/BUILD.bazel index bde34b42f2b..970489eed38 100644 --- a/schema/BUILD.bazel +++ b/schema/BUILD.bazel @@ -44,6 +44,20 @@ js_library( ], ) +js_library( + name = "opencodegraph", + srcs = [ + "opencodegraph.schema.json", + ], +) + +js_library( + name = "opencodegraph-protocol", + srcs = [ + "opencodegraph-protocol.schema.json", + ], +) + go_library( name = "schema", srcs = [ @@ -79,6 +93,8 @@ go_library( "site.schema.json", "azuredevops.schema.json", "localgit.schema.json", + "opencodegraph.schema.json", + "opencodegraph-protocol.schema.json", ], importpath = "github.com/sourcegraph/sourcegraph/schema", visibility = ["//visibility:public"], diff --git a/schema/opencodegraph-protocol.schema.json b/schema/opencodegraph-protocol.schema.json new file mode 100644 index 00000000000..74ac4d590d9 --- /dev/null +++ b/schema/opencodegraph-protocol.schema.json @@ -0,0 +1,122 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "opencodegraph-protocol.schema.json#", + "title": "OpenCodeGraphProtocol", + "description": "OpenCodeGraph client/provider protocol", + "allowComments": true, + "oneOf": [ + { "$ref": "#/definitions/RequestMessage" }, + { "$ref": "#/definitions/ResponseMessage" }, + { "$ref": "#/definitions/ResponseError" }, + { "$ref": "#/definitions/ProviderSettings" }, + { "$ref": "#/definitions/CapabilitiesParams" }, + { "$ref": "#/definitions/CapabilitiesResult" }, + { "$ref": "#/definitions/AnnotationsParams" }, + { "$ref": "#/definitions/AnnotationsResult" } + ], + "definitions": { + "RequestMessage": { + "type": "object", + "additionalProperties": false, + "required": ["method"], + "properties": { + "method": { "type": "string" }, + "params": { "type": ["object", "array"], "tsType": "unknown" }, + "settings": { "$ref": "#/definitions/ProviderSettings" } + } + }, + "ProviderSettings": { + "description": "User settings sent by the client to the provider.", + "type": "object", + "additionalProperties": true + }, + "ResponseMessage": { + "type": "object", + "additionalProperties": false, + "properties": { + "result": { "type": ["object", "array"], "tsType": "unknown" }, + "error": { "$ref": "#/definitions/ResponseError" } + } + }, + "ResponseError": { + "type": "object", + "additionalProperties": false, + "required": ["code", "message"], + "properties": { + "code": { "type": "integer" }, + "message": { "type": "string" }, + "data": { "type": ["object", "array"], "tsType": "unknown" } + } + }, + "CapabilitiesParams": { + "type": "object", + "additionalProperties": false, + "$comment": "(empty for now)", + "properties": {} + }, + "CapabilitiesResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "selector": { + "description": "Selects the scope (repositories, files, and languages) in which this provider should be called.\n\nAt least 1 must be satisfied for the provider to be called. If empty, the provider is never called. If undefined, the provider is called on all files.", + "type": "array", + "items": { + "title": "Selector", + "description": "Defines a scope in which a provider is called, as a subset of languages, repositories, and/or files.\n\nTo satisfy a selector, all of the selector's conditions must be met. For example, if both `path` and `content` are specified, the file must satisfy both conditions.", + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "description": "A glob that must match the file path. If the file's location is represented as a URI, the URI's scheme is stripped before being matched against this glob.\n\nUse `**/` before the glob to match in any parent directory. Use `/**` after the glob to match any files under a directory. Leading slashes are stripped from the path before being matched against the glob.", + "type": "string" + }, + "contentContains": { + "description": "A literal string that must be present in the file's content.", + "type": "string" + } + } + } + } + } + }, + "AnnotationsParams": { + "type": "object", + "additionalProperties": false, + "required": ["file", "content"], + "properties": { + "file": { + "description": "The file's URI.", + "type": "string" + }, + "content": { + "description": "The file's content.", + "type": "string" + } + } + }, + "AnnotationsResult": { + "type": "object", + "additionalProperties": false, + "required": ["items", "annotations"], + "properties": { + "items": { + "description": "Items that contain information relevant to the file.", + "type": "array", + "items": { + "$ref": "opencodegraph.schema.json#/definitions/OpenCodeGraphItem" + }, + "tsType": "OpenCodeGraphItem[]" + }, + "annotations": { + "description": "Annotations that attach items to specific ranges in the file.", + "type": "array", + "items": { + "$ref": "opencodegraph.schema.json#/definitions/OpenCodeGraphAnnotation" + }, + "tsType": "OpenCodeGraphAnnotation[]" + } + } + } + } +} diff --git a/schema/opencodegraph.schema.json b/schema/opencodegraph.schema.json new file mode 100644 index 00000000000..3e87bc5545f --- /dev/null +++ b/schema/opencodegraph.schema.json @@ -0,0 +1,82 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "opencodegraph.schema.json#", + "title": "OpenCodeGraphData", + "description": "Metadata about code", + "allowComments": true, + "type": "object", + "additionalProperties": false, + "required": ["items", "annotations"], + "properties": { + "items": { + "type": "array", + "items": { "$ref": "#/definitions/OpenCodeGraphItem" } + }, + "annotations": { + "type": "array", + "items": { "$ref": "#/definitions/OpenCodeGraphAnnotation" } + } + }, + "definitions": { + "OpenCodeGraphItem": { + "type": "object", + "additionalProperties": false, + "required": ["id", "title"], + "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, + "detail": { "type": "string" }, + "url": { "description": "An external URL with more information.", "type": "string", "format": "uri" }, + "preview": { "description": "Show a preview of the link.", "type": "boolean" }, + "previewUrl": { + "description": "If `preview` is set, show this URL as the preview instead of `url`.", + "type": "string" + }, + "image": { "$ref": "#/definitions/OpenCodeGraphImage" } + } + }, + "OpenCodeGraphImage": { + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "url": { "type": "string", "format": "uri" }, + "width": { "type": "number" }, + "height": { "type": "number" }, + "alt": { "type": "string" } + } + }, + "OpenCodeGraphAnnotation": { + "type": "object", + "additionalProperties": false, + "required": ["range", "item"], + "properties": { + "range": { "$ref": "#/definitions/OpenCodeGraphRange" }, + "item": { + "title": "OpenCodeGraphItemRef", + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { "type": "string" } + } + } + } + }, + "OpenCodeGraphRange": { + "type": "object", + "additionalProperties": false, + "required": ["start", "end"], + "properties": { + "start": { "$ref": "#/definitions/OpenCodeGraphPosition" }, + "end": { "$ref": "#/definitions/OpenCodeGraphPosition" } + } + }, + "OpenCodeGraphPosition": { + "type": "object", + "additionalProperties": false, + "required": ["line", "character"], + "properties": { "line": { "type": "integer" }, "character": { "type": "integer" } } + } + } +} diff --git a/schema/schema.go b/schema/schema.go index 96c28445673..a83735388f1 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -53,6 +53,18 @@ type AWSKMSEncryptionKey struct { Region string `json:"region,omitempty"` Type string `json:"type"` } +type AnnotationsParams struct { + // Content description: The file's content. + Content string `json:"content"` + // File description: The file's URI. + File string `json:"file"` +} +type AnnotationsResult struct { + // Annotations description: Annotations that attach items to specific ranges in the file. + Annotations []*OpenCodeGraphAnnotation `json:"annotations"` + // Items description: Items that contain information relevant to the file. + Items []*OpenCodeGraphItem `json:"items"` +} // App description: Configuration options for App only. type App struct { @@ -526,6 +538,14 @@ type BuiltinAuthProvider struct { AllowSignup bool `json:"allowSignup,omitempty"` Type string `json:"type"` } +type CapabilitiesParams struct { +} +type CapabilitiesResult struct { + // Selector description: Selects the scope (repositories, files, and languages) in which this provider should be called. + // + // At least 1 must be satisfied for the provider to be called. If empty, the provider is never called. If undefined, the provider is called on all files. + Selector []*Selector `json:"selector,omitempty"` +} // ChangesetTemplate description: A template describing how to create (and update) changesets with the file changes produced by the command steps. type ChangesetTemplate struct { @@ -1770,6 +1790,45 @@ type OnboardingTourConfiguration struct { DefaultSnippets map[string]any `json:"defaultSnippets,omitempty"` Tasks []*OnboardingTask `json:"tasks"` } +type OpenCodeGraphAnnotation struct { + Item OpenCodeGraphItemRef `json:"item"` + Range OpenCodeGraphRange `json:"range"` +} + +// OpenCodeGraphData description: Metadata about code +type OpenCodeGraphData struct { + Annotations []*OpenCodeGraphAnnotation `json:"annotations"` + Items []*OpenCodeGraphItem `json:"items"` +} +type OpenCodeGraphImage struct { + Alt string `json:"alt,omitempty"` + Height float64 `json:"height,omitempty"` + Url string `json:"url"` + Width float64 `json:"width,omitempty"` +} +type OpenCodeGraphItem struct { + Detail string `json:"detail,omitempty"` + Id string `json:"id"` + Image *OpenCodeGraphImage `json:"image,omitempty"` + // Preview description: Show a preview of the link. + Preview bool `json:"preview,omitempty"` + // PreviewUrl description: If `preview` is set, show this URL as the preview instead of `url`. + PreviewUrl string `json:"previewUrl,omitempty"` + Title string `json:"title"` + // Url description: An external URL with more information. + Url string `json:"url,omitempty"` +} +type OpenCodeGraphItemRef struct { + Id string `json:"id"` +} +type OpenCodeGraphPosition struct { + Character int `json:"character"` + Line int `json:"line"` +} +type OpenCodeGraphRange struct { + End OpenCodeGraphPosition `json:"end"` + Start OpenCodeGraphPosition `json:"start"` +} // OpenIDConnectAuthProvider description: Configures the OpenID Connect authentication provider for SSO. type OpenIDConnectAuthProvider struct { @@ -2045,12 +2104,26 @@ type Repository struct { // Owner description: The repository namespace. Owner string `json:"owner,omitempty"` } +type RequestMessage struct { + Method string `json:"method"` + Params any `json:"params,omitempty"` + Settings map[string]any `json:"settings,omitempty"` +} type Responders struct { Id string `json:"id,omitempty"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Username string `json:"username,omitempty"` } +type ResponseError struct { + Code int `json:"code"` + Data any `json:"data,omitempty"` + Message string `json:"message"` +} +type ResponseMessage struct { + Error *ResponseError `json:"error,omitempty"` + Result any `json:"result,omitempty"` +} // RestartStep description: Restart step type RestartStep struct { @@ -2211,6 +2284,18 @@ type SecurityEventLog struct { Location string `json:"location,omitempty"` } +// Selector description: Defines a scope in which a provider is called, as a subset of languages, repositories, and/or files. +// +// To satisfy a selector, all of the selector's conditions must be met. For example, if both `path` and `content` are specified, the file must satisfy both conditions. +type Selector struct { + // ContentContains description: A literal string that must be present in the file's content. + ContentContains string `json:"contentContains,omitempty"` + // Path description: A glob that must match the file path. If the file's location is represented as a URI, the URI's scheme is stripped before being matched against this glob. + // + // Use `**/` before the glob to match in any parent directory. Use `/**` after the glob to match any files under a directory. Leading slashes are stripped from the path before being matched against the glob. + Path string `json:"path,omitempty"` +} + // Sentry description: Configuration for Sentry type Sentry struct { // BackendDSN description: Sentry Data Source Name (DSN) for backend errors. Per the Sentry docs (https://docs.sentry.io/quickstart/#about-the-dsn), it should match the following pattern: '{PROTOCOL}://{PUBLIC_KEY}@{HOST}/{PATH}{PROJECT_ID}'. diff --git a/schema/stringdata.go b/schema/stringdata.go index de88939a286..c2c3be983de 100644 --- a/schema/stringdata.go +++ b/schema/stringdata.go @@ -104,3 +104,13 @@ var SettingsSchemaJSON string // //go:embed site.schema.json var SiteSchemaJSON string + +// OpenCodeGraphSchemaJSON is the content of the file "opencodegraph.schema.json". +// +//go:embed opencodegraph.schema.json +var OpenCodeGraphSchemaJSON string + +// OpenCodeGraphProtocolSchemaJSON is the content of the file "opencodegraph-protocol.schema.json". +// +//go:embed opencodegraph-protocol.schema.json +var OpenCodeGraphProtocolSchemaJSON string