Skip to content
3 changes: 3 additions & 0 deletions .gemini/base_GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Gemini Added Memories
@instructions.md
@context.md
3 changes: 3 additions & 0 deletions .gemini/context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Context for readline (~/code/github.com/reeflective/readline)

Add your project-specific context here.
46 changes: 46 additions & 0 deletions .gemini/fix_analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Fix Analysis: Multiline Display & Cursor Positioning

## The Issue
The cursor jumps to the top of the input area and fails to return to the bottom during multiline editing, specifically when `multiline-column` options are disabled.

## Analysis of `MultilineColumnPrint`
The function `MultilineColumnPrint` behaves differently based on configuration:
1. **`multiline-column-numbered` (ON):**
* Iterates through all lines.
* Prints a string containing `\n` for each line.
* **Effect:** The cursor physically moves down `N` lines (where `N` is the number of logical lines).
2. **`multiline-column` / Default (OFF):**
* The loop body or case is not entered (or returns empty string).
* **Effect:** The function prints nothing. The cursor does **not** move.

## The Logic Flaw in `displayMultilinePrompts` (or `Refresh`)
The calling code typically looks like this:
```go
if e.line.Lines() > 1 {
term.MoveCursorUp(e.lineRows) // Move to Top
e.prompt.MultilineColumnPrint() // Print Columns (Expectation: Moves Down)
// Missing: Explicit return to bottom if Print() didn't move us.
}
```

* **Scenario A (Numbered ON):** `MoveCursorUp` moves up. `Print` moves down (mostly). The cursor ends up near the bottom. The error is small (difference between logical newlines and wrapped rows).
* **Scenario B (All OFF):** `MoveCursorUp` moves up. `Print` does nothing. The cursor stays at the top. **This is the bug.**

## Proposed Fix
The fix requires two adjustments:
1. **Conditional Execution:** Only perform the "Move Up -> Print" sequence if a column mode is actually enabled. If disabled, there is no need to move up just to print nothing.
2. **Cursor Correction:** When enabled, ensure the cursor returns to the correct bottom position by accounting for line wrapping.

**Algorithm:**
1. Check if `multiline-column`, `numbered`, or `custom` is enabled.
2. **If Enabled:**
* Move Cursor Up `e.lineRows`.
* Call `MultilineColumnPrint`.
* Move Cursor Down `e.lineRows - e.line.Lines()`. (Correction for wrapped lines vs logical newlines).
3. **If Disabled:**
* Skip the block. The cursor remains at the bottom (where `displayLine` left it).

## Secondary Prompts (`└ `)
The current logic only prints the Secondary Prompt (`PS2`) for the *current* (last) line. Previous lines display the column indicator (`|` or number). If columns are disabled, previous lines display nothing. This appears to be intended behavior, or at least separate from the cursor jump bug.

```
58 changes: 58 additions & 0 deletions .gemini/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@

## Global Programming Instructions & Guidelines

- **Code removal:** Do not remove code that is not immediately relevant
to the changes you want to operate when being given a specific task.
- **Replacements:** When having to do many small (and rather or completely
identical) replacements in one or more files, perform these replacements
in a single call for each file, whenever possible.

## Python Programming Guidelines

Here are some guidelines that I find useful when working on a Python codebase.

### General Guidelines

* **Follow Project Conventions**: Adhere to the existing coding style, patterns, and practices used in the project.
* **Dependency Management**: Use the project's dependency manager (`uv`).

### Style and Formatting

* **PEP 8**: Follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide for Python code. Use tools like `black` for automatic formatting and `ruff` for linting to enforce it.
* **Docstrings**: Write clear and concise docstrings for all modules, functions, classes, and methods, following the [PEP 257](https://www.python.org/dev/peps/pep-0257/) conventions. Use a consistent format like Google Style or reStructuredText.
* **Typing**: Use Python's type hints (`str`, `int`, `List`, `Dict`) for all function signatures and variables where it improves clarity. Use a static type checker like `mypy` to verify type correctness.
* **Naming**:
* `snake_case` for functions, methods, and variables.
* `PascalCase` for classes.
* `UPPER_SNAKE_CASE` for constants.
* `_` for unused variables.

### Code Organization

* **Modularity**: Break down large files into smaller, more manageable modules with a single responsibility.
* **Imports**:
* Import modules, not individual functions or classes, to avoid circular dependencies and naming conflicts (e.g., `import my_module` instead of `from my_module import my_function`).
* Group imports in the following order:
1. Standard library imports (`os`, `sys`).
2. Third-party library imports (`requests`, `pandas`).
3. Local application/library specific imports.
* **Absolute vs. Relative Imports**: Prefer absolute imports (`from my_app.core import utils`) over relative imports (`from ..core import utils`) for clarity and to avoid ambiguity.

### Testing

* **Unit Tests**: Write unit tests for all new code. Use a testing framework like `pytest`.
* **Test Coverage**: Aim for high test coverage, but focus on testing the logic and edge cases rather than just the line count.
* **Test Naming**: Name test files `test_*.py` and test functions `test_*()`.
* **Mocking**: Use mocking libraries like `unittest.mock` to isolate units of code and avoid external dependencies in tests.

### Documentation

* **Comments**: Add comments to explain *why* something is done, not *what* is being done. The code itself should be self-explanatory.
* **Configuration**: Keep configuration separate from code. Use environment variables or configuration files (`.env`, `config.ini`, `settings.py`).

### Best Practices

* **List Comprehensions**: Use list comprehensions for creating lists in a concise and readable way.
* **Generators**: Use generators and generator expressions for memory-efficient iteration over large datasets.
* **Error Handling**: Be specific in exception handling. Avoid bare `except:` clauses.
* **Context Managers**: Use the `with` statement when working with resources that need to be cleaned up (e.g., files, database connections).
45 changes: 45 additions & 0 deletions .gemini/refactoring_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Display Engine Refactoring Plan

## Goals
- Fix cursor positioning issues in multiline editing.
- Ensure robust rendering of prompts, input, and helpers.
- Standardize the display sequence to avoid "lost cursor" states.

## Components
1. **Prompts:** Primary (PS1), Secondary (PS2/`└ `), Multiline Columns (`│ `), Right/Tooltip.
2. **Input:** Buffer text, Syntax Highlighting, Visual Selection, Auto-suggestions.
3. **Helpers:** Hints, Completions.

## Proposed Rendering Sequence (Refresh Cycle)

1. **Preparation & Coordinates**
* Hide Cursor.
* Reset Cursor to start of input area (after Primary Prompt).
* Compute Coordinates: `StartPos`, `LineHeight`, `CursorPos` (row/col).
* Check for buffer height changes to clear potential artifacts below.

2. **Primary Prompt**
* Reprint only if invalidated (e.g., transient prompt, clear screen).
* Otherwise, assume cursor starts at `StartPos`.

3. **Input Area Rendering**
* **Input Line:** Print the full input buffer (highlighted + auto-suggestion). Cursor ends at the end of the input text.
* **Right Prompt:**
* Calculate position relative to the end of the input.
* Move cursor, print prompt, restore cursor to end of input.
* **Multiline Indicators (Columns/Secondary):**
* Iterate through input lines.
* Move to start of each line.
* Print column indicators (`│ `, numbers) and secondary prompts (`└ `).
* **Crucial:** Return cursor to the **end of the input text** after this pass.

4. **Helpers Rendering**
* Move cursor below the last line of input.
* **Hints:** Print hints.
* **Completions:** Print completion menu/grid.
* **Cleanup:** Clear any remaining lines below if the new render is shorter.

5. **Final Cursor Positioning**
* Move cursor from bottom of helpers -> End of Input.
* Move cursor from End of Input -> Actual Cursor Position (using `CursorPos`).
* Show Cursor.
31 changes: 31 additions & 0 deletions .gemini/temp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Temporary Notes for readline (~/code/github.com/reeflective/readline)

This file is for your private notes, prompts, and scratchpad content.
It is not directly sent to Gemini, but you can easily copy/paste from here.

The behavior in the shell is the following:

I enter a first line: `testing \`
I press enter, and the shell correctly goes to a new line.
I then type again: `testing \`
and the prompt goes back to its initial position on the first line (which means it goes one up one line too much.
Starting from there, everytime I enter a character (thus causing the shell to redisplay the line) the cursor goes up 2 lines, while it should not.

Based on this, I want you to tell me what do you suspect in the code is not working correctly.

Investigate the codebase and identify the issue.

Consider this snippet from the codebase.

helpersMoved := e.displayHelpers()
if helpersMoved {
e.cursorHintToLineStart()
e.lineStartToCursorPos()
} else {
e.lineEndToCursorPos()
}


I'm pretty sure the problem is in this snippet.
I suspect that cursorHintToLineStart() or lineStartToCursorPos() are miscalculating something when
the line is a multiline string.
5 changes: 5 additions & 0 deletions .gemini/todo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Master To-Do List for readline (~/code/github.com/reeflective/readline)

## Child To-Do Lists
(BEGIN AUTO-GENERATED-TODOS)
(END AUTO-GENERATED-TODOS)
4 changes: 2 additions & 2 deletions inputrc/inputrc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func buildOpts(t *testing.T, buf []byte) []Option {
lines := bytes.Split(bytes.TrimSpace(buf), []byte{'\n'})
var opts []Option

for i := 0; i < len(lines); i++ {
for i := range lines {
line := bytes.TrimSpace(lines[i])
// If the line is empty, keep going
if len(line) == 0 {
Expand Down Expand Up @@ -406,4 +406,4 @@ func readTestdata(name string) ([]byte, error) {
}

//go:embed testdata/*.inputrc
var testdata embed.FS
var testdata embed.FS
6 changes: 3 additions & 3 deletions inputrc/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,11 +507,11 @@ func findStringEnd(seq []rune, pos, end int) (int, bool) {
quote := seq[pos]

for pos++; pos < end; pos++ {
switch char = seq[pos]; {
case char == '\\':
switch char = seq[pos]; char {
case '\\':
pos++
continue
case char == quote:
case quote:
return pos + 1, true
}
}
Expand Down
10 changes: 5 additions & 5 deletions internal/core/cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,12 @@ func (c *Cursor) LineMove(lines int) {
}

if lines < 0 {
for i := 0; i < -1*lines; i++ {
for i := 0; i < -lines; i++ {
c.moveLineUp()
c.CheckCommand()
}
} else {
for i := 0; i < lines; i++ {
for range lines {
c.moveLineDown()
c.CheckCommand()
}
Expand Down Expand Up @@ -287,7 +287,7 @@ func (c *Cursor) AtBeginningOfLine() bool {

newlines := c.line.newlines()

for line := 0; line < len(newlines); line++ {
for line := range newlines {
epos := newlines[line][0]
if epos == c.pos-1 {
return true
Expand All @@ -306,7 +306,7 @@ func (c *Cursor) AtEndOfLine() bool {

newlines := c.line.newlines()

for line := 0; line < len(newlines); line++ {
for line := range newlines {
epos := newlines[line][0]
if epos == c.pos+1 {
return true
Expand Down Expand Up @@ -394,7 +394,7 @@ func (c *Cursor) moveLineDown() {

newlines := c.line.newlines()

for line := 0; line < len(newlines); line++ {
for line := range newlines {
end := newlines[line][0]
if line < c.LinePos() {
begin = end
Expand Down
3 changes: 2 additions & 1 deletion internal/core/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import (
"regexp"
"sync"

"github.com/rivo/uniseg"

"github.com/reeflective/readline/inputrc"
"github.com/reeflective/readline/internal/strutil"
"github.com/rivo/uniseg"
)

const (
Expand Down
2 changes: 1 addition & 1 deletion internal/core/selection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1259,7 +1259,7 @@ func TestSelection_SelectKeyword(t *testing.T) {
var gotKbpos, gotKepos int
var gotMatch bool

for i := 0; i < test.args.cycles; i++ {
for range test.args.cycles {
gotKbpos, gotKepos, gotMatch = sel.SelectKeyword(test.args.bpos, test.args.epos, test.args.next)
}

Expand Down
Loading
Loading