mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 14:51:44 +00:00
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
This commit is contained in:
parent
dccbed6be2
commit
c9439d9456
@ -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 = [
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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<keyof TemporarySettings, null> = {
|
||||
'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)[]
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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),
|
||||
}),
|
||||
|
||||
@ -48,4 +48,4 @@
|
||||
"@sourcegraph/telemetry": "^0.11.0",
|
||||
"@sourcegraph/wildcard": "workspace:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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> = 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 (
|
||||
<SimpleActionItem tooltip={descriptiveText} isActive={visible} onSelect={onCycle} disabled={disabled}>
|
||||
<Icon aria-hidden={true} svgPath={icon} />
|
||||
</SimpleActionItem>
|
||||
)
|
||||
}
|
||||
|
||||
if (props.actionType === 'dropdown') {
|
||||
return (
|
||||
<RepoHeaderActionMenuLink file={true} as={Button} onClick={onCycle} disabled={disabled}>
|
||||
<Icon aria-hidden={true} svgPath={icon} />
|
||||
<span>{descriptiveText}</span>
|
||||
</RepoHeaderActionMenuLink>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={descriptiveText}>
|
||||
<RepoHeaderActionAnchor
|
||||
onSelect={onCycle}
|
||||
disabled={disabled}
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
>
|
||||
<Icon aria-hidden={true} svgPath={icon} />
|
||||
</RepoHeaderActionAnchor>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@ -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')
|
||||
}
|
||||
@ -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<BlobPageProps> = ({ 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<BlobPageProps> = ({ className, co
|
||||
/>
|
||||
)}
|
||||
</RepoHeaderContributionPortal>
|
||||
{enableOpenCodeGraph && (
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={4}
|
||||
id="toggle-opencodegraph"
|
||||
repoHeaderContributionsLifecycleProps={props.repoHeaderContributionsLifecycleProps}
|
||||
>
|
||||
{({ actionType }) => (
|
||||
<ToggleOpenCodeGraphVisibilityAction
|
||||
actionType={actionType}
|
||||
source="repoHeader"
|
||||
renderMode={renderMode}
|
||||
/>
|
||||
)}
|
||||
</RepoHeaderContributionPortal>
|
||||
)}
|
||||
<RepoHeaderContributionPortal
|
||||
position="right"
|
||||
priority={20}
|
||||
@ -582,6 +604,7 @@ export const BlobPage: React.FunctionComponent<BlobPageProps> = ({ className, co
|
||||
ariaLabel="File blob"
|
||||
isBlameVisible={isBlameVisible}
|
||||
blameHunks={blameHunks}
|
||||
ocgVisibility={ocgVisibility}
|
||||
overrideBrowserSearchKeybinding={true}
|
||||
/>
|
||||
</TraceSpanProvider>
|
||||
|
||||
@ -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<BlobProps> = props => {
|
||||
extensionsController,
|
||||
isBlameVisible,
|
||||
blameHunks,
|
||||
ocgVisibility,
|
||||
|
||||
// Reference panel specific props
|
||||
navigateToLineOnAnyClick,
|
||||
@ -325,6 +332,15 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = 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<BlobProps> = props => {
|
||||
onLineClick: navigateOnClick,
|
||||
}),
|
||||
scipSnapshot(blobInfo.content, blobInfo.snapshotData),
|
||||
openCodeGraphExtension,
|
||||
codeFoldingExtension(),
|
||||
isCodyEnabled()
|
||||
? codyWidgetExtension(
|
||||
@ -370,6 +387,7 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = 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<BlobProps> = props => {
|
||||
blobInfo,
|
||||
extensionsController,
|
||||
isCodyEnabled,
|
||||
openCodeGraphExtension,
|
||||
codeIntelExtension,
|
||||
editorRef.current,
|
||||
blameDecorations,
|
||||
@ -420,7 +439,7 @@ export const CodeMirrorBlob: React.FunctionComponent<BlobProps> = 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<Annotation[]>()
|
||||
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").
|
||||
|
||||
@ -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': {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
80
cmd/frontend/internal/httpapi/opencodegraph.go
Normal file
80
cmd/frontend/internal/httpapi/opencodegraph.go
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
35
internal/opencodegraph/BUILD.bazel
Normal file
35
internal/opencodegraph/BUILD.bazel
Normal file
@ -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"],
|
||||
)
|
||||
57
internal/opencodegraph/amplitude_provider.go
Normal file
57
internal/opencodegraph/amplitude_provider.go
Normal file
@ -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)
|
||||
}
|
||||
51
internal/opencodegraph/chromatic_util.go
Normal file
51
internal/opencodegraph/chromatic_util.go
Normal file
@ -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()
|
||||
}
|
||||
2
internal/opencodegraph/doc.go
Normal file
2
internal/opencodegraph/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package opencodegraph provides OpenCodeGraph metadata about code.
|
||||
package opencodegraph
|
||||
104
internal/opencodegraph/docs_provider.go
Normal file
104
internal/opencodegraph/docs_provider.go
Normal file
@ -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",
|
||||
},
|
||||
}
|
||||
62
internal/opencodegraph/googledocs_provider.go
Normal file
62
internal/opencodegraph/googledocs_provider.go
Normal file
@ -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)
|
||||
}
|
||||
54
internal/opencodegraph/grafana_provider.go
Normal file
54
internal/opencodegraph/grafana_provider.go
Normal file
@ -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)
|
||||
}
|
||||
76
internal/opencodegraph/multi.go
Normal file
76
internal/opencodegraph/multi.go
Normal file
@ -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
|
||||
}
|
||||
27
internal/opencodegraph/protocol.go
Normal file
27
internal/opencodegraph/protocol.go
Normal file
@ -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
|
||||
}
|
||||
51
internal/opencodegraph/protocol_test.go
Normal file
51
internal/opencodegraph/protocol_test.go
Normal file
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
21
internal/opencodegraph/providers.go
Normal file
21
internal/opencodegraph/providers.go
Normal file
@ -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}
|
||||
210
internal/opencodegraph/storybook_provider.go
Normal file
210
internal/opencodegraph/storybook_provider.go
Normal file
@ -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
|
||||
}
|
||||
31
internal/opencodegraph/util.go
Normal file
31
internal/opencodegraph/util.go
Normal file
@ -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
|
||||
}
|
||||
@ -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",
|
||||
|
||||
147
pnpm-lock.yaml
147
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
|
||||
|
||||
@ -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"],
|
||||
|
||||
122
schema/opencodegraph-protocol.schema.json
Normal file
122
schema/opencodegraph-protocol.schema.json
Normal file
@ -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[]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
schema/opencodegraph.schema.json
Normal file
82
schema/opencodegraph.schema.json
Normal file
@ -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" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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}'.
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user