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:
Quinn Slack 2023-12-06 21:39:33 -08:00 committed by GitHub
parent dccbed6be2
commit c9439d9456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1509 additions and 25 deletions

View File

@ -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 = [

View File

@ -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": {

View File

@ -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)[]

View File

@ -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",

View File

@ -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),
}),

View File

@ -48,4 +48,4 @@
"@sourcegraph/telemetry": "^0.11.0",
"@sourcegraph/wildcard": "workspace:*"
}
}
}

View File

@ -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]

View File

@ -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>
)
}

View File

@ -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')
}

View File

@ -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>

View File

@ -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").

View File

@ -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': {

View File

@ -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",

View File

@ -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

View 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
}
}

View 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"],
)

View 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)
}

View 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()
}

View File

@ -0,0 +1,2 @@
// Package opencodegraph provides OpenCodeGraph metadata about code.
package opencodegraph

View 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",
},
}

View 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)
}

View 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)
}

View 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
}

View 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
}

View 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)
}
})
}
}

View 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}

View 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
}

View 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
}

View File

@ -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",

View File

@ -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

View File

@ -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"],

View 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[]"
}
}
}
}
}

View 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" } }
}
}
}

View File

@ -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}'.

View File

@ -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