diff --git a/cmd/olliesrv/main.go b/cmd/olliesrv/main.go index 88e33a6..830f718 100644 --- a/cmd/olliesrv/main.go +++ b/cmd/olliesrv/main.go @@ -13,9 +13,9 @@ import ( "ollie/cmd/olliesrv/internal/agent" "ollie/cmd/olliesrv/internal/fs" - "ollie/env" olog "ollie/log" "ollie/paths" + "ollie/util" "9fans.net/go/plan9/client" @@ -41,7 +41,7 @@ func main() { } func runServer(sockPath string) { - env.EnsureDefaults() + util.EnsureEnv() // Remove stale sockets from this and prior runs if _, err := os.Stat(sockPath); err == nil { diff --git a/cmd/toolsrv/main.go b/cmd/toolsrv/main.go index fb114e3..b244486 100644 --- a/cmd/toolsrv/main.go +++ b/cmd/toolsrv/main.go @@ -28,8 +28,8 @@ import ( "ollie/cmd/toolsrv/internal/registry" "ollie/cmd/toolsrv/internal/server" - "ollie/env" lib9p "ollie/lib9p/client" + "ollie/util" olog "ollie/log" "ollie/paths" @@ -73,7 +73,7 @@ func main() { defer sink.Flush() logger = sink.NewLogger("toolsrv") - env.EnsureDefaults() + util.EnsureEnv() // Prepend our bin dir to PATH so landrun is found (deployed alongside us). home, _ := os.UserHomeDir() diff --git a/env/env.go b/env/env.go index 761e1c7..876ec39 100644 --- a/env/env.go +++ b/env/env.go @@ -1,52 +1,8 @@ -// Package env manages the daemon-global environment for ollie. -// It loads $XDG_CONFIG_HOME/ollie/env and provides defaults. +// Package env is retained for backward compatibility. +// New code should import ollie/util. package env -import ( - "bufio" - "os" - "path/filepath" - "strings" +import "ollie/util" - "ollie/paths" -) - -// EnsureDefaults loads $XDG_CONFIG_HOME/ollie/env and sets default values for any -// OLLIE_* vars not already present in the process environment. -func EnsureDefaults() { - loadFile(filepath.Join(paths.CfgDir(), "env")) -} - -// loadFile reads KEY=VALUE pairs from path and sets any key not already -// present in the environment. Lines beginning with # and blank lines are -// ignored. Missing file is silently ignored. -func loadFile(path string) { - f, err := os.Open(path) - if err != nil { - return - } - defer f.Close() - - sc := bufio.NewScanner(f) - for sc.Scan() { - line := strings.TrimSpace(sc.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - k, v, ok := strings.Cut(line, "=") - if !ok { - continue - } - k = strings.TrimSpace(k) - if i := strings.Index(v, " #"); i >= 0 { - v = v[:i] - } - v = strings.TrimSpace(v) - if len(v) >= 2 && (v[0] == '"' && v[len(v)-1] == '"' || v[0] == '\'' && v[len(v)-1] == '\'') { - v = v[1 : len(v)-1] - } - if k != "" && os.Getenv(k) == "" { - os.Setenv(k, paths.ExpandHome(os.ExpandEnv(v))) //nolint:errcheck - } - } -} +// EnsureDefaults loads the configured environment file and sets defaults. +func EnsureDefaults() { util.EnsureEnv() } diff --git a/env/env_test.go b/env/env_test.go deleted file mode 100644 index ffda571..0000000 --- a/env/env_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package env - -import ( - "os" - "testing" -) - -func TestEnsureDefaults(t *testing.T) { - EnsureDefaults() -} - -func TestLoadFile(t *testing.T) { - dir := t.TempDir() - f := dir + "/env" - os.WriteFile(f, []byte("OLLIE_TEST_LOAD=testval\n"), 0600) - t.Setenv("OLLIE_TEST_LOAD", "") - os.Unsetenv("OLLIE_TEST_LOAD") - loadFile(f) - if got := os.Getenv("OLLIE_TEST_LOAD"); got != "testval" { - t.Errorf("loadFile: OLLIE_TEST_LOAD = %q, want testval", got) - } -} diff --git a/lib9p/lib9p.go b/lib9p/lib9p.go index 3d83567..1c2bc3b 100644 --- a/lib9p/lib9p.go +++ b/lib9p/lib9p.go @@ -36,12 +36,12 @@ type fidHandle struct { var ( handleMu sync.Mutex - handles = make(map[C.int]*connHandle) + handles = make(map[C.int]*connHandle) nextHandle C.int = 1 - fidMu sync.Mutex - fids = make(map[C.int]*fidHandle) - nextFid C.int = 1 + fidMu sync.Mutex + fids = make(map[C.int]*fidHandle) + nextFid C.int = 1 ) func allocHandle(h *connHandle) C.int { diff --git a/paths/uuid.go b/paths/uuid.go index 9c6758c..e2c295c 100644 --- a/paths/uuid.go +++ b/paths/uuid.go @@ -8,7 +8,7 @@ import ( // NewUUID generates a random UUIDv4 string. func NewUUID() string { var b [16]byte - rand.Read(b[:]) //nolint:errcheck + rand.Read(b[:]) //nolint:errcheck b[6] = (b[6] & 0x0f) | 0x40 // version 4 b[8] = (b[8] & 0x3f) | 0x80 // variant 10xx return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", diff --git a/tools/codeintel/cmd/code_symbols/main.go b/tools/codeintel/cmd/code_symbols/main.go index 7dcc361..e616db2 100644 --- a/tools/codeintel/cmd/code_symbols/main.go +++ b/tools/codeintel/cmd/code_symbols/main.go @@ -9,7 +9,8 @@ import ( ) type args struct { - Path, Query string `json:"path"` + Path string `json:"path"` + Query string `json:"query"` } func main() { diff --git a/tools/filetools/internal/filetools/filetools.go b/tools/filetools/internal/filetools/filetools.go index 213ce76..5809b99 100644 --- a/tools/filetools/internal/filetools/filetools.go +++ b/tools/filetools/internal/filetools/filetools.go @@ -162,19 +162,126 @@ func Read(m map[string]any) (string, error) { } return status + "\n" + strings.TrimSuffix(b.String(), "\n"), nil } + +type diffOp struct { + kind byte + line string +} + func Diff(path, old, new string) string { - a := strings.SplitAfter(old, "\n") - b := strings.SplitAfter(new, "\n") + oldLines := diffLines(old) + newLines := diffLines(new) + ops := diffOps(oldLines, newLines) + + var changed []int + for i, op := range ops { + if op.kind != ' ' { + changed = append(changed, i) + } + } + if len(changed) == 0 { + return "" + } + + const context = 3 + type hunk struct{ start, end int } + var hunks []hunk + for _, i := range changed { + start := i - context + if start < 0 { + start = 0 + } + end := i + context + 1 + if len(hunks) > 0 && start <= hunks[len(hunks)-1].end { + if end > hunks[len(hunks)-1].end { + hunks[len(hunks)-1].end = end + } + continue + } + hunks = append(hunks, hunk{start: start, end: end}) + } + for i := range hunks { + if hunks[i].end > len(ops) { + hunks[i].end = len(ops) + } + } + var out strings.Builder fmt.Fprintf(&out, "--- %s\n+++ %s\n", path, path) - for _, x := range a { - fmt.Fprintf(&out, "-%s", x) - } - for _, x := range b { - fmt.Fprintf(&out, "+%s", x) + for _, h := range hunks { + oldStart, newStart := 1, 1 + for _, op := range ops[:h.start] { + if op.kind != '+' { + oldStart++ + } + if op.kind != '-' { + newStart++ + } + } + oldCount, newCount := 0, 0 + for _, op := range ops[h.start:h.end] { + if op.kind != '+' { + oldCount++ + } + if op.kind != '-' { + newCount++ + } + } + fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", oldStart, oldCount, newStart, newCount) + for _, op := range ops[h.start:h.end] { + out.WriteByte(op.kind) + out.WriteString(op.line) + if !strings.HasSuffix(op.line, "\n") { + out.WriteByte('\n') + } + } } return out.String() } + +func diffLines(s string) []string { + if s == "" { + return nil + } + lines := strings.SplitAfter(s, "\n") + if lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } + return lines +} + +func diffOps(a, b []string) []diffOp { + lcs := make([][]int, len(a)+1) + for i := range lcs { + lcs[i] = make([]int, len(b)+1) + } + for i := len(a) - 1; i >= 0; i-- { + for j := len(b) - 1; j >= 0; j-- { + if a[i] == b[j] { + lcs[i][j] = lcs[i+1][j+1] + 1 + } else if lcs[i+1][j] >= lcs[i][j+1] { + lcs[i][j] = lcs[i+1][j] + } else { + lcs[i][j] = lcs[i][j+1] + } + } + } + ops := make([]diffOp, 0, len(a)+len(b)-lcs[0][0]) + for i, j := 0, 0; i < len(a) || j < len(b); { + if i < len(a) && j < len(b) && a[i] == b[j] { + ops = append(ops, diffOp{' ', a[i]}) + i++ + j++ + } else if j == len(b) || (i < len(a) && lcs[i+1][j] >= lcs[i][j+1]) { + ops = append(ops, diffOp{'-', a[i]}) + i++ + } else { + ops = append(ops, diffOp{'+', b[j]}) + j++ + } + } + return ops +} func Edit(m map[string]any) (string, error) { p, e := Arg(m, "path") if e != nil { diff --git a/tools/filetools/internal/filetools/filetools_test.go b/tools/filetools/internal/filetools/filetools_test.go index a32cc3b..f66e1ef 100644 --- a/tools/filetools/internal/filetools/filetools_test.go +++ b/tools/filetools/internal/filetools/filetools_test.go @@ -45,6 +45,27 @@ func TestWriteAndEdit(t *testing.T) { t.Fatal(err) } } +func TestDiffUsesContextHunks(t *testing.T) { + old := "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n" + new := "one\ntwo\nTHREE\nfour\nfive\nsix\nseven\nEIGHT\n" + got := Diff("x", old, new) + want := "--- x\n+++ x\n@@ -1,8 +1,8 @@\n one\n two\n-three\n+THREE\n four\n five\n six\n seven\n-eight\n+EIGHT\n" + if got != want { + t.Fatalf("diff = %q, want %q", got, want) + } +} + +func TestDiffOmitsUnchangedFileContent(t *testing.T) { + old := "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n" + new := "line 1\nline 2\nline 3\nline 4\nchanged\nline 6\nline 7\nline 8\nline 9\nline 10\n" + got := Diff("x", old, new) + if strings.Contains(got, "line 1\n") || strings.Contains(got, "line 10\n") { + t.Fatalf("diff contains distant unchanged content: %q", got) + } + if !strings.Contains(got, "-line 5\n+changed\n line 6\n") { + t.Fatalf("diff lacks changed hunk: %q", got) + } +} func TestGlobAndGrep(t *testing.T) { d := t.TempDir() os.MkdirAll(filepath.Join(d, "sub"), 0755) diff --git a/toolsrv/client/client.go b/toolsrv/client/client.go index 8eb9a3a..bf53b9a 100644 --- a/toolsrv/client/client.go +++ b/toolsrv/client/client.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "io" + "strings" "syscall" "9fans.net/go/plan9" @@ -158,18 +159,19 @@ func (c *Conn) CallTool(ctx context.Context, name string, args json.RawMessage) defer fid.Close() // Convert JSON args to key=value format - var argMap map[string]interface{} + var argMap map[string]any if err := json.Unmarshal(args, &argMap); err != nil { - argMap = make(map[string]interface{}) + argMap = make(map[string]any) } - payload := fmt.Sprintf("token=%s\ntool=%s\nagent=%s\n", c.token, name, c.agentID) + var payload strings.Builder + fmt.Fprintf(&payload, "token=%s\ntool=%s\nagent=%s\n", c.token, name, c.agentID) for k, v := range argMap { escaped := escapeValue(fmt.Sprintf("%v", v)) - payload += fmt.Sprintf("%s=%s\n", k, escaped) + fmt.Fprintf(&payload, "%s=%s\n", k, escaped) } - if _, err := fid.Write([]byte(payload)); err != nil { + if _, err := fid.Write([]byte(payload.String())); err != nil { if ctx.Err() != nil { return nil, ctx.Err() } @@ -209,18 +211,19 @@ func (c *Conn) CallToolBackground(name string, args json.RawMessage) (int, error } defer fid.Close() - var argMap map[string]interface{} + var argMap map[string]any if err := json.Unmarshal(args, &argMap); err != nil { - argMap = make(map[string]interface{}) + argMap = make(map[string]any) } - payload := fmt.Sprintf("token=%s\ntool=%s\nagent=%s\n", c.token, name, c.agentID) + var payload strings.Builder + fmt.Fprintf(&payload, "token=%s\ntool=%s\nagent=%s\n", c.token, name, c.agentID) for k, v := range argMap { escaped := escapeValue(fmt.Sprintf("%v", v)) - payload += fmt.Sprintf("%s=%s\n", k, escaped) + fmt.Fprintf(&payload, "%s=%s\n", k, escaped) } - if _, err := fid.Write([]byte(payload)); err != nil { + if _, err := fid.Write([]byte(payload.String())); err != nil { return 0, fmt.Errorf("write: %w", err) } @@ -319,7 +322,7 @@ func (c *Conn) SetEnv(key, value string) error { return err } defer fid.Close() - _, err = fid.Write([]byte(fmt.Sprintf("env %s=%s\n", key, value))) + _, err = fmt.Fprintf(fid, "env %s=%s\n", key, value) return err } @@ -330,7 +333,7 @@ func (c *Conn) SetCWD(dir string) error { return err } defer fid.Close() - _, err = fid.Write([]byte(fmt.Sprintf("cwd %s\n", dir))) + _, err = fmt.Fprintf(fid, "cwd %s\n", dir) return err } @@ -447,7 +450,7 @@ func (c *Conn) SignalDetached(pid int, sig syscall.Signal) error { return err } defer fid.Close() - _, err = fid.Write([]byte(fmt.Sprintf("signal %d\n", sig))) + _, err = fmt.Fprintf(fid, "signal %d\n", sig) return err } diff --git a/util/env.go b/util/env.go new file mode 100644 index 0000000..acf21d5 --- /dev/null +++ b/util/env.go @@ -0,0 +1,45 @@ +// Package util manages the daemon-global environment for ollie. +// It loads $XDG_CONFIG_HOME/ollie/env and provides defaults. +package util + +import ( + "bufio" + "os" + "path/filepath" + "strings" + + "ollie/paths" +) + +// EnsureEnv loads $XDG_CONFIG_HOME/ollie/env and sets default values for any +// OLLIE_* vars not already present in the process environment. +func EnsureEnv() { + f, err := os.Open(filepath.Join(paths.CfgDir(), "env")) + if err != nil { + return + } + defer f.Close() + + sc := bufio.NewScanner(f) + for sc.Scan() { + line := strings.TrimSpace(sc.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + k, v, ok := strings.Cut(line, "=") + if !ok { + continue + } + k = strings.TrimSpace(k) + if i := strings.Index(v, " #"); i >= 0 { + v = v[:i] + } + v = strings.TrimSpace(v) + if len(v) >= 2 && (v[0] == '"' && v[len(v)-1] == '"' || v[0] == '\'' && v[len(v)-1] == '\'') { + v = v[1 : len(v)-1] + } + if k != "" && os.Getenv(k) == "" { + os.Setenv(k, paths.ExpandHome(os.ExpandEnv(v))) //nolint:errcheck + } + } +} diff --git a/virtfs/decl.go b/virtfs/decl.go index 75bbd4d..988568a 100644 --- a/virtfs/decl.go +++ b/virtfs/decl.go @@ -52,8 +52,8 @@ type FsNodeDecl struct { Stat func() os.FileInfo // Twalk — children (implies directory; at most one set). - Children []FsNodeDecl // static, known at compile time - Bindings func() ([]FsNodeDecl, error) // Each: dynamic children + Children []FsNodeDecl // static, known at compile time + Bindings func() ([]FsNodeDecl, error) // Each: dynamic children // Tread (non-blocking) + Twrite. Read func() ([]byte, error)