Detecting Arrow Key Presses in a Rust Terminal Editor

Mục tiêu: Expand the application’s event loop to recognize user input from the Up, Down, Left, and Right arrow keys. This involves adding new match arms for each KeyCode variant and using the todo!() macro as a placeholder for the future movement logic.


An enormous congratulations on reaching this milestone! In the last step, you brought your editor to life visually. By connecting your refresh_screen method to the main event loop, you transformed your abstract data model into a tangible, running application that looks and feels like a real terminal editor. That screen of tildes with a blinking cursor at the top-left is the result of a solid and well-thought-out rendering architecture.

But right now, our editor is a beautiful, static painting. The user can look at it, but they can’t interact with it. The cursor is fixed at (0, 0), a silent observer. Our next great challenge is to introduce interactivity, and that begins with the most fundamental action in any text editor: moving the cursor.

This entire step is dedicated to implementing cursor movement. Our first task is to teach our event loop to simply recognize when the user presses the arrow keys. We won’t move the cursor just yet; we’ll first set up the scaffolding to detect the user’s intent to move.

Expanding the Event Handling Logic

Our match event block is the central nervous system of the application. It’s where we interpret user input and decide what to do. Currently, it only knows about one specific key combination: Ctrl+Q. We need to expand its vocabulary to include the four arrow keys.

The crossterm crate makes this straightforward. When event::read() gives us an Event::Key, the enclosed KeyEvent struct contains a code field. This code is of the type KeyCode, a comprehensive enum that represents every possible key on the keyboard—not just printable characters like 'a' or 'b', but also special keys like Enter, Backspace, F5, and, of course, the arrow keys.

The variants we are interested in are: * KeyCode::Up * KeyCode::Down * KeyCode::Left * KeyCode::Right

We will add new “arms” to our match statement to handle each of these KeyCode variants. For now, the action for each arm will be a placeholder, signaling that we’ve recognized the key press but the logic to handle it will be built in the next task.

Updating the Event Loop

Let’s modify the loop in your main function. We will be adding the new match arms and introducing a very useful Rust macro, todo!(), as a placeholder for the work we’ll do next.

Here is the updated main function. Pay close attention to the changes inside the loop.

// src/main.rs

// Make sure KeyCode is brought into scope from crossterm::event
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
// ... other use statements ...
use std::io::{stdout, Result};

// ... RawModeGuard struct, Editor struct, and impl Editor are unchanged ...

fn main() -> Result<()> {
    terminal::enable_raw_mode()?;
    let _raw_mode_guard = RawModeGuard;
    let mut editor = Editor::new()?;

    loop {
        editor.refresh_screen()?;

        let event = event::read()?;
        match event {
            Event::Key(key_event) => {
                // We match on the key's code
                match key_event.code {
                    // This is the existing exit condition
                    KeyCode::Char('q') if key_event.modifiers == KeyModifiers::CONTROL => {
                        break;
                    }

                    // --- NEW CODE FOR THIS TASK ---
                    // For each arrow key, we'll use the todo!() macro.
                    // This is a placeholder that will panic if this code is ever
                    // executed, reminding us that we haven't implemented it yet.
                    // It's a great way to build out the structure of our match
                    // statement before writing the actual logic.
                    KeyCode::Up => todo!(),
                    KeyCode::Down => todo!(),
                    KeyCode::Left => todo!(),
                    KeyCode::Right => todo!(),
                    // -----------------------------

                    // We will temporarily ignore all other key presses.
                    // The old debug println! is no longer needed.
                    _ => {}
                }
            }
            _ => {}
        }
    }
    Ok(())
}

Code and Concept Deep Dive

  • use crossterm::event::{... KeyCode ...}: We must ensure that the KeyCode enum is in scope so we can use its variants (like KeyCode::Up) in our match statement.
  • Nested match: Notice we are now matching on key_event.code. This is a clean way to separate the logic for handling different keys from the initial check that the event is a key event.
  • KeyCode::Up => todo!(),: This is the core of our change. We’ve added a match arm for each arrow key.
    • The todo!() macro: This is a standard Rust macro that is incredibly useful during development. When the program executes a todo!(), it will immediately panic (a controlled crash) and print a message like panicked at 'not yet implemented'. This is far better than leaving an empty block ({}), which would make it seem like the key press does nothing. Using todo!() is an explicit, self-documenting way to say, “I have planned for this case, but the implementation is coming soon.” It prevents you from forgetting to wire up the logic later.
  • _ => {}: The final arm of the inner match is a catch-all. It means “for any other key code that I haven’t specifically handled, do nothing.” We have removed the old println! for debugging because our editor is becoming more sophisticated. For now, we only care about Ctrl+Q and the arrow keys.

You have now successfully laid the foundation for cursor movement. Your event loop is no longer just waiting for an exit command; it’s actively listening for the user’s directional input, ready to be connected to the logic that will update the editor’s state.

Next Steps

The todo!() macros are pointing the way. Our application structure is ready and waiting for the movement logic. In the very next task, you will create a new method on the Editor struct called move_cursor. This method will take a direction as input and will be responsible for modifying the editor.cursor_x and editor.cursor_y fields. Once that method is built, we will come back here and replace the todo!() calls with calls to our new method.


Further Reading

To learn more about the concepts you’ve just used, explore these resources:

Add a move\_cursor Method and Direction Enum in Rust

Mục tiêu: Refactor the text editor by creating a Direction enum and a move\_cursor method on the Editor struct. This centralizes cursor movement logic using a mutable reference and a match statement, separating state management from event handling.


Excellent work setting up the event loop to recognize arrow key presses! In the last task, you expertly laid the groundwork by creating match arms for each arrow key and using the todo!() macro. This is the perfect segue into our current task, as those todo!() placeholders are essentially signposts telling us, “The logic for moving the cursor belongs here.”

Now, we need to build that logic. A core principle of good software design is to keep the event handling (the “controller”) separate from the state management (the “model”). Our main function’s loop is the controller; it recognizes the user’s intent to move the cursor. The Editor struct is our model; it should be responsible for how to actually change its own state.

Therefore, we will create a dedicated method on our Editor struct, move_cursor, to handle the logic of updating the cursor coordinates.

Modeling Direction with an Enum

First, how should we tell this method which way to move the cursor? We could pass a string like "up" or a number like 0, but these approaches are error-prone. A typo in the string or a wrong number could introduce bugs. Rust gives us a much safer and more expressive tool for this: the enum.

An enum (short for enumeration) allows us to define a custom type that can only be one of a few specific variants. This is perfect for representing a fixed set of options, like directions. We will create a Direction enum with four variants: Up, Down, Left, and Right. This makes our code self-documenting and type-safe—it’s impossible to pass an invalid direction to our function.

Let’s add this new enum to src/main.rs. By convention, type definitions like structs and enums are placed near the top of the file.

Creating the move_cursor Method

With our Direction enum defined, we can now create the move_cursor method itself. This method will live inside the impl Editor block along with new, refresh_screen, and draw_rows.

There’s one critical new concept here: mutability. To change the cursor’s position, we need to modify the fields of our Editor struct (e.g., self.cursor_y -= 1;). To get permission to do this, the method must take a mutable reference to the instance, which is written as &mut self. This is a core part of Rust’s safety model: it forces you to be explicit about when and where your data can be changed.

Inside the method, we’ll use a match statement on the direction argument, creating a clean structure that mirrors the one in our main event loop. For now, we’ll fill each arm of this new match statement with todo!(), as the specific logic for each direction will be handled in the subsequent tasks.

Here is the new code to add to src/main.rs.

// src/main.rs

// ... (use statements, RawModeGuard struct remain the same) ...

// --- NEW ENUM DEFINITION ---
// We make the enum public (`pub`) which is a good practice for types
// that might be used in other modules later on, during refactoring.
pub enum Direction {
    Up,
    Down,
    Left,
    Right,
}

struct Editor {
    // ... (fields are unchanged) ...
}

impl Editor {
    fn new() -> Result<Self> {
        // ... (constructor is unchanged) ...
    }

    // ... (refresh_screen and draw_rows methods are unchanged) ...

    // --- NEW METHOD FOR THIS TASK ---

    // This method takes a mutable reference to self because it needs to modify
    // the Editor's state (the cursor position).
    fn move_cursor(&mut self, direction: Direction) {
        // A match statement is the perfect way to handle the different
        // direction variants from our enum.
        match direction {
            // For now, we use the todo!() macro as a placeholder for each
            // direction's logic. We will implement these in the next tasks.
            Direction::Up => todo!(),
            Direction::Down => todo!(),
            Direction::Left => todo!(),
            Direction::Right => todo!(),
        }
    }
}

// ... (main function is unchanged for now) ...

Understanding the New Pieces

  • pub enum Direction { ... }: We’ve defined a new public type. Now, anywhere in our code, we can use Direction::Up, Direction::Down, etc. This is far more readable and safer than using magic numbers or strings.
  • fn move_cursor(&mut self, direction: Direction): This is the method signature.
    • &mut self: This is the key. It gives the method exclusive, mutable access to the Editor instance it’s called on. This is what allows us to later write code like self.cursor_y += 1;. Without &mut, the compiler would prevent us from changing any fields.
    • direction: Direction: This specifies that our method accepts one argument named direction, which must be of the Direction enum type.
  • match direction { ... }: This is the body of our method. It inspects the direction variable and provides a clear, structured way to handle each possible variant. The todo!() placeholders act as our to-do list for the upcoming tasks.

You have now successfully created the central hub for all cursor movement logic. The structure is clean and robust. The main loop will determine the user’s intent, and this new method will be responsible for executing it, perfectly separating the concerns of your application.

Next Steps

The scaffolding is complete! Our move_cursor method exists but does nothing. In the very next task, we will bring it to life. We will start by implementing the logic for the Direction::Up case, replacing the todo!() with code that decrements self.cursor_y while ensuring it doesn’t go below zero.


Further Reading

Implement Upward Cursor Movement in Rust

Mục tiêu: Implement the logic for moving the editor’s cursor up. This involves safely decrementing the cursor’s vertical position and adding a crucial boundary check to prevent it from going below 0, thus avoiding integer underflow issues with Rust’s usize type.


You have done a fantastic job setting up the architecture for cursor movement. In the last task, you created the move_cursor method and a Direction enum, establishing a clean separation between detecting user intent in the event loop and managing the editor’s state. The todo!() placeholders you added are now calling out to be replaced with real logic. Let’s answer that call and implement the very first piece of cursor movement: moving up.

The Core Logic and Its Boundary

The fundamental logic for moving the cursor up is simple: we need to decrease its vertical position, self.cursor_y. However, we must immediately consider a critical boundary condition. The cursor cannot move above the top edge of the screen, which corresponds to row 0. We must ensure that self.cursor_y never becomes a negative number.

The Peril of Integer Underflow with usize

This boundary condition is especially important in Rust due to our choice of data type. We wisely chose usize for our coordinates, as it’s the natural type for indexing. usize is an unsigned integer, meaning it cannot represent negative values.

So, what would happen if self.cursor_y was 0 and we tried to execute self.cursor_y -= 1;?

This action is called integer underflow. In Rust, this is a serious issue that the language protects you from: * In debug mode (when you run cargo run), your program would panic. This is Rust’s way of stopping everything and alerting you to a significant bug. The program would crash with a clear error message. * In release mode (with cargo run --release), Rust prioritizes performance. Instead of panicking, the number would wrap around to the largest possible usize value (a massive number like 18,446,744,073,709,551,615 on a 64-bit system). This would cause the cursor to vanish as it tries to move to an impossibly large row number.

Both outcomes are bugs. To write robust code, we must prevent this from ever happening. The solution is simple: we must check if self.cursor_y is greater than 0 before we attempt to subtract from it.

Implementing the Upward Movement Logic

Let’s replace the todo!() in the Direction::Up arm of your move_cursor method with this safe logic.

Here is the change for the impl Editor block in src/main.rs. We are only modifying the one line inside the move_cursor method.

// src/main.rs -> inside the impl Editor block

// This method takes a mutable reference to self because it needs to modify
// the Editor's state (the cursor position).
fn move_cursor(&mut self, direction: Direction) {
    // A match statement is the perfect way to handle the different
    // direction variants from our enum.
    match direction {
        // --- THIS IS THE CHANGE FOR OUR TASK ---
        Direction::Up => {
            // Check if the cursor is not already at the top row (row 0).
            if self.cursor_y > 0 {
                // If it's not, decrement the vertical cursor position by one.
                self.cursor_y -= 1;
            }
        }
        // ----------------------------------------
        Direction::Down => todo!(),
        Direction::Left => todo!(),
        Direction::Right => todo!(),
    }
}

Wiring It Up in the Event Loop

Our move_cursor method is now ready, but it’s not being called yet. The final step is to go back to our main function’s event loop and replace the corresponding todo!() with a call to this new logic.

Here is the change for the loop inside your main function.

// src/main.rs -> inside the main function's loop

match key_event.code {
    // This is the existing exit condition
    KeyCode::Char('q') if key_event.modifiers == KeyModifiers::CONTROL => {
        break;
    }

    // --- THIS IS THE CHANGE FOR OUR TASK ---
    KeyCode::Up => editor.move_cursor(Direction::Up),
    // ----------------------------------------
    KeyCode::Down => todo!(),
    KeyCode::Left => todo!(),
    KeyCode::Right => todo!(),

    _ => {}
}

The Magic of State-Driven Rendering

With these two small changes, you have implemented a complete feature. Run your editor now with cargo run. The cursor will start at (0, 0). If you press the Up Arrow key, nothing will happen—your if self.cursor_y > 0 check is correctly preventing a crash!

While we can’t move down yet to test it properly, the most beautiful part is what you didn’t have to write. You didn’t touch a single line of rendering code. The refresh_screen method you built in the previous step automatically reads the new value of self.cursor_y on the next frame and places the physical cursor in the correct position. This is the power and elegance of the state-driven architecture you have created.

Next Steps

The logic for moving up is now complete and safe. The next logical step is to implement the opposite movement. In the next task, you will tackle the Direction::Down case. This will involve incrementing self.cursor_y and handling a different boundary condition: preventing the cursor from moving past the last line of text in the buffer.


Further Reading

Implement Downward Cursor Movement

Mục tiêu: Add the logic to move the text editor’s cursor down one line, handling boundary conditions to prevent moving past the last line or causing errors with empty files.


Excellent! You’ve successfully implemented upward cursor movement. By adding logic to the Direction::Up arm and wiring it into the event loop, you took the first major step in making your editor interactive. The check you put in place to prevent cursor_y from underflowing below zero is a perfect example of writing robust, boundary-aware code.

Now, let’s complete the vertical axis of movement. Our current task is to implement the logic for moving the cursor down. This will feel familiar, as it’s the mirror image of moving up, but it comes with its own unique boundary condition that we must handle with care.

The Logic for Downward Movement

The core logic is straightforward: when the user wants to move down, we should increment the vertical cursor position, self.cursor_y. However, just as we couldn’t move past the top edge of the file, we must not be allowed to move past the bottom edge.

The “bottom edge” is defined by the number of lines in our text buffer, self.rows. If our text buffer has 10 lines, their indices will be 0 through 9. The last valid vertical position for the cursor is 9. Therefore, we should only be allowed to increment self.cursor_y if its current value is less than the index of the last line.

The number of lines in our buffer is given by self.rows.len(). This means the index of the last line is self.rows.len() - 1. So, the condition we need to check is: if self.cursor_y < self.rows.len() - 1.

Just like in the last task, we have to be careful about potential integer underflow. What happens if the file is empty and self.rows.len() is 0? The expression 0 - 1 would cause our program to panic in debug mode. To prevent this, we must first check that our rows vector is not empty before we try to calculate the index of the last line.

The safest and clearest way to write this logic is:

let num_rows = self.rows.len();
if num_rows > 0 && self.cursor_y < num_rows - 1 {
    self.cursor_y += 1;
}

This is robust for all cases:

  • Empty File (num_rows = 0): The first part of the condition (num_rows > 0) is false, so the second part is never evaluated, preventing a panic. The cursor does not move. This is correct.
  • Single Line File (num_rows = 1): The first part is true. The cursor is at y=0. The condition becomes 0 < 1 - 1, which is 0 < 0, which is false. The cursor does not move. This is correct.
  • Multi-Line File (num_rows = 10): The cursor can be incremented as long as self.cursor_y is less than 9. This is also correct.

Implementing Downward Movement

Let’s replace the todo!() in the Direction::Down arm of your move_cursor method with this logic.

Here is the change for the impl Editor block in src/main.rs. We are only modifying the Direction::Down arm.

// src/main.rs -> inside the impl Editor block

fn move_cursor(&mut self, direction: Direction) {
    match direction {
        Direction::Up => {
            if self.cursor_y > 0 {
                self.cursor_y -= 1;
            }
        }
        // --- THIS IS THE CHANGE FOR OUR TASK ---
        Direction::Down => {
            // Get the total number of lines in the text buffer.
            let num_rows = self.rows.len();
            // We can only move down if there are rows in the file AND
            // the cursor is not already on the last line.
            // The last line's index is `num_rows - 1`.
            if num_rows > 0 && self.cursor_y < num_rows - 1 {
                // If the conditions are met, increment the vertical cursor position.
                self.cursor_y += 1;
            }
        }
        // ----------------------------------------
        Direction::Left => todo!(),
        Direction::Right => todo!(),
    }
}

Wiring It Up in the Event Loop

Just like before, our method is ready, but it needs to be called. Let’s connect it to the KeyCode::Down event in our main function’s loop.

// src/main.rs -> inside the main function's loop

match key_event.code {
    KeyCode::Char('q') if key_event.modifiers == KeyModifiers::CONTROL => {
        break;
    }

    KeyCode::Up => editor.move_cursor(Direction::Up),
    // --- THIS IS THE CHANGE FOR OUR TASK ---
    KeyCode::Down => editor.move_cursor(Direction::Down),
    // ----------------------------------------
    KeyCode::Left => todo!(),
    KeyCode::Right => todo!(),

    _ => {}
}

Test Your Progress

You’ve now implemented full vertical movement! Run your application with cargo run.

Since your editor still starts with an empty file (self.rows.len() is 0), your boundary check for downward movement will correctly prevent the cursor from moving. You can press the Down Arrow key, and nothing will happen—this is the correct behavior and proves your logic is working!

We don’t have a way to add lines yet to fully test this, but the logic is sound and will work perfectly once we can open files or insert new lines. The important part is that the foundation is solid.

Next Steps

With vertical movement complete, it’s time to tackle the horizontal axis. In the next task, you will implement the logic for Direction::Left. This will involve decrementing self.cursor_x and, just like with cursor_y, handling the boundary condition to ensure it never goes below zero.


Further Reading

  • Vector len() and is_empty() methods: Explore the official documentation for these fundamental Vec methods. Notice how using is_empty() is often more readable and sometimes faster than checking len() == 0.
  • Short-Circuiting in Logical Expressions: The && (AND) and || (OR) operators in Rust use short-circuiting. This is the behavior that made our num_rows > 0 && ... check safe. Learn more about how it works.

Implement Leftward Cursor Movement in Rust Text Editor

Mục tiêu: Implement the logic for moving the cursor to the left. This includes handling two scenarios: decrementing the horizontal position when in the middle of a line, and wrapping the cursor to the end of the previous line when at the start of a line.


You have successfully implemented the full vertical axis of movement! Your move_cursor method now handles moving up and down, with robust checks to prevent the cursor from going beyond the boundaries of the file. This is a huge step in making the editor feel interactive.

With the vertical movement in place, we will now apply the same principles to the horizontal axis. Our current task is to implement the logic for moving the cursor left. This movement is slightly more complex than up/down because it has two distinct behaviors depending on the cursor’s position on the line.

Two Scenarios for Moving Left

  1. In the Middle of a Line: If the cursor is anywhere on a line other than the very first column (cursor_x > 0), moving left is simple: we just decrement self.cursor_x by one.
  2. At the Start of a Line: If the cursor is at the beginning of a line (cursor_x == 0), moving left should “wrap” the cursor to the end of the previous line. This can only happen if there is a previous line (i.e., cursor_y > 0). If it’s possible, we need to do two things:
    • Move up one line by decrementing self.cursor_y.
    • Move the horizontal cursor to the end of that new line by setting self.cursor_x to the length of the string at that line.

This logic creates the intuitive behavior users expect from a text editor, where pressing the left arrow key at the start of one line seamlessly joins it with the end of the one above.

Implementing Leftward Movement

We will use an if/else if structure within our move_cursor method to handle these two scenarios. This allows us to check for the first case (moving left on the same line) and only consider the second case (wrapping to the previous line) if the first one isn’t true.

Let’s replace the todo!() in the Direction::Left arm with this new logic.

Here is the change for the impl Editor block in src/main.rs. We are only modifying the Direction::Left arm.

// src/main.rs -> inside the impl Editor block

fn move_cursor(&mut self, direction: Direction) {
    match direction {
        Direction::Up => {
            if self.cursor_y > 0 {
                self.cursor_y -= 1;
            }
        }
        Direction::Down => {
            let num_rows = self.rows.len();
            if num_rows > 0 && self.cursor_y < num_rows - 1 {
                self.cursor_y += 1;
            }
        }
        // --- THIS IS THE CHANGE FOR OUR TASK ---
        Direction::Left => {
            // Scenario 1: Cursor is not at the beginning of a line.
            if self.cursor_x > 0 {
                self.cursor_x -= 1;
            // Scenario 2: Cursor is at the start of a line (x=0) and not on the first line (y>0).
            } else if self.cursor_y > 0 {
                // Move up to the previous line.
                self.cursor_y -= 1;
                // Set the horizontal cursor to the end of that line.
                // `self.rows[self.cursor_y].len()` gets the length of the string
                // on the new line, which is now the correct x-position.
                self.cursor_x = self.rows[self.cursor_y].len();
            }
        }
        // ----------------------------------------
        Direction::Right => todo!(),
    }
}

Code and Concept Deep Dive

  • if self.cursor_x > 0: This handles our first, most common scenario. The check is crucial because, just like with cursor_y, we cannot let an unsigned usize underflow below zero.
  • else if self.cursor_y > 0: This block only runs if the first condition was false (meaning self.cursor_x was 0). This check then ensures we are not on the very first line of the file before we try to move up. If the cursor is at (0, 0), neither of these conditions will be true, and nothing will happen, which is the correct behavior.
  • self.cursor_x = self.rows[self.cursor_y].len();: This is a key line. After we’ve moved up a line (by decrementing self.cursor_y), we need to find the new horizontal position. The len() method on a String gives us its length in bytes. For simple ASCII text, this is equivalent to the number of characters. Setting cursor_x to this value places the cursor right after the last character, which is the standard behavior for the end of a line.

Wiring It Up in the Event Loop

Our method is ready to handle leftward movement. Now we just need to call it when the user presses the left arrow key.

// src/main.rs -> inside the main function's loop

match key_event.code {
    KeyCode::Char('q') if key_event.modifiers == KeyModifiers::CONTROL => {
        break;
    }

    KeyCode::Up => editor.move_cursor(Direction::Up),
    KeyCode::Down => editor.move_cursor(Direction::Down),
    // --- THIS IS THE CHANGE FOR OUR TASK ---
    KeyCode::Left => editor.move_cursor(Direction::Left),
    // ----------------------------------------
    KeyCode::Right => todo!(),

    _ => {}
}

Testing Your Work

You have now implemented leftward movement. Run your editor with cargo run. The cursor starts at (0, 0). If you press the left arrow key, nothing will happen. This is correct! Your else if self.cursor_y > 0 condition correctly prevents the cursor from trying to wrap to a non-existent line -1.

While we can’t fully test the wrapping behavior until we can add text, the logic is sound, safe, and ready for when our editor becomes more capable.

Next Steps

You’ve successfully implemented leftward movement. The final piece of the basic movement puzzle is to implement its counterpart. In the next task, you will tackle Direction::Right, which will involve incrementing self.cursor_x and handling the logic for wrapping to the beginning of the next line.


Further Reading

  • String Length: len() vs. chars().count(): The len() method on a String returns its size in bytes. For simple ASCII characters, 1 character = 1 byte. For complex Unicode characters (like ‘é’ or ‘😊’), one character can be multiple bytes. While len() is fine for now, it’s good to know about my_string.chars().count() for counting actual characters. We’ll revisit this when making our editor more robust.
  • if/else Expressions in Rust: A closer look at how conditional logic, including else if, is structured in Rust.

Implement Rightward Cursor Movement in Rust Text Editor

Mục tiêu: Implement the logic for moving the cursor to the right. This involves handling two scenarios: incrementing the cursor’s x-position on the same line, and wrapping it to the start of the next line when at the end of a line. The implementation requires safe boundary checks to prevent panics.


You have done a stellar job implementing leftward cursor movement. Your move_cursor method now expertly handles both decrementing the cursor’s position on a line and wrapping it to the end of the previous line. With this logic in place, we are now ready to complete the horizontal axis by implementing the final direction: moving right.

This task is the mirror image of the previous one, involving similar logic but with its own distinct boundary conditions. Just like moving left, moving right has two primary behaviors depending on the cursor’s position.

Two Scenarios for Moving Right

  1. In the Middle of a Line: If the cursor is not at the very end of a line, moving right is a simple matter of incrementing self.cursor_x by one.
  2. At the End of a Line: If the cursor is positioned right after the last character of a line, moving right should “wrap” the cursor to the beginning of the next line. This is only possible if a next line exists. If so, we must increment self.cursor_y and reset self.cursor_x to 0.

Implementing Rightward Movement Safely

To implement this logic correctly, we first need to know the length of the current line to determine if the cursor is at its end. The safest way to get the current line is to use self.rows.get(self.cursor_y), which returns an Option<&String>. This prevents our program from crashing if the text buffer is empty.

Our logic will be structured as follows:

  • First, we’ll safely get the current line.
  • If a line exists, we check if cursor_x is less than the line’s length. If it is, we’re in Scenario 1 and can safely increment cursor_x.
  • If cursor_x is not less than the line’s length (meaning it’s at the end), we check if we’re on the last line of the file. If we are not, we’re in Scenario 2 and can wrap to the next line.

Let’s replace the final todo!() in our move_cursor method with this robust logic.

Here is the change for the impl Editor block in src/main.rs.

// src/main.rs -> inside the impl Editor block

fn move_cursor(&mut self, direction: Direction) {
    match direction {
        Direction::Up => {
            if self.cursor_y > 0 {
                self.cursor_y -= 1;
            }
        }
        Direction::Down => {
            let num_rows = self.rows.len();
            if num_rows > 0 && self.cursor_y < num_rows - 1 {
                self.cursor_y += 1;
            }
        }
        Direction::Left => {
            if self.cursor_x > 0 {
                self.cursor_x -= 1;
            } else if self.cursor_y > 0 {
                self.cursor_y -= 1;
                self.cursor_x = self.rows[self.cursor_y].len();
            }
        }
        // --- THIS IS THE CHANGE FOR OUR TASK ---
        Direction::Right => {
            // We use `get` to safely access the current row, preventing a panic if the file is empty.
            if let Some(row) = self.rows.get(self.cursor_y) {
                // Scenario 1: The cursor is somewhere in the middle of the line.
                // We check if the cursor's x-position is less than the length of the current line's string.
                if self.cursor_x < row.len() {
                    self.cursor_x += 1;
                // Scenario 2: The cursor is at the end of the line, so we try to wrap to the next one.
                // We must check that we are not on the very last line of the file.
                } else if self.cursor_y < self.rows.len() - 1 {
                    // Move down to the next line.
                    self.cursor_y += 1;
                    // Move the cursor to the beginning of the new line.
                    self.cursor_x = 0;
                }
            }
        }
        // ----------------------------------------
    }
}

Code and Concept Deep Dive

  • if let Some(row) = self.rows.get(self.cursor_y): This is our safety net. The entire block of logic for moving right will only execute if there is a line at the cursor’s current y position. For an empty file, self.rows.get(0) returns None, so nothing happens, which is the correct behavior.
  • if self.cursor_x < row.len(): This condition checks if the cursor is to the left of the end of the line. The valid indices for characters in a string of length L are 0 through L-1. The cursor position can be from 0 to L, where L is the position after the last character. So, if cursor_x is less than L, we can safely increment it.
  • else if self.cursor_y < self.rows.len() - 1: This handles the wrap-around. This check is only performed if the first condition was false (meaning self.cursor_x is already at the end of the line). We then ensure that a “next line” exists before we try to move to it. If self.rows.len() is 1, the last index is 0. The condition 0 < 1 - 1 (0 < 0) is false, correctly preventing us from moving down.

Wiring It Up in the Event Loop

Our move_cursor method is now complete for all four basic directions. The final step is to connect this new logic to the event loop in main.

// src/main.rs -> inside the main function's loop

match key_event.code {
    KeyCode::Char('q') if key_event.modifiers == KeyModifiers::CONTROL => {
        break;
    }

    KeyCode::Up => editor.move_cursor(Direction::Up),
    KeyCode::Down => editor.move_cursor(Direction::Down),
    KeyCode::Left => editor.move_cursor(Direction::Left),
    // --- THIS IS THE CHANGE FOR OUR TASK ---
    KeyCode::Right => editor.move_cursor(Direction::Right),
    // ----------------------------------------

    _ => {}
}

Milestone Reached: Full Basic Movement

An enormous congratulations! You have now implemented all four cardinal directions for cursor movement. Your editor is now truly interactive. Run the application with cargo run. While you still start with an empty file (so you can’t move anywhere just yet), the underlying logic is complete, robust, and safe. You have successfully separated the concerns of event detection from state manipulation, and your state-driven rendering loop will ensure that any change you make to cursor_x or cursor_y is immediately reflected on screen.

Next Steps

You’ve probably noticed a potential issue. If you have a short line of text followed by a long one, and you move the cursor down from the long line to the short one, the horizontal cursor position (cursor_x) might be larger than the length of the short line. This would place the cursor in an invalid position. Our next task is to address exactly this problem: we will add logic to ensure cursor_x does not go past the end of the current line’s length when moving up or down.


Further Reading

Implement Cursor Snapping for Vertical Movement

Mục tiêu: Fix an edge case in the text editor’s cursor movement. Implement logic to ‘snap’ the cursor’s horizontal position to the end of a line when moving vertically from a longer line to a shorter one, preventing an invalid cursor state.


In our last few tasks, you have brilliantly assembled a complete and robust move_cursor method, handling all four cardinal directions with their unique boundary conditions. Your editor is now genuinely interactive. However, as with any complex system, perfecting the details is what transforms a functional piece of software into a great one. We’ve encountered a subtle but important edge case that we need to address.

The Problem: Ragged Edges and a Wandering Cursor

Imagine this scenario: you have two lines of text.

  1. Hello, this is a very long line.
  2. A short line.

You move your cursor to the end of the first line, so cursor_x is at a high value, let’s say 30. Now, you press the Down Arrow key. Our current logic correctly increments cursor_y to 1. However, it doesn’t change cursor_x. The editor’s internal state now thinks the cursor is at column 30 on a line that is only 12 characters long.

This leads to inconsistent and buggy behavior. The physical cursor might appear at the end of the short line, but our editor’s logical state is out of sync. If you were to start typing, the character might appear in an unexpected place. A professional editor must handle this gracefully. The expected behavior is that the horizontal cursor position “snaps” to the end of the shorter line.

Our current task is to implement this snapping logic.

Implementing the Cursor “Snap”

The fix for this is to add a check after we’ve moved the cursor up or down. Once the cursor_y has been updated to its new value, we need to:

  1. Get the length of the string at the new cursor_y.
  2. Compare the current cursor_x with that length.
  3. If cursor_x is greater than the line’s length, we must update cursor_x to be equal to the line’s length.

This ensures that the cursor is never in an invalid horizontal position. We will add this logic to both the Direction::Up and Direction::Down arms of our move_cursor method.

Here are the changes for your impl Editor block. Notice how we are adding the same block of logic in two places.

// src/main.rs -> inside the impl Editor block

fn move_cursor(&mut self, direction: Direction) {
    match direction {
        Direction::Up => {
            if self.cursor_y > 0 {
                self.cursor_y -= 1;
            }
        }
        Direction::Down => {
            let num_rows = self.rows.len();
            if num_rows > 0 && self.cursor_y < num_rows - 1 {
                self.cursor_y += 1;
            }
        }
        Direction::Left => {
            // ... (this logic is unchanged)
            if self.cursor_x > 0 {
                self.cursor_x -= 1;
            } else if self.cursor_y > 0 {
                self.cursor_y -= 1;
                self.cursor_x = self.rows[self.cursor_y].len();
            }
        }
        Direction::Right => {
            // ... (this logic is unchanged)
            if let Some(row) = self.rows.get(self.cursor_y) {
                if self.cursor_x < row.len() {
                    self.cursor_x += 1;
                } else if self.cursor_y < self.rows.len() - 1 {
                    self.cursor_y += 1;
                    self.cursor_x = 0;
                }
            }
        }
    }

    // --- NEW CODE FOR THIS TASK ---
    // This logic is applied after every movement to snap the cursor to the end of the line.

    // Safely get the length of the line the cursor is now on.
    // If the line exists, we get its length. If not (e.g., empty buffer), we default to 0.
    let current_row_len = if let Some(row) = self.rows.get(self.cursor_y) {
        row.len()
    } else {
        0
    };

    // If the cursor's horizontal position is now past the end of the line,
    // snap it to the end of the line.
    if self.cursor_x > current_row_len {
        self.cursor_x = current_row_len;
    }
}

Code and Concept Deep Dive

Let’s refactor our move_cursor method a bit. Instead of adding the same code to both the Up and Down arms, we can be more efficient. Since this “snapping” logic needs to happen after any vertical movement, we can place it once at the very end of the move_cursor method. It will run after any direction is processed, but it will only have a meaningful effect when cursor_y has changed and cursor_x might be out of bounds. This is a great example of the DRY (Don’t Repeat Yourself) principle.

  • let current_row_len = if let Some(row) = ...: This is a powerful and safe way to get the length. We use .get(self.cursor_y) to avoid any panics.
    • If a row exists at the new cursor_y, the if let succeeds, and we assign row.len() to current_row_len.
    • If no row exists (which can happen if the buffer is empty), the else block triggers, and we assign 0 to current_row_len. This makes our logic robust even for an empty file.
  • if self.cursor_x > current_row_len: This is the core check. It directly translates our problem statement into code: “Is the horizontal cursor position greater than the length of the current line?”
  • self.cursor_x = current_row_len: If the check is true, we perform the “snap” by correcting the value of cursor_x.

Milestone Achieved!

An enormous congratulations! You have now completed the entire “Implement basic cursor movement” step. Your editor’s cursor now moves up, down, left, and right, handling all the tricky edge cases like wrapping around lines and snapping to the correct position on lines of different lengths. This is a truly significant accomplishment and forms the bedrock of all user interaction to come.

Your state-driven architecture is paying huge dividends. You made these complex logical changes entirely within the Editor’s methods, and the rendering loop you built earlier will automatically reflect these changes on screen without any modification. This is the mark of a well-designed application.

Next Steps

With a fully mobile cursor, the user’s next immediate desire will be to change the text. Our next major step in the project roadmap is “Implement basic text editing features.” We will begin by tackling the most fundamental editing action: inserting a character when the user presses a key.


Further Reading

Conceptual Review: State-Driven Architecture

Mục tiêu: A conceptual task to understand and appreciate the Model-View-Controller (MVC) architecture and state-driven design implemented for cursor movement in the text editor. No new code is required.


You have done an absolutely outstanding job. In the previous task, you implemented the final piece of the cursor movement logic: the “snap” that ensures the horizontal cursor position remains valid when moving between lines of different lengths. With that, your move_cursor method is now a complete, robust, and intelligent piece of machinery. You have officially built the entire logical foundation for cursor navigation.

This brings us to the final, and perhaps most rewarding, task of this step. This task is different; it’s not about writing new code. Instead, it’s about taking a moment to appreciate the architectural elegance of what you have built and to understand why all the work you just did in move_cursor will “just work” without touching a single line of your rendering code.

The Magic of State-Driven Rendering

When you run your editor and press the arrow keys, the cursor will move. This might seem obvious, but there’s a beautiful simplicity in how it’s achieved. You never wrote any code that says, “When the up arrow is pressed, move the physical cursor up one spot.” Instead, you created a more powerful, decoupled system. This is the essence of state-driven design.

Let’s break down the flow of a single key press to see this principle in action:

  1. The Event: You press the Down Arrow key.
  2. The Controller (main loop): Your main function’s loop is the Controller. It’s waiting for input.

    • event::read() captures the key press.
    • The match statement recognizes KeyCode::Down.
    • It executes the line: editor.move_cursor(Direction::Down);.
    • At this moment, the Controller’s job is done. It has translated user intent into a command for the model.
  3. The Model (Editor struct): Your Editor struct and its methods are the Model. It is the single source of truth for the entire application’s state.

    • The move_cursor method is called.
    • It checks the boundaries and, if valid, executes self.cursor_y += 1;.
    • It then runs the “snap” logic at the end to ensure self.cursor_x is valid for the new line.
    • The Editor struct’s state is now updated. cursor_y has a new value. The Model is now internally consistent and correct, but nothing has changed on the screen yet.
  4. The View (refresh_screen): The loop in main begins its next iteration. The very first line is the key to everything:

    • editor.refresh_screen()?;
    • This method is your View. Its only job is to look at the current state of the Model (editor) and make the terminal look exactly like it. It doesn’t care how the state changed; it only cares about what the state is right now.
    • It clears the screen, calls draw_rows(), and then executes this crucial line you wrote in the previous step:

    rust // This line is inside your refresh_screen method execute!( stdout(), cursor::MoveTo(self.cursor_x as u16, self.cursor_y as u16) )?; * This command reads the new, updated values of self.cursor_x and self.cursor_y from the Model and tells the terminal to place its physical cursor at that exact spot.

This is the “magic.” Because your rendering logic is completely decoupled from your state-modification logic, any change you make to the state is automatically and correctly reflected on the screen in the very next frame. You don’t need to manually synchronize anything. The loop guarantees it.

Milestone Achieved: Fully Interactive Movement

An enormous congratulations is in order! You have successfully completed a major milestone in your project. You have implemented a fully-featured, robust cursor movement system. Your editor is no longer a static painting; it’s an interactive canvas.

The architectural patterns you have established here—separating the Controller, Model, and View—are fundamental to building complex, maintainable applications. The dividends from this clean design will continue to pay off as we add more features.

Next Steps

With a cursor that can navigate anywhere, the very next thing a user will want to do is modify the text. The foundation is perfectly set for our next major step: Implement basic text editing features. We will dive right into the heart of the editor by adding a case for KeyCode::Char(c) to our event loop and creating an insert_char method to handle typing.


Further Reading

To deepen your understanding of the powerful architecture you’ve just proven, explore these concepts:

  • Model-View-Controller (MVC) Pattern: The architecture you’ve built is a classic example of the MVC pattern. Understanding the formal definitions of this pattern will provide a strong mental framework for future projects.

  • Unidirectional Data Flow: Your application follows this principle. Events flow in one direction (input -> state change -> render), which makes the application easier to reason about and debug. This is a core concept in many modern UI frameworks like React.

  • Decoupling in Software Design: This is the principle of designing components so that they can be changed independently of one another. Your Editor methods can change without refresh_screen knowing the details, and vice-versa.