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
315 lines
6.9 KiB
Go
315 lines
6.9 KiB
Go
package core
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
defaultShowHeadLines = 80
|
|
defaultShowContextLines = 8
|
|
defaultShowMaxRange = 120
|
|
defaultShowMaxEntries = 50
|
|
)
|
|
|
|
type referenceViewMode string
|
|
|
|
const (
|
|
referenceViewFileHead referenceViewMode = "file_head"
|
|
referenceViewContext referenceViewMode = "context"
|
|
referenceViewRange referenceViewMode = "range"
|
|
referenceViewDir referenceViewMode = "dir"
|
|
)
|
|
|
|
type referenceViewRequest struct {
|
|
Ref *localReference
|
|
Mode referenceViewMode
|
|
Window int
|
|
MaxLines int
|
|
MaxEntries int
|
|
}
|
|
|
|
func buildReferenceViewRequest(rawRef, workspaceDir string) (*referenceViewRequest, error) {
|
|
ref, err := parseUserLocalReference(rawRef, workspaceDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ref.kind == referenceKindDir && ref.locationFormat != referenceLocationNone {
|
|
return nil, fmt.Errorf("directory reference cannot carry a location")
|
|
}
|
|
req := &referenceViewRequest{
|
|
Ref: ref,
|
|
Window: defaultShowContextLines,
|
|
MaxLines: defaultShowMaxRange,
|
|
MaxEntries: defaultShowMaxEntries,
|
|
}
|
|
switch {
|
|
case ref.kind == referenceKindDir:
|
|
req.Mode = referenceViewDir
|
|
case ref.locationFormat == referenceLocationColonRange:
|
|
req.Mode = referenceViewRange
|
|
case ref.locationFormat != referenceLocationNone:
|
|
req.Mode = referenceViewContext
|
|
default:
|
|
req.Mode = referenceViewFileHead
|
|
req.MaxLines = defaultShowHeadLines
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func renderReferenceView(req *referenceViewRequest) (string, error) {
|
|
if req == nil || req.Ref == nil {
|
|
return "", fmt.Errorf("nil view request")
|
|
}
|
|
path := req.Ref.pathAbs
|
|
if path == "" {
|
|
path = req.Ref.pathOriginal
|
|
}
|
|
if path == "" {
|
|
return "", fmt.Errorf("empty path")
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", fmt.Errorf("path does not exist")
|
|
}
|
|
return "", err
|
|
}
|
|
if info.IsDir() {
|
|
if req.Ref.locationFormat != referenceLocationNone {
|
|
return "", fmt.Errorf("directory reference cannot carry a location")
|
|
}
|
|
return renderReferenceDir(path, req)
|
|
}
|
|
return renderReferenceFile(path, req)
|
|
}
|
|
|
|
func renderReferenceFile(path string, req *referenceViewRequest) (string, error) {
|
|
var (
|
|
lines []string
|
|
truncated bool
|
|
err error
|
|
note string
|
|
)
|
|
|
|
switch req.Mode {
|
|
case referenceViewContext:
|
|
lines, _, err = readFileContext(path, req.Ref.lineStart, req.Window, req.Window, req.MaxLines)
|
|
case referenceViewRange:
|
|
lines, truncated, err = readFileRange(path, req.Ref.lineStart, req.Ref.lineEnd, req.MaxLines)
|
|
if truncated {
|
|
note = fmt.Sprintf("Only showing the first %d lines of the requested range.", req.MaxLines)
|
|
}
|
|
default:
|
|
lines, truncated, err = readFileHead(path, req.MaxLines)
|
|
if truncated {
|
|
note = fmt.Sprintf("Only showing the first %d lines.", req.MaxLines)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
title := showReferenceTitle(req.Ref)
|
|
if title == "" {
|
|
title = cleanDisplayPath(path)
|
|
}
|
|
var sb strings.Builder
|
|
sb.WriteString(title)
|
|
if note != "" {
|
|
sb.WriteString("\n")
|
|
sb.WriteString(note)
|
|
}
|
|
sb.WriteString("\n```")
|
|
if lang := codeFenceLanguage(path); lang != "" {
|
|
sb.WriteString(lang)
|
|
}
|
|
sb.WriteString("\n")
|
|
if len(lines) > 0 {
|
|
sb.WriteString(strings.Join(lines, "\n"))
|
|
sb.WriteString("\n")
|
|
}
|
|
sb.WriteString("```")
|
|
return sb.String(), nil
|
|
}
|
|
|
|
func renderReferenceDir(path string, req *referenceViewRequest) (string, error) {
|
|
entries, truncated, err := readDirEntries(path, req.MaxEntries)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
title := showReferenceTitle(req.Ref)
|
|
if title == "" {
|
|
title = "📁 " + appendDirSuffix(cleanDisplayPath(path), referenceKindDir)
|
|
}
|
|
var sb strings.Builder
|
|
sb.WriteString(title)
|
|
if len(entries) == 0 {
|
|
sb.WriteString("\n(empty)")
|
|
return sb.String(), nil
|
|
}
|
|
sb.WriteString("\n")
|
|
for _, entry := range entries {
|
|
sb.WriteString("- ")
|
|
sb.WriteString(entry)
|
|
sb.WriteString("\n")
|
|
}
|
|
if truncated {
|
|
sb.WriteString(fmt.Sprintf("\nOnly showing the first %d entries.", req.MaxEntries))
|
|
}
|
|
return strings.TrimRight(sb.String(), "\n"), nil
|
|
}
|
|
|
|
func showReferenceTitle(ref *localReference) string {
|
|
if ref == nil {
|
|
return ""
|
|
}
|
|
body := referenceDisplaySource(ref, "relative") + renderReferenceLocation(ref)
|
|
return applyReferenceMarker("emoji", ref.kind, body)
|
|
}
|
|
|
|
func readFileHead(path string, maxLines int) ([]string, bool, error) {
|
|
if maxLines <= 0 {
|
|
maxLines = defaultShowHeadLines
|
|
}
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
buf := make([]byte, 0, 64*1024)
|
|
scanner.Buffer(buf, 1024*1024)
|
|
|
|
lines := make([]string, 0, maxLines)
|
|
truncated := false
|
|
for scanner.Scan() {
|
|
if len(lines) >= maxLines {
|
|
truncated = true
|
|
break
|
|
}
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, false, err
|
|
}
|
|
return lines, truncated, nil
|
|
}
|
|
|
|
func readFileRange(path string, start, end, maxLines int) ([]string, bool, error) {
|
|
if start <= 0 {
|
|
return nil, false, fmt.Errorf("invalid start line")
|
|
}
|
|
if end <= 0 || end < start {
|
|
return nil, false, fmt.Errorf("invalid end line")
|
|
}
|
|
if maxLines <= 0 {
|
|
maxLines = defaultShowMaxRange
|
|
}
|
|
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
buf := make([]byte, 0, 64*1024)
|
|
scanner.Buffer(buf, 1024*1024)
|
|
|
|
lines := make([]string, 0, minInt(end-start+1, maxLines))
|
|
lineNo := 0
|
|
truncated := false
|
|
for scanner.Scan() {
|
|
lineNo++
|
|
if lineNo < start {
|
|
continue
|
|
}
|
|
if lineNo > end {
|
|
break
|
|
}
|
|
if len(lines) >= maxLines {
|
|
truncated = true
|
|
break
|
|
}
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, false, err
|
|
}
|
|
return lines, truncated, nil
|
|
}
|
|
|
|
func readFileContext(path string, line, before, after, maxLines int) ([]string, bool, error) {
|
|
if line <= 0 {
|
|
return nil, false, fmt.Errorf("invalid line")
|
|
}
|
|
start := line - before
|
|
if start < 1 {
|
|
start = 1
|
|
}
|
|
end := line + after
|
|
return readFileRange(path, start, end, maxLines)
|
|
}
|
|
|
|
func readDirEntries(path string, maxEntries int) ([]string, bool, error) {
|
|
if maxEntries <= 0 {
|
|
maxEntries = defaultShowMaxEntries
|
|
}
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
out := make([]string, 0, minInt(len(entries), maxEntries))
|
|
truncated := false
|
|
for i, entry := range entries {
|
|
if i >= maxEntries {
|
|
truncated = true
|
|
break
|
|
}
|
|
name := entry.Name()
|
|
if entry.IsDir() {
|
|
name += "/"
|
|
}
|
|
out = append(out, name)
|
|
}
|
|
return out, truncated, nil
|
|
}
|
|
|
|
func codeFenceLanguage(path string) string {
|
|
switch strings.ToLower(filepath.Ext(path)) {
|
|
case ".go":
|
|
return "go"
|
|
case ".ts":
|
|
return "ts"
|
|
case ".tsx":
|
|
return "tsx"
|
|
case ".js":
|
|
return "js"
|
|
case ".jsx":
|
|
return "jsx"
|
|
case ".py":
|
|
return "python"
|
|
case ".md":
|
|
return "markdown"
|
|
case ".json":
|
|
return "json"
|
|
case ".yaml", ".yml":
|
|
return "yaml"
|
|
case ".sh":
|
|
return "bash"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func minInt(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|