diff --git a/internal/ui/app.go b/internal/ui/app.go index 06a9294..8e7363c 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -415,7 +415,7 @@ func (a App) keySaveAs(msg tea.KeyMsg) (App, tea.Cmd) { // browse handles the keys the listing shares between both dialogs. func (a *App) browse(msg tea.KeyMsg) { - window := a.panelHeight() + window := a.listRows() switch msg.String() { case "up": a.exp.move(-1, window) @@ -478,7 +478,12 @@ func (a *App) commitName() tea.Cmd { } path := filepath.Join(a.exp.dir, name) - if _, err := os.Stat(path); err == nil && path != a.ed.Path { + edPath := a.ed.Path + if abs, err := filepath.Abs(edPath); err == nil { + edPath = abs + } + + if _, err := os.Stat(path); err == nil && path != edPath { a.name.Blur() a.confirm = confirmation{ message: "overwrite " + filepath.Base(path) + "?", diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index b252edc..902cb54 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -1,6 +1,7 @@ package ui import ( + "fmt" "os" "path/filepath" "testing" @@ -189,6 +190,65 @@ func TestSavingAnUnnamedDocumentOpensTheDialog(t *testing.T) { } } +// browse scrolled by the full panel height, but save-as only draws +// height-2 rows for the listing — the other two hold the filename field — +// so holding the cursor down could push it below what filePanel renders. +func TestSaveAsScrollStaysInsideWhatItDraws(t *testing.T) { + dir := t.TempDir() + for i := range 40 { + name := filepath.Join(dir, fmt.Sprintf("archivo%02d.md", i)) + if err := os.WriteFile(name, nil, 0o644); err != nil { + t.Fatal(err) + } + } + + a := testApp(t) + a.mode = ModeSaveAs + a.exp.refresh(dir, "") + + for range 50 { + a.browse(tea.KeyMsg{Type: tea.KeyDown}) + } + + rows := a.listRows() + if a.exp.cursor < a.exp.offset || a.exp.cursor >= a.exp.offset+rows { + t.Errorf("cursor %d outside the visible window [%d, %d)", a.exp.cursor, a.exp.offset, a.exp.offset+rows) + } +} + +// commitName compared an absolute path built from a.exp.dir against +// a.ed.Path exactly as the user typed it — relative if justwrite was +// launched as `justwrite nota.md`. Saving over the file already open under +// that name must not ask to overwrite itself. +func TestSaveAsOverTheOpenFileAsksNothing(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "nota.md"), []byte("hola"), 0o644); err != nil { + t.Fatal(err) + } + + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(wd) + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + + a, err := NewApp("nota.md") + if err != nil { + t.Fatalf("NewApp: %v", err) + } + a.exp.refresh(a.startDir(), "") + a.name.SetValue("nota.md") + + a.commitName() + + if a.mode == ModeConfirm { + t.Error("asked to overwrite the file that is already open") + } +} + // Home, End and PageUp all index into the rows of the page, so a terminal too // narrow to hold any text still has to produce one. If that guard ever goes, // this test panics rather than fails. diff --git a/internal/ui/explorer.go b/internal/ui/explorer.go index ab721e9..faa8d36 100644 --- a/internal/ui/explorer.go +++ b/internal/ui/explorer.go @@ -166,12 +166,11 @@ func (e *explorer) rows(n, width int, focused bool) []string { // filePanel draws the open and save-as dialogs. They share the listing; save-as // adds the filename field underneath it. func (a App) filePanel() string { - width, height := a.panelWidth(), a.panelHeight() + width := a.panelWidth() - listRows := height + listRows := a.listRows() title := "open" if a.mode == ModeSaveAs { - listRows = max(height-2, 1) title = "save as" } diff --git a/internal/ui/overlay.go b/internal/ui/overlay.go index d7ccc4f..5d2e4de 100644 --- a/internal/ui/overlay.go +++ b/internal/ui/overlay.go @@ -32,6 +32,16 @@ func (a App) panel() string { func (a App) panelWidth() int { return min(max(a.w-8, 20), 64, a.w-2) } func (a App) panelHeight() int { return clamp(a.h-6, 6, 20) } +// listRows is how many rows of the listing filePanel actually draws, so +// browse scrolls by the same window instead of recomputing a different one — +// save-as reserves two rows underneath for the filename field. +func (a App) listRows() int { + if a.mode == ModeSaveAs { + return max(a.panelHeight()-2, 1) + } + return a.panelHeight() +} + func clamp(v, lo, hi int) int { return min(max(v, lo), hi) } // divider stands in for a body line that should be drawn as a rule joining