Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) + "?",
Expand Down
60 changes: 60 additions & 0 deletions internal/ui/app_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ui

import (
"fmt"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 2 additions & 3 deletions internal/ui/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Expand Down
10 changes: 10 additions & 0 deletions internal/ui/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down