mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-07-03 12:28:10 +08:00
* feat(references): normalize and render local agent references * feat(show): add reference-aware file and directory viewing * docs: document references config and show usage * chore: remove local working docs from PR branch * fix: address lint issues in reference pipeline * fix: drop stale quiet leftovers after main rebase * fix: clean remaining rebase markers in show tests
197 lines
5.1 KiB
Go
197 lines
5.1 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
type referenceKind string
|
|
|
|
const (
|
|
referenceKindUnknown referenceKind = "unknown"
|
|
referenceKindFile referenceKind = "file"
|
|
referenceKindDir referenceKind = "dir"
|
|
)
|
|
|
|
type referenceLocationFormat string
|
|
|
|
const (
|
|
referenceLocationNone referenceLocationFormat = ""
|
|
referenceLocationColonLine referenceLocationFormat = "colon_line"
|
|
referenceLocationColonLineCol referenceLocationFormat = "colon_line_col"
|
|
referenceLocationColonRange referenceLocationFormat = "colon_line_range"
|
|
referenceLocationHashLine referenceLocationFormat = "hash_line"
|
|
referenceLocationHashLineCol referenceLocationFormat = "hash_line_col"
|
|
)
|
|
|
|
type localReference struct {
|
|
kind referenceKind
|
|
raw string
|
|
pathOriginal string
|
|
pathAbs string
|
|
pathRel string
|
|
isRelative bool
|
|
locationFormat referenceLocationFormat
|
|
lineStart int
|
|
lineEnd int
|
|
column int
|
|
}
|
|
|
|
var (
|
|
reMarkdownLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)\s]+)\)((?::\d+(?::\d+)?|:\d+-\d+)?)?`)
|
|
reHashLocation = regexp.MustCompile(`^(.*?)(#L(\d+)(?:C(\d+))?)$`)
|
|
reColonLineCol = regexp.MustCompile(`^(.*):(\d+):(\d+)$`)
|
|
reColonLineRange = regexp.MustCompile(`^(.*):(\d+)-(\d+)$`)
|
|
reColonLineOnly = regexp.MustCompile(`^(.*):(\d+)$`)
|
|
)
|
|
|
|
func parseUserLocalReference(raw, workspaceDir string) (*localReference, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil, fmt.Errorf("empty reference")
|
|
}
|
|
if match := reMarkdownLink.FindStringSubmatch(raw); len(match) >= 3 && match[0] == raw {
|
|
suffix := ""
|
|
if len(match) >= 4 {
|
|
suffix = match[3]
|
|
}
|
|
raw = match[2] + suffix
|
|
}
|
|
ref, ok := parseLocalReference(raw, workspaceDir)
|
|
if !ok {
|
|
return nil, fmt.Errorf("cannot parse local reference")
|
|
}
|
|
return ref, nil
|
|
}
|
|
|
|
func parseLocalReference(raw, workspaceDir string) (*localReference, bool) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" || isWebURL(raw) || strings.HasPrefix(raw, "//") {
|
|
return nil, false
|
|
}
|
|
ref := &localReference{raw: raw}
|
|
pathPart := raw
|
|
switch {
|
|
case reHashLocation.MatchString(pathPart):
|
|
m := reHashLocation.FindStringSubmatch(pathPart)
|
|
pathPart = m[1]
|
|
ref.lineStart = atoiSafe(m[3])
|
|
ref.column = atoiSafe(m[4])
|
|
if ref.column > 0 {
|
|
ref.locationFormat = referenceLocationHashLineCol
|
|
} else {
|
|
ref.locationFormat = referenceLocationHashLine
|
|
}
|
|
case reColonLineCol.MatchString(pathPart):
|
|
m := reColonLineCol.FindStringSubmatch(pathPart)
|
|
pathPart = m[1]
|
|
ref.lineStart = atoiSafe(m[2])
|
|
ref.column = atoiSafe(m[3])
|
|
ref.locationFormat = referenceLocationColonLineCol
|
|
case reColonLineRange.MatchString(pathPart):
|
|
m := reColonLineRange.FindStringSubmatch(pathPart)
|
|
pathPart = m[1]
|
|
ref.lineStart = atoiSafe(m[2])
|
|
ref.lineEnd = atoiSafe(m[3])
|
|
ref.locationFormat = referenceLocationColonRange
|
|
case reColonLineOnly.MatchString(pathPart):
|
|
m := reColonLineOnly.FindStringSubmatch(pathPart)
|
|
pathPart = m[1]
|
|
ref.lineStart = atoiSafe(m[2])
|
|
ref.locationFormat = referenceLocationColonLine
|
|
}
|
|
if strings.HasPrefix(pathPart, "file://") {
|
|
u, err := url.Parse(pathPart)
|
|
if err != nil || u.Path == "" {
|
|
return nil, false
|
|
}
|
|
pathPart = u.Path
|
|
}
|
|
if !looksLikeLocalPath(pathPart) {
|
|
return nil, false
|
|
}
|
|
ref.pathOriginal = pathPart
|
|
ref.isRelative = !filepath.IsAbs(pathPart)
|
|
if ref.isRelative {
|
|
if workspaceDir != "" {
|
|
ref.pathAbs = filepath.Clean(filepath.Join(workspaceDir, pathPart))
|
|
if rel, err := filepath.Rel(workspaceDir, ref.pathAbs); err == nil {
|
|
ref.pathRel = filepath.ToSlash(rel)
|
|
}
|
|
}
|
|
} else {
|
|
ref.pathAbs = filepath.Clean(pathPart)
|
|
if workspaceDir != "" {
|
|
if rel, err := filepath.Rel(workspaceDir, ref.pathAbs); err == nil {
|
|
ref.pathRel = filepath.ToSlash(rel)
|
|
}
|
|
}
|
|
}
|
|
ref.kind = inferReferenceKind(ref)
|
|
return ref, true
|
|
}
|
|
|
|
func inferReferenceKind(ref *localReference) referenceKind {
|
|
if ref == nil {
|
|
return referenceKindUnknown
|
|
}
|
|
if ref.pathAbs != "" {
|
|
if info, err := os.Stat(ref.pathAbs); err == nil {
|
|
if info.IsDir() {
|
|
return referenceKindDir
|
|
}
|
|
return referenceKindFile
|
|
}
|
|
}
|
|
if ref.locationFormat != referenceLocationNone {
|
|
return referenceKindFile
|
|
}
|
|
if strings.HasSuffix(ref.pathOriginal, "/") {
|
|
return referenceKindDir
|
|
}
|
|
base := filepath.Base(strings.TrimSuffix(ref.pathOriginal, "/"))
|
|
if filepath.Ext(base) != "" {
|
|
return referenceKindFile
|
|
}
|
|
return referenceKindUnknown
|
|
}
|
|
|
|
func looksLikeLocalPath(path string) bool {
|
|
if path == "" || strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "//") {
|
|
return false
|
|
}
|
|
switch {
|
|
case strings.HasPrefix(path, "/"):
|
|
return true
|
|
case strings.HasPrefix(path, "./"), strings.HasPrefix(path, "../"):
|
|
return true
|
|
case strings.Contains(path, "/"):
|
|
return true
|
|
default:
|
|
base := filepath.Base(path)
|
|
return strings.Contains(base, ".")
|
|
}
|
|
}
|
|
|
|
func isWebURL(s string) bool {
|
|
return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://")
|
|
}
|
|
|
|
func atoiSafe(s string) int {
|
|
if s == "" {
|
|
return 0
|
|
}
|
|
var n int
|
|
for _, r := range s {
|
|
if r < '0' || r > '9' {
|
|
return 0
|
|
}
|
|
n = n*10 + int(r-'0')
|
|
}
|
|
return n
|
|
}
|