fix: sync shell working directory for suggestions (#78)
## Summary - synchronize the shell current working directory with Iris over the existing IPC pipe - use the synchronized directory for file, workspace, Git, and history suggestions on macOS - preserve the Linux `/proc/<pid>/cwd` and process-CWD fallbacks - update zsh, bash, and fish integrations while preserving command exit status ## Problem On macOS, `/proc/<pid>/cwd` is unavailable, so `GetCWD` fell back to the directory where Iris was launched, usually `$HOME`. After changing directories in the shell, file suggestions could therefore continue to come from `$HOME`. ## Tests - `go test -race ./spec` - `go test ./root -run ^'$'` - `go test ./... -run ^'$'` - generated zsh, bash, and fish hook syntax checks Co-authored-by: Pavel Naumov <Pavel.Naumov@berekebank.kz>
This commit is contained in:
co-authored by
Pavel Naumov
parent
cee756e2c1
commit
9f43fcb352
+18
-4
@@ -42,8 +42,14 @@ if [ -n "$IRIS_PID" ] && [ -n "$IRIS_FD" ]; then
|
|||||||
print -u $IRIS_FD -N -r -- "$LBUFFER" 2>/dev/null
|
print -u $IRIS_FD -N -r -- "$LBUFFER" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_iris_sync_cwd() {
|
||||||
|
print -u $IRIS_FD -N -r -- "IRIS_CWD:$PWD" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
_iris_precmd() {
|
_iris_precmd() {
|
||||||
print -u $IRIS_FD -N -r -- "IRIS_CMD_STOP" 2>/dev/null
|
local iris_exit_code=$?
|
||||||
|
_iris_sync_cwd
|
||||||
|
print -u $IRIS_FD -N -r -- "IRIS_CMD_STOP:$iris_exit_code" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
_iris_preexec() {
|
_iris_preexec() {
|
||||||
@@ -56,6 +62,7 @@ if [ -n "$IRIS_PID" ] && [ -n "$IRIS_FD" ]; then
|
|||||||
add-zle-hook-widget line-pre-redraw _iris_send_lbuffer
|
add-zle-hook-widget line-pre-redraw _iris_send_lbuffer
|
||||||
add-zsh-hook precmd _iris_precmd
|
add-zsh-hook precmd _iris_precmd
|
||||||
add-zsh-hook preexec _iris_preexec
|
add-zsh-hook preexec _iris_preexec
|
||||||
|
add-zsh-hook chpwd _iris_sync_cwd
|
||||||
fi
|
fi
|
||||||
`)
|
`)
|
||||||
case "bash":
|
case "bash":
|
||||||
@@ -75,7 +82,9 @@ fi
|
|||||||
# Iris Autocomplete Hook
|
# Iris Autocomplete Hook
|
||||||
if [ -n "$IRIS_PID" ] && [ -n "$IRIS_FD" ]; then
|
if [ -n "$IRIS_PID" ] && [ -n "$IRIS_FD" ]; then
|
||||||
_iris_bash_precmd() {
|
_iris_bash_precmd() {
|
||||||
printf "IRIS_CMD_STOP\x00" >&$IRIS_FD 2>/dev/null
|
local iris_exit_code=$?
|
||||||
|
printf "IRIS_CWD:%%s\x00" "$PWD" >&$IRIS_FD 2>/dev/null
|
||||||
|
printf "IRIS_CMD_STOP:%%s\x00" "$iris_exit_code" >&$IRIS_FD 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ ";$PROMPT_COMMAND;" != *";_iris_bash_precmd;"* ]]; then
|
if [[ ";$PROMPT_COMMAND;" != *";_iris_bash_precmd;"* ]]; then
|
||||||
@@ -103,10 +112,15 @@ end
|
|||||||
# Iris Autocomplete Hook
|
# Iris Autocomplete Hook
|
||||||
if set -q IRIS_PID; and set -q IRIS_FD
|
if set -q IRIS_PID; and set -q IRIS_FD
|
||||||
function _iris_fish_postexec --on-event fish_postexec
|
function _iris_fish_postexec --on-event fish_postexec
|
||||||
echo -n -e "IRIS_CMD_STOP\x00" >&$IRIS_FD 2>/dev/null
|
set -l iris_exit_code $status
|
||||||
|
printf "IRIS_CWD:%%s\x00" "$PWD" >&$IRIS_FD 2>/dev/null
|
||||||
|
printf "IRIS_CMD_STOP:%%s\x00" "$iris_exit_code" >&$IRIS_FD 2>/dev/null
|
||||||
|
end
|
||||||
|
function _iris_fish_prompt --on-event fish_prompt
|
||||||
|
printf "IRIS_CWD:%%s\x00" "$PWD" >&$IRIS_FD 2>/dev/null
|
||||||
end
|
end
|
||||||
function _iris_fish_preexec --on-event fish_preexec
|
function _iris_fish_preexec --on-event fish_preexec
|
||||||
echo -n -e "IRIS_CMD_START\x00" >&$IRIS_FD 2>/dev/null
|
printf "IRIS_CMD_START\x00" >&$IRIS_FD 2>/dev/null
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
`)
|
`)
|
||||||
|
|||||||
@@ -386,6 +386,11 @@ func runWrapper() {
|
|||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
query := scanner.Text()
|
query := scanner.Text()
|
||||||
|
|
||||||
|
if cwd, ok := strings.CutPrefix(query, "IRIS_CWD:"); ok {
|
||||||
|
spec.SetCWD(cwd)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if query == "IRIS_CMD_START" {
|
if query == "IRIS_CMD_START" {
|
||||||
isCommandActive.Store(true)
|
isCommandActive.Store(true)
|
||||||
bufferMu.Lock()
|
bufferMu.Lock()
|
||||||
|
|||||||
+31
-6
@@ -6,24 +6,49 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/versenilvis/iris/internal/config"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// for tracking current working dir
|
var (
|
||||||
var ShellPID int
|
// ShellPID tracks the underlying shell process.
|
||||||
|
ShellPID int
|
||||||
|
|
||||||
|
shellCWDMu sync.RWMutex
|
||||||
|
shellCWD string
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetCWD updates the working directory reported by the underlying shell.
|
||||||
|
// Shell integrations call this whenever the prompt directory changes.
|
||||||
|
func SetCWD(cwd string) {
|
||||||
|
if cwd != "" && !filepath.IsAbs(cwd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shellCWDMu.Lock()
|
||||||
|
shellCWD = cwd
|
||||||
|
shellCWDMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// GetCWD returns the current working directory of the underlying shell
|
// GetCWD returns the current working directory of the underlying shell
|
||||||
// on Linux, it reads it from /proc/[pid]/cwd
|
// reported by shell integration. Linux can also read it from /proc/[pid]/cwd.
|
||||||
func GetCWD() string {
|
func GetCWD() string {
|
||||||
|
shellCWDMu.RLock()
|
||||||
|
cwd := shellCWD
|
||||||
|
shellCWDMu.RUnlock()
|
||||||
|
if cwd != "" {
|
||||||
|
return cwd
|
||||||
|
}
|
||||||
|
|
||||||
if ShellPID > 0 {
|
if ShellPID > 0 {
|
||||||
path := fmt.Sprintf("/proc/%d/cwd", ShellPID)
|
path := fmt.Sprintf("/proc/%d/cwd", ShellPID)
|
||||||
cwd, err := os.Readlink(path)
|
procCWD, err := os.Readlink(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return cwd
|
return procCWD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ = os.Getwd()
|
||||||
return cwd
|
return cwd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFileGenerator(t *testing.T) {
|
func TestFileGenerator(t *testing.T) {
|
||||||
|
SetCWD("")
|
||||||
|
|
||||||
// Setup mock files
|
// Setup mock files
|
||||||
tmp := t.TempDir()
|
tmp := t.TempDir()
|
||||||
_ = os.MkdirAll(filepath.Join(tmp, "src"), 0755)
|
_ = os.MkdirAll(filepath.Join(tmp, "src"), 0755)
|
||||||
@@ -92,3 +94,50 @@ func TestFileGenerator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetCWDUsesShellReportedDirectory(t *testing.T) {
|
||||||
|
launcherDir := t.TempDir()
|
||||||
|
shellDir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(shellDir, "from-shell.txt"), nil, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldWd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(launcherDir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
SetCWD("")
|
||||||
|
_ = os.Chdir(oldWd)
|
||||||
|
})
|
||||||
|
|
||||||
|
SetCWD(shellDir)
|
||||||
|
if got := GetCWD(); got != shellDir {
|
||||||
|
t.Fatalf("GetCWD() = %q, want shell-reported directory %q", got, shellDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := FileGenerator()([]string{"cat", ""}, "cat ", "")
|
||||||
|
found := false
|
||||||
|
for _, result := range results {
|
||||||
|
if result.Cmd == "from-shell.txt" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("FileGenerator() did not read suggestions from shell directory %q", shellDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetCWDRejectsRelativePaths(t *testing.T) {
|
||||||
|
SetCWD("")
|
||||||
|
t.Cleanup(func() { SetCWD("") })
|
||||||
|
|
||||||
|
SetCWD("relative/path")
|
||||||
|
if got := GetCWD(); got == "relative/path" {
|
||||||
|
t.Fatalf("GetCWD() accepted relative shell directory %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user