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:
- Organization: All the important data for your application lives in one well-defined place.
- Clarity: It’s easy to see at a glance all the pieces of information that define the editor’s current state.
- 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
- Defining and Instantiating Structs (The Rust Programming Language Book): This is the definitive guide to understanding structs in Rust.
- Managing Complexity with State Structs: A conceptual article on the benefits of centralizing application state.
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:
- Indexing:
usizeis the type used for indexing into collections like vectors and strings. Since our text buffer will eventually be a vector of strings,cursor_ywill directly correspond to an index in that vector, andcursor_xwill correspond to a byte index within a string. Usingusizemakes this interaction seamless and avoids unnecessary type casting. - Architecture-Dependent Size:
usizeis a pointer-sized unsigned integer. This means on a 32-bit system, it’s au32, and on a 64-bit system, it’s au64. 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. - Non-Negative: Since cursor positions and screen dimensions can never be negative, an unsigned integer type is a perfect fit.
usizeenforces 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
- Primitive Data Types in Rust (The Rust Book): A great overview of Rust’s built-in types, including the integer types like
usize. - Rust
std::primitive::usizeDocumentation: The official documentation for theusizetype. - Structs in Rust By Example: A more hands-on, example-driven look at how to define and use structs.
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
- Structs in Rust (The Rust Programming Language Book): A great refresher on how to define and use structs.
crossterm::terminal::size()function: Take a sneak peek at thecrosstermfunction we will eventually use to get the terminal dimensions. Notice how it returns a(u16, u16), which we will convert tousize.
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: AVec(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 aString. AStringin 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: usizefield is a direct index into therowsvector, telling us which line the cursor is on. - The
cursor_x: usizefield is an index into theStringatrows[cursor_y], telling us the column of the cursor on that line. - The
screen_rowsandscreen_colsfields define the size of the viewport or “window” through which we will view a portion of therowsbuffer.
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:
std::vec::Vec- The Rust Vector: The official documentation for Rust’s growable list type.std::string::String- The Rust String: The official documentation for theStringtype.- Common Rust Collections (The Rust Book): A fantastic chapter covering
Vec,String, andHashMap, which are essential tools for any Rust programmer.
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?
- Cursor: The cursor should start at the very beginning of the document, so
cursor_xandcursor_ywill be initialized to0. - Text Buffer: The editor should start with a blank slate, so the
rowsfield will be an emptyVec<String>. We can create this withVec::new(). - 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,
crosstermprovides the perfect tool for this: thecrossterm::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 toEditorwill go inside this block.fn new() -> std::io::Result<Self>: This defines our associated functionnew. It takes no arguments and returns aResultthat, on success, contains a newEditorinstance.let (cols, rows) = terminal::size()?;: We call thecrosstermfunction. If it succeeds, the tuple(u16, u16)is destructured, and its values are bound to thecolsandrowsvariables. If it fails, the?operator causes ournewfunction to immediately return the error.Ok(Self { ... }): We create theEditorinstance 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 theu16value fromcolsinto ausizevalue 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:
- Methods and Associated Functions (The Rust Book): A detailed explanation of
implblocks, methods (which take&self), and associated functions (which don’t). - The
SelfKeyword (Rust by Example): A clear explanation of howSelf(with a capital S) is used as an alias for the current type. - Primitive Casting (The Rust Reference): For a technical look at how the
askeyword works for type conversions.
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 namededitor. Themutkeyword is critical. It signals our intent to the Rust compiler that this variable’s value (theEditorstruct instance) will be modified later in the program. Withoutmut, any attempt to changeeditor.cursor_xor add toeditor.rowswould 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. IfEditor::new()returnsOk(editor_instance), the?unwraps it, and theeditor_instanceis assigned to oureditorvariable. IfEditor::new()returns anErr(io_error), the?will immediately stop themainfunction and return that error, ensuring ourRawModeGuardcleans 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:
- Designed a comprehensive
Editorstruct to hold all application state. - Implemented a robust constructor that initializes this state with live data.
- 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:
- Variables and Mutability (The Rust Programming Language Book): The definitive chapter on
letandmut. - What is Ownership? (The Rust Programming Language Book): Understanding ownership is key to understanding why Rust’s safety guarantees work.