fix integration
This commit is contained in:
@@ -6,11 +6,19 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
sourcegraph_service "code.gitea.io/gitea/services/sourcegraph"
|
sourcegraph_service "code.gitea.io/gitea/services/sourcegraph"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// sourcegraphRepoName returns the full repository name for Sourcegraph,
|
||||||
|
// which includes the server domain prefix (e.g., "git.example.com/owner/repo")
|
||||||
|
func sourcegraphRepoName(ctx *context.APIContext) string {
|
||||||
|
return setting.Domain + "/" + ctx.Repo.Repository.FullName()
|
||||||
|
}
|
||||||
|
|
||||||
// SourcegraphHover returns hover information at a position
|
// SourcegraphHover returns hover information at a position
|
||||||
func SourcegraphHover(ctx *context.APIContext) {
|
func SourcegraphHover(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /repos/{owner}/{repo}/sourcegraph/hover repository repoSourcegraphHover
|
// swagger:operation GET /repos/{owner}/{repo}/sourcegraph/hover repository repoSourcegraphHover
|
||||||
@@ -80,7 +88,7 @@ func SourcegraphHover(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := client.Hover(ctx, ctx.Repo.Repository.FullName(), ref, path, line, char)
|
result, err := client.Hover(ctx, sourcegraphRepoName(ctx), ref, path, line, char)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIError(http.StatusBadGateway, err)
|
ctx.APIError(http.StatusBadGateway, err)
|
||||||
return
|
return
|
||||||
@@ -91,6 +99,14 @@ func SourcegraphHover(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render markdown content to HTML
|
||||||
|
if result.Contents != "" {
|
||||||
|
rendered, err := markdown.RenderString(markup.NewRenderContext(ctx).WithMetas(markup.ComposeSimpleDocumentMetas()), result.Contents)
|
||||||
|
if err == nil {
|
||||||
|
result.Contents = string(rendered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, result)
|
ctx.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +179,7 @@ func SourcegraphDefinition(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := client.Definition(ctx, ctx.Repo.Repository.FullName(), ref, path, line, char)
|
result, err := client.Definition(ctx, sourcegraphRepoName(ctx), ref, path, line, char)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIError(http.StatusBadGateway, err)
|
ctx.APIError(http.StatusBadGateway, err)
|
||||||
return
|
return
|
||||||
@@ -245,7 +261,7 @@ func SourcegraphReferences(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := client.References(ctx, ctx.Repo.Repository.FullName(), ref, path, line, char)
|
result, err := client.References(ctx, sourcegraphRepoName(ctx), ref, path, line, char)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIError(http.StatusBadGateway, err)
|
ctx.APIError(http.StatusBadGateway, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ func (c *Client) graphqlRequest(ctx context.Context, query string, variables map
|
|||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug("Sourcegraph GraphQL request to %s: variables=%v", c.baseURL+"/.api/graphql", variables)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/.api/graphql", bytes.NewReader(bodyBytes))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/.api/graphql", bytes.NewReader(bodyBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
@@ -110,9 +112,12 @@ func (c *Client) graphqlRequest(ctx context.Context, query string, variables map
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Warn("Sourcegraph GraphQL error status %d: %s", resp.StatusCode, string(respBody))
|
||||||
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody))
|
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug("Sourcegraph GraphQL response: %s", string(respBody))
|
||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
Data json.RawMessage `json:"data"`
|
Data json.RawMessage `json:"data"`
|
||||||
Errors []struct {
|
Errors []struct {
|
||||||
@@ -124,6 +129,7 @@ func (c *Client) graphqlRequest(ctx context.Context, query string, variables map
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(result.Errors) > 0 {
|
if len(result.Errors) > 0 {
|
||||||
|
log.Warn("Sourcegraph GraphQL errors: %v", result.Errors)
|
||||||
return nil, fmt.Errorf("graphql error: %s", result.Errors[0].Message)
|
return nil, fmt.Errorf("graphql error: %s", result.Errors[0].Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,10 +145,13 @@ func cacheKey(queryType, repo, rev, path string, line, char int) string {
|
|||||||
func (c *Client) Hover(ctx context.Context, repo, rev, path string, line, char int) (*HoverResult, error) {
|
func (c *Client) Hover(ctx context.Context, repo, rev, path string, line, char int) (*HoverResult, error) {
|
||||||
key := cacheKey("hover", repo, rev, path, line, char)
|
key := cacheKey("hover", repo, rev, path, line, char)
|
||||||
|
|
||||||
|
log.Debug("Sourcegraph Hover request: repo=%s rev=%s path=%s line=%d char=%d", repo, rev, path, line, char)
|
||||||
|
|
||||||
// Try cache first
|
// Try cache first
|
||||||
if cached, hit := cache.GetCache().Get(key); hit {
|
if cached, hit := cache.GetCache().Get(key); hit {
|
||||||
var result HoverResult
|
var result HoverResult
|
||||||
if err := json.Unmarshal([]byte(cached), &result); err == nil {
|
if err := json.Unmarshal([]byte(cached), &result); err == nil {
|
||||||
|
log.Debug("Sourcegraph Hover cache hit")
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,16 @@
|
|||||||
.sourcegraph-hover {
|
.sourcegraph-hover {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow: auto;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourcegraph-hover .sg-content {
|
.sourcegraph-hover .sg-content {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0; /* Allow shrinking */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourcegraph-hover .sg-content pre {
|
.sourcegraph-hover .sg-content pre {
|
||||||
|
|||||||
@@ -94,23 +94,25 @@ function hideRefsPanel(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createHoverContent(
|
async function createHoverContent(
|
||||||
contents: string,
|
contents: string,
|
||||||
config: SourcegraphConfig,
|
config: SourcegraphConfig,
|
||||||
path: string,
|
path: string,
|
||||||
line: number,
|
line: number,
|
||||||
char: number,
|
char: number,
|
||||||
): HTMLElement {
|
): Promise<HTMLElement> {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'sourcegraph-hover';
|
el.className = 'sourcegraph-hover';
|
||||||
|
|
||||||
// Render markdown content as pre-formatted text (simple approach)
|
// Content is pre-rendered as HTML by the backend
|
||||||
// In production, you might want to use a proper markdown renderer
|
|
||||||
const contentDiv = document.createElement('div');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.className = 'sg-content';
|
contentDiv.className = 'sg-content markup';
|
||||||
contentDiv.innerHTML = `<pre>${escapeHtml(contents)}</pre>`;
|
contentDiv.innerHTML = contents;
|
||||||
el.appendChild(contentDiv);
|
el.appendChild(contentDiv);
|
||||||
|
|
||||||
|
// Pre-fetch definitions to know if button should be enabled
|
||||||
|
const definitions = await fetchDefinition(config, path, line, char);
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
const actionsDiv = document.createElement('div');
|
const actionsDiv = document.createElement('div');
|
||||||
actionsDiv.className = 'sg-actions';
|
actionsDiv.className = 'sg-actions';
|
||||||
@@ -118,15 +120,19 @@ function createHoverContent(
|
|||||||
const goToDefBtn = document.createElement('button');
|
const goToDefBtn = document.createElement('button');
|
||||||
goToDefBtn.className = 'ui mini basic button';
|
goToDefBtn.className = 'ui mini basic button';
|
||||||
goToDefBtn.textContent = 'Go to definition';
|
goToDefBtn.textContent = 'Go to definition';
|
||||||
goToDefBtn.addEventListener('click', async () => {
|
if (definitions.length === 0) {
|
||||||
hideActiveTippy();
|
goToDefBtn.disabled = true;
|
||||||
const locations = await fetchDefinition(config, path, line, char);
|
goToDefBtn.classList.add('disabled');
|
||||||
if (locations.length === 1) {
|
} else {
|
||||||
navigateToLocation(locations[0]);
|
goToDefBtn.addEventListener('click', () => {
|
||||||
} else if (locations.length > 1) {
|
hideActiveTippy();
|
||||||
showLocationsPanel('Definitions', locations);
|
if (definitions.length === 1) {
|
||||||
}
|
navigateToLocation(definitions[0]);
|
||||||
});
|
} else {
|
||||||
|
showLocationsPanel('Definitions', definitions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
actionsDiv.appendChild(goToDefBtn);
|
actionsDiv.appendChild(goToDefBtn);
|
||||||
|
|
||||||
const findRefsBtn = document.createElement('button');
|
const findRefsBtn = document.createElement('button');
|
||||||
@@ -151,6 +157,17 @@ function escapeHtml(text: string): string {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRepoName(repo: string): string {
|
||||||
|
// Sourcegraph returns repo names like "git.example.com/owner/repo"
|
||||||
|
// We need just "owner/repo" for Gitea URLs
|
||||||
|
const parts = repo.split('/');
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
// Has domain prefix - return last two parts (owner/repo)
|
||||||
|
return parts.slice(-2).join('/');
|
||||||
|
}
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
|
||||||
function navigateToLocation(loc: Location): void {
|
function navigateToLocation(loc: Location): void {
|
||||||
// Build URL to the location
|
// Build URL to the location
|
||||||
// If it's in the same repo, navigate to file
|
// If it's in the same repo, navigate to file
|
||||||
@@ -159,14 +176,17 @@ function navigateToLocation(loc: Location): void {
|
|||||||
const repoMatch = currentPath.match(/^\/([^/]+\/[^/]+)/);
|
const repoMatch = currentPath.match(/^\/([^/]+\/[^/]+)/);
|
||||||
const currentRepo = repoMatch ? repoMatch[1] : '';
|
const currentRepo = repoMatch ? repoMatch[1] : '';
|
||||||
|
|
||||||
|
// Normalize repo name (strip domain prefix if present)
|
||||||
|
const targetRepo = normalizeRepoName(loc.repo || '');
|
||||||
|
|
||||||
let url: string;
|
let url: string;
|
||||||
if (loc.repo === currentRepo || !loc.repo) {
|
if (targetRepo === currentRepo || !targetRepo) {
|
||||||
// Same repo - construct relative URL
|
// Same repo - construct relative URL
|
||||||
const basePath = currentPath.replace(/\/src\/.*$/, '');
|
const basePath = currentPath.replace(/\/src\/.*$/, '');
|
||||||
url = `${basePath}/src/branch/main/${loc.path}#L${loc.line + 1}`;
|
url = `${basePath}/src/branch/main/${loc.path}#L${loc.line + 1}`;
|
||||||
} else {
|
} else {
|
||||||
// Different repo
|
// Different repo
|
||||||
url = `/${loc.repo}/src/branch/main/${loc.path}#L${loc.line + 1}`;
|
url = `/${targetRepo}/src/branch/main/${loc.path}#L${loc.line + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
@@ -298,7 +318,7 @@ export function initSourcegraph(): void {
|
|||||||
hideActiveTippy();
|
hideActiveTippy();
|
||||||
|
|
||||||
// Create and show new tippy
|
// Create and show new tippy
|
||||||
const content = createHoverContent(hover.contents, config, pos.path, pos.line, pos.char);
|
const content = await createHoverContent(hover.contents, config, pos.path, pos.line, pos.char);
|
||||||
activeTippy = createTippy(target, {
|
activeTippy = createTippy(target, {
|
||||||
content,
|
content,
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
@@ -324,11 +344,22 @@ export function initSourcegraph(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close refs panel when clicking outside
|
// Close tippy and refs panel when clicking outside
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (refsPanel && !refsPanel.contains(e.target as Node)) {
|
const target = e.target as Node;
|
||||||
|
|
||||||
|
// Close refs panel if clicking outside it
|
||||||
|
if (refsPanel && !refsPanel.contains(target)) {
|
||||||
hideRefsPanel();
|
hideRefsPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close tippy if clicking outside it
|
||||||
|
if (activeTippy) {
|
||||||
|
const tippyBox = document.querySelector('.tippy-box');
|
||||||
|
if (tippyBox && !tippyBox.contains(target)) {
|
||||||
|
hideActiveTippy();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user