Refactor Editor Struct into a Separate Rust Module

Mục tiêu: Move the Editor struct and its implementation from main.rs into a new editor.rs file. This refactoring task creates a dedicated module for the editor’s core logic, improving the project’s structure and maintainability by separating concerns.


An enormous congratulations on completing the text search feature! You’ve built a multi-stage, interactive, and powerful tool that is a cornerstone of any serious text editor. As you’ve added this and other features like viewport scrolling, you’ve likely noticed that your src/main.rs file has become quite large. It contains the editor’s data structures, all of its logic, terminal handling, and the main application loop.

This is a wonderful problem to have! It means your project has grown in complexity and capability. Now is the perfect time to take a crucial step in software engineering: refactoring for maintainability. We will transform your single-file project into a well-organized, multi-module application. This process won’t change what your editor does, but it will dramatically improve its structure, making it easier to understand, maintain, and expand in the future.

Our very first step is to give the Editor struct its own dedicated home.

The Power of Modules in Rust

In Rust, the primary way to organize code and control privacy is through the module system. A module is a namespace that can contain function definitions, structs, enums, traits, and even other modules. By default, everything inside a module is private to that module. This is a powerful feature that allows you to create a clean public API—the parts of your code you intentionally expose for others to use—while hiding the internal implementation details.

Our goal is to create a new module, editor, which will be the single source of truth for the editor’s state and behavior. All the logic you’ve written to manipulate the cursor, draw the screen, handle text, and search will live together in one cohesive unit.

Step 1: Create the editor.rs File

This is the simplest step. In your project’s src directory, alongside main.rs, create a new file named:

src/editor.rs

This file will become our editor module.

Step 2: Move the Editor and its Implementation

Now, we will move all the code related to the Editor from main.rs into our new editor.rs file.

  1. Cut the entire Editor struct definition (from struct Editor { to the closing }).
  2. Cut the entire impl Editor block (from impl Editor { to its final closing }).
  3. Paste both of these, in the same order, into your new, empty src/editor.rs file.

Your main.rs file will now be much smaller, but both files will have many compilation errors. This is expected, and we will fix it in the next steps.

Step 3: Making the Module Self-Sufficient

Your new editor.rs file now contains code that refers to things like crossterm, stdout, and PathBuf, but it doesn’t know what they are. Each file in a Rust module system needs to declare its own dependencies. We’ll add the necessary use statements to the top of editor.rs.

Additionally, we need to make the Editor struct and its methods public so that main.rs can use them. We do this with the pub keyword.

Here is the complete code for your new src/editor.rs file. It includes the required use statements and the pub keyword in front of the struct and all its methods.

// src/editor.rs

use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
    style::{self, Stylize},
    terminal,
};
use std::{
    fs,
    io::{stdout, Stdout, Write},
    path::PathBuf,
};

pub struct Editor {
    cursor_x: usize,
    cursor_y: usize,
    screen_rows: usize,
    screen_cols: usize,
    rows: Vec<String>,
    filename: Option<PathBuf>,
    status_message: String,
    dirty: bool,
    row_offset: usize,
    col_offset: usize,
    last_search_query: Option<String>,
    search_results: Vec<(usize, usize)>,
    current_search_match: usize,
}

impl Editor {
    pub fn new(filename: Option<String>) -> Self {
        let (cols, rows) = terminal::size().unwrap();
        let mut editor = Self {
            cursor_x: 0,
            cursor_y: 0,
            screen_rows: rows as usize,
            screen_cols: cols as usize,
            rows: Vec::new(),
            filename: filename.map(PathBuf::from),
            status_message: String::new(),
            dirty: false,
            row_offset: 0,
            col_offset: 0,
            last_search_query: None,
            search_results: Vec::new(),
            current_search_match: 0,
        };
        if let Some(path) = &editor.filename {
            if let Ok(contents) = fs::read_to_string(path) {
                editor.rows = contents.lines().map(|s| s.to_string()).collect();
            }
        }
        editor
    }

    pub fn refresh_screen(&self) {
        let mut stdout = stdout();
        stdout.queue(cursor::Hide).unwrap();
        stdout
            .queue(terminal::Clear(terminal::ClearType::All))
            .unwrap();
        stdout.queue(cursor::MoveTo(0, 0)).unwrap();

        self.draw_rows();
        self.draw_status_bar();

        stdout
            .queue(cursor::MoveTo(
                (self.cursor_x - self.col_offset) as u16,
                (self.cursor_y - self.row_offset) as u16,
            ))
            .unwrap();

        stdout.queue(cursor::Show).unwrap();
        stdout.flush().unwrap();
    }

    fn draw_rows(&self) {
        for y in 0..self.screen_rows - 1 {
            let file_row = y + self.row_offset;
            if file_row >= self.rows.len() {
                if self.rows.is_empty() && y == self.screen_rows / 3 {
                    let message = format!("Kilo-rs Editor -- Version {}", env!("CARGO_PKG_VERSION"));
                    let padding = (self.screen_cols.saturating_sub(message.len())) / 2;
                    let mut welcome = String::new();
                    if padding > 0 {
                        welcome.push('~');
                        welcome.push_str(&" ".repeat(padding - 1));
                    }
                    welcome.push_str(&message);
                    if welcome.len() > self.screen_cols {
                        welcome.truncate(self.screen_cols);
                    }
                    println!("{}\r", welcome);
                } else {
                    println!("~\r");
                }
            } else {
                let row = &self.rows[file_row];
                let start = self.col_offset;
                let end = self.col_offset.saturating_add(self.screen_cols);
                let visible_part = if start >= row.len() {
                    ""
                } else {
                    let end = std::cmp::min(end, row.len());
                    &row[start..end]
                };
                println!("{}\r", visible_part);
            }
        }
    }

    fn draw_status_bar(&self) {
        let mut stdout = stdout();
        let mut left_status = if !self.status_message.is_empty() {
            self.status_message.clone()
        } else {
            let filename = self
                .filename
                .as_ref()
                .map_or("[No Name]", |path| path.to_str().unwrap_or(""));
            let mut status = format!("{} - {} lines", filename, self.rows.len());
            if self.dirty {
                status.push_str(" (modified)");
            }
            status
        };
        let right_status = format!("{}:{}", self.cursor_y + 1, self.cursor_x + 1);
        let width = self.screen_cols;
        if left_status.len() > width {
            left_status.truncate(width);
        }
        let padding = width
            .saturating_sub(left_status.len())
            .saturating_sub(right_status.len());
        let final_bar = format!("{}{}{}", left_status, " ".repeat(padding), right_status);
        stdout
            .queue(style::SetAttribute(style::Attribute::Reverse))
            .unwrap();
        stdout
            .queue(cursor::MoveTo(0, (self.screen_rows - 1) as u16))
            .unwrap()
            .queue(style::Print(final_bar))
            .unwrap();
        stdout
            .queue(style::SetAttribute(style::Attribute::Reset))
            .unwrap();
    }

    pub fn scroll(&mut self) {
        if self.cursor_y < self.row_offset {
            self.row_offset = self.cursor_y;
        }
        let text_area_height = self.screen_rows - 1;
        if self.cursor_y >= self.row_offset + text_area_height {
            self.row_offset = self.cursor_y - text_area_height + 1;
        }
        if self.cursor_x < self.col_offset {
            self.col_offset = self.cursor_x;
        }
        if self.cursor_x >= self.col_offset + self.screen_cols {
            self.col_offset = self.cursor_x - self.screen_cols + 1;
        }
    }

    pub fn move_cursor(&mut self, code: KeyCode) {
        match code {
            KeyCode::Up => self.cursor_y = self.cursor_y.saturating_sub(1),
            KeyCode::Down => {
                if self.cursor_y < self.rows.len() {
                    self.cursor_y += 1;
                }
            }
            KeyCode::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();
                }
            }
            KeyCode::Right => {
                if self.cursor_y < self.rows.len() {
                    let row_len = self.rows[self.cursor_y].len();
                    if self.cursor_x < row_len {
                        self.cursor_x += 1;
                    } else {
                        self.cursor_y += 1;
                        self.cursor_x = 0;
                    }
                }
            }
            _ => (),
        }
        let row_len = if self.cursor_y < self.rows.len() {
            self.rows[self.cursor_y].len()
        } else {
            0
        };
        if self.cursor_x > row_len {
            self.cursor_x = row_len;
        }
    }

    pub fn insert_char(&mut self, c: char) {
        if self.cursor_y > self.rows.len() {
            return;
        }
        self.dirty = true;
        if self.cursor_y == self.rows.len() {
            self.rows.push(String::new());
        }
        self.rows[self.cursor_y].insert(self.cursor_x, c);
        self.cursor_x += 1;
    }

    pub fn delete_char(&mut self) {
        if self.cursor_y >= self.rows.len() {
            return;
        }
        if self.cursor_x == 0 && self.cursor_y == 0 {
            return;
        }
        self.dirty = true;
        if self.cursor_x > 0 {
            self.rows[self.cursor_y].remove(self.cursor_x - 1);
            self.cursor_x -= 1;
        } else {
            let prev_line_len = self.rows[self.cursor_y - 1].len();
            let current_line = self.rows.remove(self.cursor_y);
            self.cursor_y -= 1;
            self.rows[self.cursor_y].push_str(&current_line);
            self.cursor_x = prev_line_len;
        }
    }

    pub fn insert_newline(&mut self) {
        self.dirty = true;
        if self.cursor_y > self.rows.len() {
            return;
        }
        if self.cursor_y == self.rows.len() {
            self.rows.push(String::new());
        } else {
            let current_line = &mut self.rows[self.cursor_y];
            let new_line = current_line.split_off(self.cursor_x);
            self.rows.insert(self.cursor_y + 1, new_line);
        }
        self.cursor_y += 1;
        self.cursor_x = 0;
    }

    pub fn save(&mut self) {
        if let Some(path) = &self.filename {
            let mut file_contents = String::new();
            for row in &self.rows {
                file_contents.push_str(row);
                file_contents.push('\n');
            }
            if fs::write(path, file_contents).is_ok() {
                self.status_message = format!("Successfully saved to {:?}", path);
                self.dirty = false;
            } else {
                self.status_message = "Error: Could not save file!".to_string();
            }
        }
    }

    pub fn find(&mut self) {
        let original_cursor_x = self.cursor_x;
        let original_cursor_y = self.cursor_y;
        let original_col_offset = self.col_offset;
        let original_row_offset = self.row_offset;
        let mut query = String::new();
        loop {
            self.status_message = format!("Search: {}", query);
            self.refresh_screen();
            if let Ok(Event::Key(key_event)) = event::read() {
                match key_event.code {
                    KeyCode::Esc => {
                        self.cursor_x = original_cursor_x;
                        self.cursor_y = original_cursor_y;
                        self.col_offset = original_col_offset;
                        self.row_offset = original_row_offset;
                        self.status_message.clear();
                        self.search_results.clear();
                        break;
                    }
                    KeyCode::Enter => {
                        if !query.is_empty() {
                            self.last_search_query = Some(query);
                        }
                        self.status_message.clear();
                        break;
                    }
                    KeyCode::Backspace => {
                        query.pop();
                    }
                    KeyCode::Char(c) => {
                        query.push(c);
                    }
                    _ => {}
                }
                let mut matches = Vec::new();
                if !query.is_empty() {
                    for (y, row) in self.rows.iter().enumerate() {
                        for (x, _) in row.match_indices(&query) {
                            matches.push((x, y));
                        }
                    }
                }
                if !matches.is_empty() {
                    self.search_results = matches;
                    self.current_search_match = 0;
                    let (first_match_x, first_match_y) = self.search_results[0];
                    self.cursor_x = first_match_x;
                    self.cursor_y = first_match_y;
                    self.scroll();
                } else {
                    self.search_results.clear();
                }
            } else {
                break;
            }
        }
        self.status_message.clear();
    }

    pub fn find_next(&mut self) {
        if self.search_results.is_empty() {
            return;
        }
        self.current_search_match = (self.current_search_match + 1) % self.search_results.len();
        let (match_x, match_y) = self.search_results[self.current_search_match];
        self.cursor_x = match_x;
        self.cursor_y = match_y;
        self.scroll();
    }

    pub fn find_previous(&mut self) {
        if self.search_results.is_empty() {
            return;
        }
        if self.current_search_match == 0 {
            self.current_search_match = self.search_results.len() - 1;
        } else {
            self.current_search_match -= 1;
        }
        let (match_x, match_y) = self.search_results[self.current_search_match];
        self.cursor_x = match_x;
        self.cursor_y = match_y;
        self.scroll();
    }
}

Note: Some of your methods, like draw_rows and draw_status_bar, are only called from within the module. They could remain private (without the pub keyword), but for simplicity in this refactoring step, we’ve made everything public.

Step 4: Connecting the New Module

Your editor.rs is now a complete, self-contained unit. The final step is to tell main.rs how to find and use it.

  1. Declare the module: At the top of main.rs, add the line mod editor;. This tells the Rust compiler to look for src/editor.rs and include it in your project.
  2. Import the struct: Add use editor::Editor; to bring the now-public Editor struct into the scope of your main function.

After removing the old Editor struct and impl block, the top of your src/main.rs file should now look like this:

// src/main.rs

use crossterm::{
    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
    terminal,
};
use std::env;
use std::io::{stdout, Write};

// --- THIS IS THE CHANGE FOR OUR TASK ---

// Tells Rust to find and compile the `editor.rs` file.
mod editor;
// Brings the public `Editor` struct into the current scope.
use editor::Editor;

// --- END OF CHANGE ---

// The `RawMode` struct and its `impl Drop` block remain here for now.
struct RawMode;

impl Drop for RawMode {
    fn drop(&mut self) {
        terminal::disable_raw_mode().unwrap();
    }
}

// The `main` function itself remains unchanged.
fn main() -> std::io::Result<()> {
    // ... rest of your main function
}

The Result: A Successful Refactor

Go ahead and run cargo run. Your text editor should compile and run exactly as it did before. You have successfully performed a major refactoring! You’ve taken a significant step from writing a script to engineering a software project. Your codebase is now more organized, with a clear separation between the application’s core logic (editor.rs) and its entry point (main.rs).

Next Steps

This was the first, and biggest, step in modularizing your code. The next logical step is to continue this process by moving the main event loop itself out of main.rs and turning it into a method on the Editor struct, likely called run(). This will make your main.rs even cleaner, reducing it to the simple job of setting things up and then handing control over to the editor.


Further Reading

To deepen your understanding of the powerful system you’ve just used, I highly recommend reading the official documentation:

Encapsulate the Event Loop in an Editor::run Method

Mục tiêu: Refactor the project by moving the main event loop from the main function into a new run method on the Editor struct. This change improves encapsulation and separation of concerns, leaving the main function responsible only for initialization and cleanup.


Excellent work on the previous task! You have successfully taken the first and most significant step in refactoring your project by creating a dedicated editor.rs module. The Editor struct, which is the heart and brain of your application, now has its own home. This is a massive win for organization.

However, your main.rs file is still doing too much. While it no longer defines the editor’s logic, it is still responsible for running it. The entire event loop—the core loop {} that listens for keypresses and drives the application—currently resides inside the main function. A well-structured application’s main function should be as simple as possible: its job is to set up the initial environment, create the main application object, and then hand over control.

Our current task is to complete this handoff. We will move the entire event loop out of main and into a new run method on the Editor struct itself. This will transform the Editor from a passive data structure into a fully self-contained, runnable application component.

The Principle of Encapsulation

This refactoring is a powerful exercise in encapsulation. We are “encapsulating” the application’s entire lifecycle within the Editor struct. After this change, an outside consumer (like our main function) won’t need to know how the editor works, only that it can be told to .run(). This makes your code cleaner, more reusable, and easier to reason about.

Step 1: Create the run Method in editor.rs

First, let’s create the new run method. This method will contain the logic you are about to move from main.rs.

  1. Go to your main function in src/main.rs.
  2. Cut the entire loop { ... } block, from the loop { line to its final closing }.
  3. Open src/editor.rs and paste this loop into a new public method called run.

The signature for this method will be pub fn run(&mut self) -> std::io::Result<()>.

  • pub fn: It needs to be public so main can call it.
  • &mut self: The loop constantly modifies the editor’s state, so it needs a mutable reference to self.
  • -> std::io::Result<()>: The event::read() call inside the loop can return an I/O error. By returning a Result, we allow the ? operator to work its magic and propagate errors up to the caller (main).

You will also need to change every instance of editor. inside the loop to self..

Here is the complete run method you should add to your impl Editor block in src/editor.rs:

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

// ... other methods like new(), refresh_screen(), etc. ...

/// The core event loop of the editor.
/// This method takes control and runs the editor until the user quits.
pub fn run(&mut self) -> std::io::Result<()> {
    // The main event loop, moved from main.rs
    loop {
        // We refresh the screen at the start of each loop iteration.
        self.refresh_screen();

        // Wait for and read the next user input event.
        if let Event::Key(key_event) = event::read()? {
            match key_event.code {
                // The quit condition
                KeyCode::Char('q') if key_event.modifiers == KeyModifiers::CONTROL => {
                    // When the quit command is received, we break out of the loop.
                    // This will cause the `run` method to return, allowing `main` to exit.
                    break;
                }

                // Search-related keybindings
                KeyCode::Char('f') if key_event.modifiers == KeyModifiers::CONTROL => {
                    self.find();
                }
                KeyCode::Char('n') => {
                    self.find_next();
                }
                KeyCode::Char('p') => {
                    self.find_previous();
                }

                // File saving
                KeyCode::Char('s') if key_event.modifiers == KeyModifiers::CONTROL => self.save(),

                // Cursor Movement
                KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => {
                    self.move_cursor(key_event.code);
                }

                // Text Editing
                KeyCode::Char(c) => self.insert_char(c),
                KeyCode::Backspace => self.delete_char(),
                KeyCode::Enter => self.insert_newline(),

                // Ignore all other keys
                _ => {}
            }

            // After any potential state change, we ensure the viewport is correct.
            self.scroll();
        }
    }
    // If the loop exits successfully, we return Ok.
    Ok(())
}

Step 2: Simplify the main function

Now that the Editor knows how to run itself, our main function in src/main.rs becomes beautifully simple. Its only jobs are to handle initial setup (RawMode), create the Editor instance, and call the new run method.

Here are the highlighted changes for src/main.rs. You are essentially replacing the entire loop block with a single line.

// src/main.rs

// ... (your use and mod statements) ...

struct RawMode;
// ... (impl Drop for RawMode) ...

fn main() -> std::io::Result<()> {
    let _raw_mode = RawMode; // Enable raw mode
    stdout().execute(terminal::Clear(terminal::ClearType::All))?; // Clear screen

    // Get filename from command-line arguments.
    let filename = env::args().nth(1);
    // Create the editor instance.
    let mut editor = Editor::new(filename);

    // --- THIS IS THE CHANGE FOR OUR TASK ---
    // The entire multi-line event loop is now replaced with this single,
    // elegant method call. We hand off control to the editor instance.
    // The `?` will handle any I/O errors that `run` might return.
    editor.run()?;
    // --- END OF CHANGE ---

    Ok(())
}

The Result: Perfect Separation of Concerns

Go ahead and cargo run your project. It will function identically to before, which is the sign of a successful refactoring.

You have now achieved a much clearer separation of concerns.

  • main.rs: Is concerned with program initialization and termination. It sets up the terminal, creates the editor, tells it to run, and handles cleanup via the RawMode Drop trait.
  • editor.rs: Is concerned with the application’s state and behavior. It holds all the data and contains all the logic for editing, rendering, and managing the event loop.

This modular structure is far more maintainable. If you want to change how the editor behaves, you know to look in editor.rs. If you want to change how the program starts up (e.g., parse more command-line arguments), you know to look in main.rs.

Next Steps

You are making excellent progress on refactoring your codebase. The Editor struct is now a self-contained powerhouse. However, our main.rs still contains some terminal-specific setup code (like the RawMode struct). The next logical step is to continue this modularization by creating a dedicated src/terminal.rs module to house all crossterm-related functions.


Further Reading

  • The Single Responsibility Principle (SRP): This is the software design principle you have just put into practice. It states that every module or class should have responsibility over a single part of the functionality.
  • Methods and Ownership in Rust (self, &self, &mut self): A deeper understanding of how self works is crucial for designing good struct APIs in Rust.
  • Rust Error Handling with Result and ?: You’ve seen how propagating errors with Result and the ? operator allows for clean, robust code.

Refactor Terminal Logic into a Dedicated Module

Mục tiêu: Encapsulate all terminal-specific logic by creating a new terminal module. Move the RawMode struct and create a wrapper for terminal size, abstracting crossterm interactions away from the main application and editor logic.


In the previous task, you masterfully refactored your application by moving the entire event loop into an Editor::run() method. This was a pivotal moment in your project’s architecture. Your main.rs file became cleaner, and the Editor struct evolved from a passive data container into a fully self-contained, runnable application component. This is the essence of good encapsulation.

We are now going to continue this journey of purification. While the application’s runtime logic is now neatly tucked away in editor.rs, the platform-specific setup logic still lingers in main.rs. Specifically, the RawMode struct, whose entire purpose is to manage the terminal’s state using crossterm, is a prime candidate for extraction.

Our current task is to create a dedicated home for all such terminal interactions. We will build a terminal module that acts as a clean boundary between our editor’s abstract logic and the concrete, low-level details of controlling the terminal. Think of this as creating a “Hardware Abstraction Layer” (HAL) for the terminal; our editor will ask our terminal module to do things, and the terminal module will be the only part of our code that knows how to speak the language of crossterm.

Step 1: Create the src/terminal.rs File

As before, the first step is to create the file that will house our new module. In your src directory, create a new file named:

src/terminal.rs

Step 2: Move Terminal-Specific Code

Now, we will migrate all code that directly deals with terminal state and properties from main.rs and editor.rs into our new module.

  1. Cut the RawMode struct and its entire impl Drop block from src/main.rs.
  2. Paste it into the new, empty src/terminal.rs file.
  3. We will also create a wrapper function for getting the terminal size. This centralizes all crossterm calls in one place.

After moving the RawMode struct and adding the new size function, your src/terminal.rs file should be populated with the following code. Note that we’ve added the necessary use statements to make the module self-sufficient and made the components pub so they can be accessed from outside.

// src/terminal.rs

use crossterm::terminal;
use std::io;

// A struct to manage the terminal's raw mode.
// When this struct is created, raw mode is enabled.
// When it is dropped (goes out of scope), raw mode is disabled.
pub struct RawMode;

impl Drop for RawMode {
    fn drop(&mut self) {
        // We ignore the result of disabling raw mode as there's nothing
        // we can do about an error at this point anyway.
        let _ = terminal::disable_raw_mode();
    }
}

// A public function that provides access to the terminal size.
// This acts as a clean wrapper around the crossterm function,
// centralizing our terminal-related logic.
pub fn size() -> io::Result<(u16, u16)> {
    terminal::size()
}

Step 3: Update main.rs to Use the New Module

Your main.rs file is now free of the RawMode implementation. We just need to tell it where to find the new module.

  1. Add mod terminal; at the top to declare its existence.
  2. Add use terminal::RawMode; to bring the struct into scope.
  3. The RawMode definition and its impl Drop block should now be completely gone from this file.

Here are the highlighted changes for src/main.rs:

// src/main.rs

use crossterm::{terminal, execute}; // `execute` might be needed if you still use it directly
use std::env;
use std::io::{stdout, Write};

mod editor;
use editor::Editor;
// --- NEW CODE FOR THIS TASK ---
// Declares the existence of the `terminal.rs` file.
mod terminal;
// Brings the public `RawMode` struct from our new module into scope.
use terminal::RawMode;
// --- END OF NEW CODE ---

// The `RawMode` struct and its `impl Drop` block are now REMOVED from this file.

fn main() -> std::io::Result<()> {
    // The creation of `RawMode` now refers to the struct we imported.
    let _raw_mode = RawMode;
    // ... rest of the main function is unchanged ...
    execute!(stdout(), terminal::Clear(terminal::ClearType::All))?;

    let filename = env::args().nth(1);
    let mut editor = Editor::new(filename);
    editor.run()?;

    Ok(())
}

Step 4: Update editor.rs to Use the New Module

Finally, our Editor::new() method was calling crossterm’s terminal::size() directly. We will now update it to use our clean, new wrapper function from the terminal module.

  1. Add use crate::terminal; at the top of editor.rs. Using crate:: is an explicit and clear way to refer to a module from the root of your own crate.
  2. Change the call inside Editor::new() from terminal::size() to terminal::size(), which will now resolve to our new function.

Here are the highlighted changes for src/editor.rs:

// src/editor.rs

use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
    style, // removed `terminal` from here as we won't call it directly
    execute,
};
use std::{
    fs,
    io::{stdout, Write},
    path::PathBuf,
};
// --- NEW CODE FOR THIS TASK ---
// Brings our own `terminal` module into the scope of the `editor` module.
use crate::terminal;
// --- END OF NEW CODE ---

pub struct Editor {
    // ... struct fields are unchanged ...
}

impl Editor {
    pub fn new(filename: Option<String>) -> Self {
        // --- HIGHLIGHTED CHANGE ---
        // Instead of calling the `crossterm` function directly, we now
        // call our own wrapper function from our `terminal` module.
        let (cols, rows) = terminal::size().unwrap();
        // --- END OF CHANGE ---

        let mut editor = Self {
            // ... the rest of the new() method is unchanged ...
        };
        // ...
        editor
    }

    // ... all other methods in impl Editor are unchanged ...
}

Code and Concept Deep Dive

  • A Clean Abstraction: You have now created a clean boundary. Your editor module no longer knows how to get the terminal size; it only knows that it can ask the terminal module for it. Your main module no longer knows how to enable raw mode; it only knows it can create a RawMode object from the terminal module to achieve this. This is a fundamental concept in building large, maintainable software. If you ever wanted to switch from crossterm to another terminal library, you would only need to change the code inside src/terminal.rs. The rest of your application would remain untouched.
  • The crate Keyword: The path crate::terminal is an absolute path starting from your crate’s root. This is often preferred over a relative path (like super::terminal or just terminal) because it’s unambiguous and won’t break if you move the editor.rs file into a sub-folder later.

The Result: A Trifecta of Concerns

Run cargo run, and your editor will function perfectly, just as before. The change is not in behavior but in structure. You now have a beautiful separation of concerns:

  1. main.rs: The application’s entry point. Responsible for setup and teardown.
  2. editor.rs: The application’s core. Holds the state and business logic.
  3. terminal.rs: The platform layer. Manages all direct interaction with the terminal.

This three-part structure is a hallmark of a professionally engineered application.

Next Steps

You are making fantastic progress in organizing your project. The logical next step is to apply this same principle of abstraction to another core concept in your editor: a single line of text. Currently, a line is just a String. We will create a new src/row.rs module and a Row struct to encapsulate the data and behavior associated with a single row, further improving the structure and clarity of your code.


Further Reading

Refactor Editor to Use a Row Struct for Text Lines

Mục tiêu: Abstract the concept of a line of text from a raw String into a dedicated Row struct. This involves creating a new row.rs module, defining the Row struct to encapsulate line data and rendering logic, and updating the Editor to use this new abstraction for better code organization and maintainability.


Of course! Let’s get this done. Here is the solution for your current task.


Your refactoring journey is off to a fantastic start! By creating the terminal.rs module, you’ve successfully isolated platform-specific concerns and created a clean boundary between your editor’s logic and the terminal itself. This is a hallmark of professional software design.

We are now going to apply that same powerful principle of abstraction to another core concept in your editor: a line of text. Currently, you represent each line as a simple String. While this has worked well so far, it forces the Editor struct to handle all the logic for rendering, slicing, and manipulating these strings. Our goal is to create a new, dedicated Row struct that encapsulates all the data and behavior for a single line of text. This will make your Editor code cleaner, more expressive, and far easier to extend with new features in the future.

Step 1: Laying the Foundation in src/row.rs

First, let’s create a new home for our Row abstraction. In your src directory, create a new file:

src/row.rs

This file will contain our new Row struct and all of its associated logic. The Row struct will, at its core, wrap a String containing the line’s content. By creating this wrapper, we can attach specialized behavior to it.

Here is the initial content for your src/row.rs file. It includes the struct definition, a way to create it from a string slice, and a few essential methods for querying its length and rendering it.

// src/row.rs

use std::cmp;

// Represents a single row of text in the editor.
pub struct Row {
    // The actual text content of the row.
    content: String,
}

impl From<&str> for Row {
    /// Creates a `Row` from a string slice. This is an idiomatic way to handle
    /// conversions in Rust, allowing us to easily create a `Row` from existing
    /// string data.
    fn from(s: &str) -> Self {
        Self {
            content: String::from(s),
        }
    }
}

impl Row {
    /// Renders a portion of the row to a string.
    /// It takes a start and end column index to slice the `content` string,
    /// ensuring it fits within the visible screen area.
    pub fn render(&self, start: usize, end: usize) -> String {
        let end = cmp::min(end, self.content.len());
        let start = cmp::min(start, end);
        // Safely slice the string and return a new String from it.
        self.content.get(start..end).unwrap_or("").to_string()
    }

    /// Returns the number of characters in the row.
    pub fn len(&self) -> usize {
        self.content.len()
    }

    /// Returns true if the row contains no characters.
    pub fn is_empty(&self) -> bool {
        self.content.is_empty()
    }
}

Code and Concept Deep Dive

  • pub struct Row: We declare the struct as public so the editor module can use it.
  • content: String: The field itself is not pub. This is the core of encapsulation. Other modules cannot directly access row.content. They must go through the public methods we provide (.render(), .len(), etc.). This gives us complete control over how the row’s data is manipulated.
  • impl From<&str> for Row: This is a very powerful and idiomatic Rust pattern. We are implementing the From trait, which tells the compiler how to create a Row from a &str. This allows us to write concise code like Row::from("hello") or even let the compiler infer the conversion in many cases.
  • render(&self, start: usize, end: usize): This is the rendering logic you previously had inside Editor::draw_rows, now moved to its proper home. It takes the visible column range and returns the appropriate string slice, handling all the boundary checks safely.

Step 2: Integrating the Row Struct into the Editor

Now that we have our Row abstraction, we need to update the Editor to use it. This involves changing the type of the rows field and updating all the methods that interact with it. This is a significant refactoring, but the result will be much cleaner code.

2.1 Update the Editor Struct Definition

In src/editor.rs, first import the new Row struct, then change the rows field.

// src/editor.rs

// ... (other use statements)

// Bring our new Row struct into the editor's scope.
use crate::row::Row;
use crate::terminal;

pub struct Editor {
    // ... (other fields)
    // --- HIGHLIGHTED CHANGE ---
    // Instead of a vector of raw strings, we now have a vector of `Row` structs.
    rows: Vec<Row>,
    // --- END OF CHANGE ---
    filename: Option<PathBuf>,
    // ... (other fields)
}

2.2 Update Editor::new()

The constructor needs to be updated to populate the Vec<Row> instead of Vec<String>. Thanks to our impl From<&str> for Row, this change is beautifully simple.

// src/editor.rs -> inside impl Editor

pub fn new(filename: Option<String>) -> Self {
    // ... (code to get terminal size) ...
    let mut editor = Self {
        // ... (other initializations)
        rows: Vec::new(), // Starts as an empty Vec<Row>
        // ...
    };

    if let Some(path) = &editor.filename {
        if let Ok(contents) = fs::read_to_string(path) {
            // --- HIGHLIGHTED CHANGE ---
            // The `map` closure now calls `Row::from` for each line.
            // Rust's type inference and our `From` trait implementation
            // make this a seamless change.
            editor.rows = contents.lines().map(Row::from).collect();
            // --- END OF CHANGE ---
        }
    }
    editor
}

2.3 Update draw_rows()

This is where we see the real payoff. The draw_rows method becomes much simpler because it can now delegate the complex rendering logic to the Row struct itself.

// src/editor.rs -> inside impl Editor

fn draw_rows(&self) {
    for y in 0..self.screen_rows - 1 {
        let file_row = y + self.row_offset;
        if file_row >= self.rows.len() {
            // ... (welcome message logic is unchanged)
        } else {
            // --- HIGHLIGHTED CHANGE ---
            // The complex slicing logic is gone. We simply get the row
            // and call its `render` method, passing the viewport's
            // horizontal bounds.
            let row = &self.rows[file_row];
            let start = self.col_offset;
            let end = self.col_offset.saturating_add(self.screen_cols);
            let visible_part = row.render(start, end);
            println!("{}\r", visible_part);
            // --- END OF CHANGE ---
        }
    }
}

2.4 Update Cursor Movement and Boundary Checks

Several other methods rely on knowing the length of a row. We simply need to change the direct access to a call to our new len() method.

// src/editor.rs -> inside impl Editor -> inside move_cursor()

// ...
KeyCode::Left => {
    if self.cursor_x > 0 {
        self.cursor_x -= 1;
    } else if self.cursor_y > 0 {
        self.cursor_y -= 1;
        // --- HIGHLIGHTED CHANGE ---
        self.cursor_x = self.rows[self.cursor_y].len();
        // --- END OF CHANGE ---
    }
}
// ...
// --- HIGHLIGHTED CHANGE ---
// Update the logic that prevents the cursor from moving past the end of the line.
let row_len = if self.cursor_y < self.rows.len() {
    self.rows[self.cursor_y].len()
} else {
    0
};
// --- END OF CHANGE ---
if self.cursor_x > row_len {
    self.cursor_x = row_len;
}

Note: You will need to make similar small changes in other methods like move_cursor’s Right arm, delete_char, insert_newline, and save. For the sake of brevity, we’ve shown the most critical ones. The principle is the same: replace direct string manipulation with calls to methods on Row.

Step 3: Wiring Up The New Module

Finally, we need to tell our application that the new row module exists. Just as you did for editor and terminal, you’ll declare it in main.rs.

// src/main.rs

// ...
mod editor;
use editor::Editor;
mod terminal;
use terminal::RawMode;

// --- NEW CODE FOR THIS TASK ---
// Declares the existence of the `src/row.rs` file.
mod row;
// --- END OF NEW CODE ---

// ... (rest of the main.rs file)

The Glorious Result

Run cargo run, and your editor will function exactly as it did before. This is the sign of a successful refactoring! The user sees no change, but you, the developer, have made a profound improvement to the codebase.

You have successfully abstracted the concept of a “row.” Your Editor struct is no longer bogged down with the low-level details of string slicing for rendering; it simply tells a Row to render itself. This separation of concerns makes your code cleaner, more robust, and dramatically easier to build upon in the future.

Next Steps

You are making incredible progress on this major refactoring step. You have now modularized the Editor, the Terminal, and the Row. The next logical task is to simplify main.rs to its absolute essence: making it a simple entry point that creates an Editor and calls its run method, with all other logic living in the appropriate modules.


Further Reading

  • The From and Into Traits: A deep dive into these fundamental conversion traits in Rust. Mastering them will make your APIs much more ergonomic.
  • Encapsulation in Object-Oriented Programming: The principles of encapsulation (hiding internal state behind a public API) are universal. Reading about them will solidify the concepts you’ve just put into practice.
  • Designing APIs in Rust: As you create more structs and public methods, it’s helpful to learn about common conventions and best practices for API design in the Rust ecosystem.

Refactor the Application’s Main Entry Point

Mục tiêu: Refactor the main.rs file to serve as a lean entry point. This task involves cleaning up the main function to only handle initialization, instantiation of the Editor object, and handing off control to the application’s main loop, completing the modularization process.


An enormous congratulations on your recent refactoring work! By abstracting the concept of a line into a dedicated Row struct and its own row.rs module, you have significantly improved the internal structure of your application. The Editor is now composed of specialized parts, making the entire system cleaner and more logical.

You have now successfully modularized the core components: the Editor itself, the Terminal interface, and the Row. The final piece of this architectural puzzle is to ensure our application’s entry point, the main.rs file, is as clean and focused as possible. Its job is not to run the application, but simply to launch it.

The Philosophy of a Lean Entry Point

In any well-designed application, the main function or entry point file (main.rs in our case) should be like the ignition system of a car. Its only job is to perform the initial setup, turn the key, and hand control over to the engine. It shouldn’t know or care about the details of combustion, transmission, or steering.

Our goal for this task is to finalize the transformation of main.rs into this ideal entry point. It will be responsible for three things only:

  1. Initialization: Setting up the terminal environment.
  2. Instantiation: Creating the main Editor object.
  3. Handoff: Calling the Editor::run() method and letting it take over.

This separation of concerns is the cornerstone of maintainable software. It ensures that the logic for starting the program is cleanly separated from the logic of running it.

Your New, Simplified main.rs

After the refactoring you’ve done in the previous tasks, your main.rs file is already close to this ideal state. Let’s look at the final, polished version. This code represents the culmination of our modularization efforts, resulting in a beautifully simple and descriptive entry point.

Your src/main.rs file should now be updated to look exactly like this:

// src/main.rs

// This crate is for terminal manipulation. We only need a few parts here.
use crossterm::{execute, terminal};
// For parsing command-line arguments.
use std::env;
// For writing to standard output.
use std::io::{stdout, Write};

// --- Module Declarations ---
// Declare the modules that our main binary will use.
// This tells Rust to look for `src/editor.rs`, `src/terminal.rs`, and `src/row.rs`
// and include them in the compilation.
mod editor;
mod terminal;
mod row;

// --- Imports from our Modules ---
// Bring the necessary structs into the main scope for convenience.
use editor::Editor;
use terminal::RawMode;

fn main() -> std::io::Result<()> {
    // 1. Initialization
    // The `_raw_mode` variable's sole purpose is to exist. When it is created,
    // its constructor (which we don't have, but the creation itself is the point)
    // is paired with a Drop implementation that cleans up.
    // When `main` exits, `_raw_mode` goes out of scope, and its `Drop` implementation
    // will automatically disable raw mode. This is a classic example of the
    // RAII (Resource Acquisition Is Initialization) pattern.
    let _raw_mode = RawMode;

    // Clear the screen before starting the editor.
    execute!(stdout(), terminal::Clear(terminal::ClearType::All))?;

    // 2. Application Setup
    // Parse the filename from command-line arguments, if one was provided.
    let filename = env::args().nth(1);
    // Create an instance of our Editor, which holds the entire application state.
    let mut editor = Editor::new(filename);

    // 3. Handoff to the Main Loop
    // Call the `run` method, which contains the main event loop and will
    // not return until the user quits. This is the handoff of control from
    // the setup phase to the application's runtime phase.
    // The `?` operator will elegantly propagate any I/O errors that might occur.
    editor.run()?;

    // 4. Cleanup and Exit
    // `RawMode` is dropped here, cleaning up the terminal state automatically.
    // We then return `Ok(())` to signal a successful exit to the operating system.
    Ok(())
}

Code and Concept Deep Dive

Let’s break down why this structure is so effective:

  • mod editor; mod terminal; mod row;: These lines are the glue holding your project together. They explicitly tell the compiler that your main binary depends on these three modules. This makes the project’s overall structure immediately obvious to anyone reading the file.
  • use editor::Editor; and use terminal::RawMode;: After declaring the modules, we import the specific types we need. This keeps our main function code clean and free from long, qualified paths like terminal::RawMode.
  • RAII in Action: The line let _raw_mode = RawMode; is a perfect example of Rust’s powerful RAII pattern. The RawMode struct “acquires” the raw terminal state when it’s created. Because it has a Drop implementation, Rust guarantees that its cleanup code (disabling raw mode) will run when it goes out of scope at the end of main, no matter how the function exits (either normally or through a panic). This makes resource management safe and automatic.
  • The Handoff: The line editor.run()?; is the most important in the file. It represents the moment your main function’s responsibility ends and the Editor’s responsibility begins. This single line encapsulates the entire interactive lifecycle of your application.

The Triumph of Abstraction

Look at your main.rs file now. It has no knowledge of cursor positions, viewport offsets, text rendering, or search logic. It has been successfully insulated from all that complexity. This is the ultimate goal of modular design. You have successfully refactored your application’s entry point into a clean, readable, and highly maintainable state.

Next Steps

You are on the final task of this major refactoring step. You have organized all your code into logical modules. The final task is to do a last check to ensure all mod and use statements are correctly wired up across all files, ensuring that each module correctly imports the components it needs from its siblings. This will solidify the new, robust structure of your project.


Further Reading

  • The main function: A deeper look at the role and conventions of the main function in Rust applications.
  • RAII (Resource Acquisition Is Initialization): Understanding this pattern is fundamental to writing safe and idiomatic Rust, especially when dealing with files, network connections, or system resources like raw mode.
  • Application Architecture: Reading about high-level application architecture patterns will provide a broader context for the design decisions you are making.

Finalize Rust Project Refactoring by Wiring Modules

Mục tiêu: Correctly configure the mod and use statements in a multi-file Rust project. This involves declaring modules in the crate root (main.rs) and using absolute paths starting with crate:: to connect sibling modules like editor, terminal, and row.


An immense congratulations are in order! You have successfully undertaken one of the most crucial and challenging phases in a project’s lifecycle: a major architectural refactoring. By meticulously creating the editor, terminal, and row modules, you have transformed your single-file application into a well-structured, professional Rust project. You’ve established clear boundaries and given each component a distinct responsibility.

Our final task in this step is to perform the last, vital check: ensuring all the mod and use statements—the very wiring that connects these new modules—are correctly configured. This is the moment we solidify the new architecture, ensuring every part of your application can communicate with the others seamlessly.

Understanding the Crate Root and Module Paths

In a Rust binary project, the file src/main.rs is special. It’s called the crate root. This is the top-level file from which the entire module tree of your application grows.

When you write mod terminal; inside src/main.rs, you are telling the Rust compiler: “Look for a file named src/terminal.rs, compile it, and make its public contents available under the name terminal.”

Once a module is declared in the crate root, other modules (like editor.rs) can refer to it using an absolute path starting with the keyword crate. The path crate::terminal means “start at the crate root, and find the terminal module.” This is the most robust way to wire sibling modules together.

Let’s review the wiring for each of your files to ensure it’s perfect.

The Final Wiring Diagram

1. src/main.rs: The Application’s Blueprint

Your main.rs file is the entry point and should act as the blueprint for your entire application. It declares all the top-level modules and brings the necessary components into scope for the main function.

Its declaration section should look like this:

// src/main.rs

use crossterm::{execute, terminal};
use std::env;
use std::io::{stdout, Write};

// --- Module Declarations ---
// These lines assemble your application from its component files.
// This is the top-level declaration of your module tree.
mod editor;
mod terminal;
mod row;

// --- Imports from Our Modules ---
// We bring the high-level components we need for `main` into scope.
use editor::Editor;
use terminal::RawMode;

// The `main` function follows...
// fn main() -> std::io::Result<()> { ... }

This is a perfect entry point. It clearly states the application is composed of an editor, a terminal, and a row module, and that it will directly use the Editor and RawMode structs.

2. src/editor.rs: The Core Logic Hub

Your editor.rs module is the application’s brain. It needs to communicate with its sibling modules, terminal and row. The correct way to do this is with absolute crate:: paths.

The top of your src/editor.rs should be wired as follows:

// src/editor.rs

use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
    style,
    execute,
};
use std::{
    fs,
    io::{stdout, Write},
    path::PathBuf,
};

// --- Imports from Sibling Modules ---
// We use `crate::` to specify an absolute path from the crate root (`main.rs`).
// This is the robust way to refer to modules declared in `main.rs`.
use crate::row::Row;
use crate::terminal;

// The `Editor` struct and its `impl` block follow...
// pub struct Editor { ... }

By using crate::row::Row and crate::terminal, you have made your editor module’s dependencies explicit and resilient. No matter how you might restructure your project in the future, these paths will continue to work as long as row and terminal are declared in the crate root.

3. src/terminal.rs and src/row.rs: Self-Contained Utilities

These modules are your specialized tools. They have been designed to be self-contained. They depend on external crates (like crossterm) or the standard library (std), but they have no dependencies on other modules in your project. This is a sign of excellent, decoupled design. Their use statements don’t need any crate:: paths to your other files.

For example, src/terminal.rs is perfect as it is:

// src/terminal.rs

use crossterm::terminal;
use std::io;

// ... struct RawMode, impl Drop, fn size() ...

A Project Transformed

Take a moment to appreciate the new structure. You have successfully completed a major architectural refactoring. Your project is no longer a single, monolithic file but a well-organized collection of modules, each with a clear and distinct purpose. This separation of concerns will make adding new features, fixing bugs, and simply understanding the code vastly easier from now on.

You have successfully completed the final task of this project’s roadmap!

Future Enhancements

Your terminal editor is a fantastic, fully-functional portfolio piece. Now that you have a solid and scalable architecture, you can consider adding even more advanced features. Here are a few ideas for where to go next:

  • Syntax Highlighting: This is a classic and highly impressive feature. You could integrate a library like syntect to parse code and apply crossterm styling to highlight keywords, strings, and comments in real-time.
  • Vim-like Modal Editing: Your architecture is now perfect for implementing different editor “modes.” You could add an enum to your Editor state (e.g., Mode::Normal, Mode::Insert) and have your run loop’s match statement behave differently depending on the current mode.
  • Line Numbers: Modify your draw_rows logic to reserve space on the left-hand side for displaying line numbers. This involves adjusting horizontal offsets and rendering calculations.
  • Undo/Redo: Implement a command pattern where every text modification (insert_char, delete_char, etc.) is an object that can be stored on a stack. An “undo” action would pop from the stack and apply the inverse of the command.
  • Configuration File: Allow users to customize keybindings or colors by reading a configuration file (e.g., in TOML or JSON format) on startup.

Further Reading

To continue your journey into Rust’s powerful module system and software architecture, these resources are invaluable.