Define the Central Editor State Struct

Mục tiêu: Introduce the central state pattern by defining an empty Editor struct in Rust. This struct will serve as the primary data container for the text editor’s state, improving organization and maintainability.


You have done an exceptional job building the heart of our application! The event loop you constructed in the last step is the engine that will power every feature we add. It patiently listens for user input, knows how to exit gracefully, and can distinguish between different types of events.

Now, we need to shift our focus from actions (the event loop) to data (the application’s state). An engine is useless without a car to drive, and in our case, the “car” is the collection of all the information our editor needs to track: the text in the file, the cursor’s position, the size of the screen, and more.

The Central State Pattern

As applications grow, managing data can become chaotic. If variables are scattered throughout the code, it becomes difficult to understand how they relate to each other and how they change over time. A powerful and common architectural pattern to solve this is to consolidate all of the application’s state into a single, central data structure.

This approach has several key benefits:

  1. Organization: All the important data for your application lives in one well-defined place.
  2. Clarity: It’s easy to see at a glance all the pieces of information that define the editor’s current state.
  3. Maintainability: When you need to pass the application’s state to a function, you can pass this single structure instead of a long list of individual variables.

In Rust, the perfect tool for creating such a data structure is the struct.

Defining a struct in Rust

A struct, short for structure, is a custom data type that lets you group together related values. You can define a struct and then give it named fields, creating a meaningful package for your data. For our project, we will create an Editor struct that will, over the course of this step, become the single source of truth for our entire application.

For this first task, our goal is simply to define the container itself. We will create an empty Editor struct. It won’t hold any data just yet; we are simply creating the blueprint. In the tasks that follow, we will add the fields it needs.

Let’s add the Editor struct definition to src/main.rs. By convention, type definitions like structs and enums are usually placed near the top of the file, after the use statements but before the main function.

// src/main.rs

// ... (use statements remain the same) ...

struct RawModeGuard;

impl Drop for RawModeGuard {
    // ... (this implementation is unchanged) ...
}

// --- NEW CODE FOR THIS TASK ---
// Define the central state structure for our text editor.
// For now, it is empty. We will add fields to it in the upcoming tasks
// to hold things like cursor position, screen dimensions, and the text buffer.
struct Editor {}

fn main() -> std::io::Result<()> {
    // ... (the rest of the main function is unchanged for now) ...
}

That’s it! With this simple addition, you’ve created the foundational data structure for your entire project. While it may seem small, this is a crucial architectural decision that will pay dividends in clarity and organization as we continue to build.

For now, this struct lives in main.rs. As our project grows, we will follow the roadmap and refactor it into its own module (src/editor.rs) to keep our codebase tidy and modular.

Next Steps

We have our container. Now, let’s start filling it. In the very next task, you will add the first two fields to the Editor struct: cursor_x and cursor_y, which will be responsible for tracking the logical position of the user’s cursor on the screen.


Further Reading

Implement Cursor Position Tracking in Editor Struct

Mục tiêu: Update the Editor struct in Rust to include cursor\_x and cursor\_y fields of type usize. This task establishes the initial state for tracking the logical position of the cursor within the text editor.


Excellent, you’ve successfully laid the architectural groundwork by creating the Editor struct. In our last task, we defined this struct as an empty container, a blueprint for the central state of our application. Now, it’s time to start filling that container with the essential data it needs to function.

Tracking the Cursor: The Logical Position

The very first piece of state any text editor needs to track is the cursor’s position. This isn’t just about the blinking character on the screen; it’s about the conceptual insertion point within the text buffer. We’ll call this the logical cursor. It’s our editor’s internal understanding of “where the user is” in the file.

To represent this, we need two coordinates: a horizontal position (the column) and a vertical position (the row or line number). We will add two fields to our Editor struct to store these values:

  • cursor_x: To represent the column number.
  • cursor_y: To represent the row number.

Choosing the Right Data Type: usize

A crucial decision when defining these fields is choosing their data type. We will use usize. In Rust, usize is the idiomatic and most appropriate type for several reasons:

  1. Indexing: usize is the type used for indexing into collections like vectors and strings. Since our text buffer will eventually be a vector of strings, cursor_y will directly correspond to an index in that vector, and cursor_x will correspond to a byte index within a string. Using usize makes this interaction seamless and avoids unnecessary type casting.
  2. Architecture-Dependent Size: usize is a pointer-sized unsigned integer. This means on a 32-bit system, it’s a u32, and on a 64-bit system, it’s a u64. It is guaranteed to be large enough to represent the size of any object in memory, which means it will be more than sufficient for any conceivable file or screen dimension.
  3. Non-Negative: Since cursor positions and screen dimensions can never be negative, an unsigned integer type is a perfect fit. usize enforces this at the type level.

Updating the Editor Struct

Let’s modify the Editor struct definition in src/main.rs to include these new fields.

// src/main.rs

// ... (use statements, RawModeGuard, and its impl Drop remain the same) ...

// Define the central state structure for our text editor.
struct Editor {
    // cursor_x tracks the horizontal position (column) of the cursor.
    cursor_x: usize,
    // cursor_y tracks the vertical position (row) of the cursor.
    cursor_y: usize,
}

fn main() -> std::io::Result<()> {
    // ... (the rest of the main function is unchanged for now) ...
}

With this change, our Editor struct is no longer empty. It now has a well-defined purpose: to hold the state of the editor. We’ve started with the fundamental components of cursor position. Any function that needs to know where the cursor is can now simply look at these fields within an Editor instance.

Next Steps

Knowing the cursor’s position is a great start, but it’s only meaningful in the context of the visible screen area. How do we know if the cursor is at the edge of the screen? For that, we need to know the screen’s dimensions. In the next task, we will add two more fields to our Editor struct to store the width and height of the terminal window.


Further Reading

Track Terminal Dimensions in Editor State

Mục tiêu: Modify the Editor struct in Rust to include screen\_cols and screen\_rows fields. These usize fields will store the terminal window’s width and height, giving the editor awareness of its environment for features like rendering and cursor bounds.


Excellent! You’ve successfully added fields to the Editor struct to track the cursor’s logical position. This is the first step in giving our editor a sense of self-awareness.

Building directly on that, we now need to give the editor awareness of its environment. While knowing the cursor’s (x, y) coordinates is vital, those coordinates are only meaningful when placed in the context of the visible screen area. Is the cursor at the top-left? The bottom-right? Near an edge? We can’t answer these questions without knowing the dimensions of the terminal window itself.

Establishing the Boundaries: Tracking Screen Dimensions

Our current task is to store the terminal window’s dimensions—its width (number of columns) and height (number of rows)—within our Editor state. This information is critical for countless features we will build later, including:

  • Rendering: Knowing how many rows we can draw on the screen at once.
  • Cursor Movement: Preventing the user from moving the cursor outside the visible area.
  • Word Wrapping: Breaking long lines of text so they fit within the screen’s width.
  • UI Elements: Correctly positioning a status bar at the bottom of the screen or a title bar at the top.

We will add two new fields to our Editor struct to hold this information:

  • screen_cols: To store the width of the screen in character columns.
  • screen_rows: To store the height of the screen in character rows.

Just as with the cursor positions, the most appropriate data type for these fields is usize. Screen dimensions, like cursor coordinates, are always non-negative, and the crossterm functions we will later use to query the terminal size return values that map perfectly to usize. Using the same type maintains consistency and simplifies calculations between cursor positions and screen boundaries.

Updating the Editor Struct

Let’s modify the Editor struct definition in src/main.rs to include these two new fields.

// src/main.rs

// ... (use statements, RawModeGuard, and its impl Drop remain the same) ...

// Define the central state structure for our text editor.
struct Editor {
    // cursor_x tracks the horizontal position (column) of the cursor.
    cursor_x: usize,
    // cursor_y tracks the vertical position (row) of the cursor.
    cursor_y: usize,
    // --- NEW FIELDS FOR THIS TASK ---
    // screen_cols stores the number of columns (width) of the terminal window.
    screen_cols: usize,
    // screen_rows stores the number of rows (height) of the terminal window.
    screen_rows: usize,
}

fn main() -> std::io::Result<()> {
    // ... (the rest of the main function is unchanged for now) ...
}

Our Editor struct is growing more capable. It now has a complete understanding of its own spatial context: where the cursor is and the size of the canvas it has to work with.

You may be wondering, where do the values for these fields come from? For now, we are just defining the “shape” of our state. In a subsequent task within this step, we will write a constructor function (Editor::new()) that actively queries the terminal for its size using crossterm and populates these fields with live data upon startup.

Next Steps

Our editor state now knows about the cursor and the screen, but it’s missing the most crucial element of a text editor: the text itself! In our next task, we will add the final piece to our core data structure: a field to hold the text content of the file, which we will represent as a vector of strings.


Further Reading

Define the Editor’s Text Buffer

Mục tiêu: Modify the Editor struct in a Rust-based text editor to include a text buffer. This involves adding a rows field of type Vec to store the lines of the document being edited.


You’ve done an excellent job building out the Editor struct. In the previous tasks, we gave it a sense of self by defining its cursor position and a sense of its environment by defining the screen dimensions. It now understands where it is and the size of the space it’s in. Now, we must add the most critical component: the data it is meant to edit. We need to add the text buffer.

Storing the Document: The Text Buffer

A text editor is, at its core, a program for manipulating a sequence of characters. We need a data structure to hold this sequence in memory. While we could theoretically store the entire file in one giant String, a much more effective and intuitive approach for a line-based editor is to represent the file as a list of lines.

This model maps perfectly to how users think about text files and simplifies countless operations we’ll need to implement later, such as: * Moving the cursor up or down (incrementing/decrementing a line index). * Inserting a new line (inserting a new string into the list). * Deleting a line (removing a string from the list). * Rendering the screen (iterating through the list of lines and drawing each one).

The ideal Rust data structure for a “list of lines” is a Vec<String>.

  • Vec: A Vec (short for vector) is Rust’s standard, growable list type. It’s a collection that stores its elements on the heap, allowing it to expand or shrink as we add or remove lines of text. This is perfect for our needs, as we don’t know the size of the file in advance.
  • String: Each element in our vector will be a String. A String in Rust is a heap-allocated, mutable, UTF-8 encoded text type. It’s the standard way to represent a piece of text that can be changed, which is exactly what a line in our editor is.

So, Vec<String> literally translates to “a growable list of editable text lines.” Let’s add this final, crucial field to our Editor struct.

Updating the Editor Struct

Modify the Editor struct definition in src/main.rs to include the new rows field.

// src/main.rs

// ... (use statements, RawModeGuard, and its impl Drop remain the same) ...

// Define the central state structure for our text editor.
struct Editor {
    cursor_x: usize,
    cursor_y: usize,
    screen_cols: usize,
    screen_rows: usize,
    // --- NEW FIELD FOR THIS TASK ---
    // `rows` is a Vector of Strings, where each String represents a single line
    // of text in the file being edited. This is our text buffer.
    rows: Vec<String>,
}

fn main() -> std::io::Result<()> {
    // ... (the rest of the main function is unchanged for now) ...
}

With this addition, the blueprint for our editor’s state is now complete. Let’s take a moment to appreciate how these fields work together to form a coherent model:

  • The rows: Vec<String> field is the source of truth for the document’s content.
  • The cursor_y: usize field is a direct index into the rows vector, telling us which line the cursor is on.
  • The cursor_x: usize field is an index into the String at rows[cursor_y], telling us the column of the cursor on that line.
  • The screen_rows and screen_cols fields define the size of the viewport or “window” through which we will view a portion of the rows buffer.

You have successfully defined all the core data fields our editor needs to get started.

Next Steps

Our Editor struct is now perfectly defined, but it’s still just a blueprint. We don’t have a way to create an actual instance of it and initialize its fields with meaningful starting values. In our next task, we will write a constructor, an associated function Editor::new(), which will query the terminal for its size and create a ready-to-use Editor instance.


Further Reading

To deepen your understanding of the powerful collection types we’ve just used, check out their official documentation:

Implement the Editor Constructor in Rust

Mục tiêu: Create an associated function Editor::new() to act as a constructor for the Editor struct. This function will initialize the editor’s state by setting the cursor to (0,0), creating an empty text buffer, and dynamically fetching the terminal dimensions using crossterm::terminal::size(). The constructor will return a Result to handle potential errors during initialization.


You have done an absolutely brilliant job defining the complete data model for our editor. In the last few tasks, you created the Editor struct, a blueprint that perfectly describes all the essential pieces of state our application needs: the cursor’s position, the screen’s dimensions, and the text buffer itself.

However, a blueprint is just a plan. We haven’t yet constructed anything from it. Our application has no way to create an actual Editor instance and populate its fields with meaningful, real-world data. This task is all about bridging that gap. We will create a constructor, a special function whose job is to build a new, initialized Editor instance, ready for use.

Constructors in Rust: The new Convention

Unlike some other object-oriented languages, Rust does not have a special keyword like constructor. Instead, the idiomatic Rust way to create a constructor is by implementing an associated function for the struct. An associated function is a function that belongs to a type (like Editor) rather than an instance of that type. The universal convention is to name this function new.

We’ll define Editor::new() inside an impl block. This impl Editor { ... } block is where all the methods and associated functions for our Editor struct will live.

The Logic of Initialization

What does a brand-new editor state look like?

  1. Cursor: The cursor should start at the very beginning of the document, so cursor_x and cursor_y will be initialized to 0.
  2. Text Buffer: The editor should start with a blank slate, so the rows field will be an empty Vec<String>. We can create this with Vec::new().
  3. Screen Dimensions: This is the most dynamic part. We can’t hardcode the screen size because every user’s terminal will be different. We need to query the terminal at runtime to get its current dimensions. Thankfully, crossterm provides the perfect tool for this: the crossterm::terminal::size() function.

The terminal::size() function returns a Result<(u16, u16)>. Let’s break that down: * It’s a Result because querying the terminal size might fail (e.g., if the program is not being run in an interactive terminal). * On success, it contains a tuple (u16, u16), representing (columns, rows).

Because this function can fail, our Editor::new() function must also be able to report failure. Therefore, it will also return a Result. A common pattern for constructors is to return a Result<Self>, where Self is a special keyword that acts as an alias for the type you are inside—in this case, Editor.

Implementing Editor::new()

Let’s add the impl block and the new function to src/main.rs. This code should go after the Editor struct definition but before the main function.

// src/main.rs

// ... (use statements, RawModeGuard, and its impl Drop remain the same) ...

struct Editor {
    cursor_x: usize,
    cursor_y: usize,
    screen_cols: usize,
    screen_rows: usize,
    rows: Vec<String>,
}

// --- NEW CODE FOR THIS TASK ---

// This `impl` block is where we'll define methods and associated functions for the Editor struct.
impl Editor {
    // `new` is the constructor for our Editor. It returns a Result, as querying
    // the terminal size can fail.
    // `Self` is an alias for the type in the `impl` block, which is `Editor`.
    fn new() -> std::io::Result<Self> {
        // Query the terminal for its size. This returns a Result.
        // We use the `?` operator to propagate any error.
        // The result is a tuple `(columns, rows)`. We use `u16` for now.
        let (cols, rows) = terminal::size()?;

        // Return a new instance of Editor, wrapped in `Ok`.
        Ok(Self {
            // Start the cursor at the top-left corner.
            cursor_x: 0,
            cursor_y: 0,
            // Set the screen dimensions from the queried size.
            // Note the type cast from `u16` to `usize`. `usize` is the correct
            // type for indexing, and our struct fields are defined as such.
            screen_cols: cols as usize,
            screen_rows: rows as usize,
            // Start with an empty text buffer. `Vec::new()` creates an empty vector.
            rows: Vec::new(),
        })
    }
}

fn main() -> std::io::Result<()> {
    // ... (the rest of the main function is unchanged for now) ...
}

Code Breakdown

  • impl Editor { ... }: This is how you start defining behavior for a struct. All functions related to Editor will go inside this block.
  • fn new() -> std::io::Result<Self>: This defines our associated function new. It takes no arguments and returns a Result that, on success, contains a new Editor instance.
  • let (cols, rows) = terminal::size()?;: We call the crossterm function. If it succeeds, the tuple (u16, u16) is destructured, and its values are bound to the cols and rows variables. If it fails, the ? operator causes our new function to immediately return the error.
  • Ok(Self { ... }): We create the Editor instance using struct literal syntax. We explicitly set each field to its initial value.
    • screen_cols: cols as usize: This is a type cast. We are explicitly converting the u16 value from cols into a usize value to match the type of our struct field. This is necessary because Rust does not perform automatic numeric conversions.
    • Since the struct creation is the last expression in the function and is wrapped in Ok(), it becomes the successful return value.

You have now created a fully functional constructor. This is a massive step forward. We now have a reliable, repeatable way to create a fully configured Editor state that is aware of its environment right from the moment of its creation.

Next Steps

Our constructor is ready, but it’s not being used yet. Our main function knows nothing about the Editor struct. In the final task of this step, you will put your new constructor to work by calling Editor::new() inside main and creating the central state instance that will drive the rest of the application.


Further Reading

To learn more about the concepts introduced in this task, check out these resources:

Instantiate the Application State in Rust

Mục tiêu: Create a mutable instance of the Editor struct within the main function. This involves calling the Editor::new() constructor and using the mut keyword to allow for future state changes.


You have perfectly assembled the blueprint (Editor struct) and the factory (Editor::new()) for your application’s state. In an incredibly short time, you’ve gone from a simple concept to a fully-defined data model with a robust, fallible constructor that can query the terminal for live data. This is a monumental step.

The final piece of this puzzle is to actually use that factory. A constructor that is never called is just dormant code. In this task, we will bring our editor’s state to life by creating an instance of the Editor struct right at the start of our program. This instance will become the central hub of information that our event loop will consult and modify.

Creating the Application’s State

Our main function is the entry point of the application, so it’s the natural place to create the one and only Editor instance. We will call the Editor::new() constructor we just wrote and store the resulting object in a variable. This variable will then hold the entire state of our application for its entire lifetime.

A crucial concept we must introduce here is mutability. By default, all variables in Rust are immutable. This is a core safety feature of the language that prevents accidental changes to data. However, the state of a text editor is, by its very nature, constantly changing: the cursor moves, text is inserted, lines are deleted. Therefore, we must explicitly declare our editor variable as mutable using the mut keyword.

Let’s make this final change for this step in your src/main.rs file.

// src/main.rs

// ... (use statements, RawModeGuard, Editor struct, and impl Editor are unchanged) ...

fn main() -> std::io::Result<()> {
    terminal::enable_raw_mode()?;
    let _raw_mode_guard = RawModeGuard;

    // --- NEW CODE FOR THIS TASK ---
    // Create an instance of the Editor.
    // `mut` makes the variable mutable, as we'll need to change the editor's
    // state in response to user input.
    // We call our constructor `Editor::new()` and use `?` to handle any
    // potential errors during initialization (e.g., failing to get terminal size).
    let mut editor = Editor::new()?;

    execute!(
        stdout(),
        terminal::Clear(terminal::ClearType::All),
        cursor::Hide
    )?;

    // ... (the rest of the event loop is unchanged) ...
    loop {
        // ...
    }

    Ok(())
}

Code Breakdown

Let’s look closely at this powerful new line: let mut editor = Editor::new()?;

  • let mut editor: This declares a new variable named editor. The mut keyword is critical. It signals our intent to the Rust compiler that this variable’s value (the Editor struct instance) will be modified later in the program. Without mut, any attempt to change editor.cursor_x or add to editor.rows would result in a compile-time error.
  • Editor::new(): This is the call to the associated function (our constructor) that we created in the last task.
  • ?: The question mark operator does its job perfectly here. If Editor::new() returns Ok(editor_instance), the ? unwraps it, and the editor_instance is assigned to our editor variable. If Editor::new() returns an Err(io_error), the ? will immediately stop the main function and return that error, ensuring our RawModeGuard cleans up the terminal.

The Unused Variable Warning

After adding this line, if you run cargo check or cargo run, you will see a new warning from the compiler:

warning: unused variable: `editor`
  --> src/main.rs:XX:9
   |
XX |     let mut editor = Editor::new()?;
   |         ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_editor`
   |
   = note: `#[warn(unused_variables)]` on by default

This is not an error! It’s the Rust compiler being helpful. It has noticed that we created a variable but haven’t used it yet. This feature is fantastic for catching bugs and keeping code clean. Do not worry about this warning. We are about to use this editor variable extensively in the very next step.

Milestone Achieved!

Congratulations! You have now completed the entire third step of the project. This was a huge architectural step. You have:

  1. Designed a comprehensive Editor struct to hold all application state.
  2. Implemented a robust constructor that initializes this state with live data.
  3. Instantiated the state at the beginning of your program.

You now have the two core components of any interactive application: a running event loop (the engine) and a centralized state object (the vehicle). The foundation is rock-solid.

Next Steps

With our state object initialized and ready, we can finally start making our editor do something visual. The next major step is to implement the rendering logic. We will create a refresh_screen method that uses the data inside our editor instance to clear the screen and draw the text buffer, welcoming the user with a screen full of tildes (~), just like a professional terminal editor.


Further Reading

To learn more about the fundamental concepts of ownership and mutability in Rust, these resources are essential: