Building a Simple Modal Line Editor in Python: A Step-by-Step Guide

Post Stastics

  • This post has 1883 words.
  • Estimated read time is 8.97 minute(s).

Introduction

In many text-processing tasks, a line editor can be a valuable tool. Unlike full-screen editors like Vim or Emacs, line editors operate on one line at a time. This makes them simpler and more suitable for quick edits in a command-line environment. In this tutorial, we’ll build a simple modal line editor in Python, breaking down each feature step-by-step to understand its functionality. By the end, you’ll have a basic yet functional line editor that supports inserting, editing, and deleting lines.

Step 1: Setting Up the Basic Structure

We’ll start by creating a class to represent our line editor. This class will have a constructor to initialize the list of lines and the current mode (insert or command).

Code

class ModalLineEditor:
    def __init__(self):
        self.lines = []
        self.mode = 'insert'

Explanation

  • __init__: Initializes an empty list lines to store the lines of text and sets the initial mode to insert.

Step 2: Displaying the Lines

Next, we need a method to display the current lines with their line numbers. This will help users see the current state of the text they are editing.

Code

def display(self):
    """Display the current lines with their line numbers."""
    for idx, line in enumerate(self.lines, 1):
        print(f"{idx}: {line}")

Explanation

  • display: Iterates over the lines list, printing each line with its corresponding line number.

Step 3: Adding Lines in Insert Mode

We’ll implement a method to add new lines. This will be the primary function in insert mode.

Code

def add_line(self, line):
    """Add a new line to the list of lines."""
    self.lines.append(line)

Explanation

  • add_line: Appends the new line to the lines list.

Step 4: Editing and Deleting Lines

In command mode, users should be able to edit or delete existing lines. We’ll add a method for this functionality.

Code

def edit_line(self, line_number, new_content):
    """Edit an existing line. If new_content is empty, the line is deleted."""
    if 1 <= line_number <= len(self.lines):
        if new_content.strip() == "":
            del self.lines[line_number - 1]
        else:
            self.lines[line_number - 1] = new_content
    else:
        print("Invalid line number.")

Explanation

  • edit_line: Takes a line_number and new_content. If new_content is empty, it deletes the line; otherwise, it updates the line with the new content. It also checks if the line number is valid.

Step 5: Implementing the Run Method

The run method will handle the user interface, switching between insert and command modes based on user input.

Code

def run(self):
    """Run the line editor interface."""
    print("Modal Line Editor. Type ':q' to quit, ':e' to switch to Command Mode, ':i' to switch to Insert Mode.")
    while True:
        if self.mode == 'insert':
            new_line = input("Insert Mode - Enter new line (or ':e' to switch to Command Mode): ").strip()
            if new_line == ":q":
                break
            elif new_line == ":e":
                self.mode = 'command'
            else:
                self.add_line(new_line)
        elif self.mode == 'command':
            self.display()
            command = input("Command Mode - Enter line number to edit, ':i' to switch to Insert Mode, ':q' to quit: ").strip().lower()
            if command == ":q":
                break
            elif command == ":i":
                self.mode = 'insert'
            elif command.isdigit():
                line_number = int(command)
                new_content = input(f"Editing line {line_number}. Enter new content (leave empty to delete): ").strip()
                self.edit_line(line_number, new_content)
            else:
                print("Invalid command. Please enter a line number, ':i' to switch to Insert Mode, or ':q' to quit.")

Explanation

  • run: Manages the main loop of the editor. It prints instructions and switches between modes based on user input. In insert mode, it adds new lines or switches to command mode. In command mode, it displays the lines and allows for editing or deletion of lines.

Step 6: Running the Editor

Finally, we’ll add a simple block to instantiate and run our editor.

Code

if __name__ == "__main__":
    editor = ModalLineEditor()
    editor.run()

Explanation

  • This block checks if the script is run directly (not imported) and starts the editor.

Complete Code

Here is the complete code for our simple modal line editor:

class ModalLineEditor:
    def __init__(self):
        self.lines = []
        self.mode = 'insert'

    def display(self):
        """Display the current lines with their line numbers."""
        for idx, line in enumerate(self.lines, 1):
            print(f"{idx}: {line}")

    def add_line(self, line):
        """Add a new line to the list of lines."""
        self.lines.append(line)

    def edit_line(self, line_number, new_content):
        """Edit an existing line. If new_content is empty, the line is deleted."""
        if 1 <= line_number <= len(self.lines):
            if new_content.strip() == "":
                del self.lines[line_number - 1]
            else:
                self.lines[line_number - 1] = new_content
        else:
            print("Invalid line number.")

    def run(self):
        """Run the line editor interface."""
        print("Modal Line Editor. Type ':q' to quit, ':e' to switch to Command Mode, ':i' to switch to Insert Mode.")
        while True:
            if self.mode == 'insert':
                new_line = input("Insert Mode - Enter new line (or ':e' to switch to Command Mode): ").strip()
                if new_line == ":q":
                    break
                elif new_line == ":e":
                    self.mode = 'command'
                else:
                    self.add_line(new_line)
            elif self.mode == 'command':
                self.display()
                command = input("Command Mode - Enter line number to edit, ':i' to switch to Insert Mode, ':q' to quit: ").strip().lower()
                if command == ":q":
                    break
                elif command == ":i":
                    self.mode = 'insert'
                elif command.isdigit():
                    line_number = int(command)
                    new_content = input(f"Editing line {line_number}. Enter new content (leave empty to delete): ").strip()
                    self.edit_line(line_number, new_content)
                else:
                    print("Invalid command. Please enter a line number, ':i' to switch to Insert Mode, or ':q' to quit.")

if __name__ == "__main__":
    editor = ModalLineEditor()
    editor.run()

Going Further

Enhancing Insert Mode

We can enhance the insert mode to allow users to specify whether they want to insert a new line before or after a target line number. This makes the editor more versatile and user-friendly.

Code

def add_line(self, line, position=None, target_line=None):
    """Add a new line to the list of lines. If position and target_line are specified, insert before or after the target line."""
    if position and target_line and 1 <= target_line <= len(self.lines):
        index = target_line - 1
        if position == 'before':
            self.lines.insert(index, line)
        elif position == 'after':
            self.lines.insert(index + 1, line)
        else:
            print("Invalid position. Use 'before' or 'after'.")
    else:
        self.lines.append(line)

Explanation

  • add_line: Now accepts position and target_line parameters. If these are provided and valid, the new line is inserted before or after the specified line.

Modifying the run Method

We’ll modify the run method to handle the new insertion options in insert mode.

Code

def run(self):
    """Run the line editor interface."""
    print("Modal Line Editor. Type ':q' to quit, ':e' to switch to Command Mode, ':i' to switch to Insert Mode.")
    while True:
        if self.mode == 'insert':
            command = input("Insert Mode - Enter new line, 'before [line number]', 'after [line number]', or ':e' to switch to Command Mode: ").strip()
            if command == ":q":
                break
            elif command == ":e":
                self.mode = 'command'
            elif command.startswith('before ') or command.startswith('after '):
                try:
                    position, target_line = command.split()
                    target_line = int(target_line)
                    new_line = input(f"Enter new line to insert {position} line {target_line}: ").strip()
                    self.add_line(new_line, position, target_line)
                except ValueError:
                    print("Invalid command. Please use 'before [line number]' or 'after [line number]'.")
            else:
                self.add_line(command)
        elif self.mode == 'command':
            self.display()
            command = input("Command Mode - Enter line number to edit, ':i' to switch to Insert Mode, ':q' to quit: ").strip().lower()
            if command == ":q":
                break
            elif command == ":i":
                self.mode = 'insert'
            elif command.isdigit():
                line_number = int(command)
                new_content = input(f"Editing line {line_number}. Enter new content (leave empty to delete): ").strip()
                self.edit_line(line_number, new_content)
            else:
                print("Invalid command. Please enter a line number, ':i' to switch to Insert Mode, or ':q' to quit.")

Explanation

  • The run method now interprets commands for inserting lines before or after a specified line number.

Adding a Yank Command

A yank command can be used to delete a target line, similar to cutting a line of text.

Code

def yank_line(self, line_number):
    """Yank (delete) a line without requiring new content."""
    if 1 <= line_number <= len(self.lines):
        del self.lines[line_number - 1]
    else:
        print("Invalid line number.")

Explanation

  • yank_line: Deletes the specified line without requiring new content.

Modifying the run Method for Yank Command

We’ll update the run method to incorporate the yank command.

Code

def run(self):
    """Run the line editor interface."""
    print("Modal Line Editor. Type ':q' to quit, ':e' to switch to Command Mode, ':i' to switch to Insert Mode.")
    while True:
        if self.mode == 'insert':
            command = input("Insert Mode - Enter new line, 'before [line number]', 'after [line number]', or ':e' to switch to Command Mode: ").strip()
            if command == ":q":
                break
            elif command == ":e":
                self.mode = 'command'
            elif command.startswith('before ') or command.startswith('after '):
                try:
                    position, target_line = command.split()
                    target_line = int(target_line)
                    new_line = input(f"Enter new line to insert {position} line {target_line}: ").strip()
                    self.add_line(new_line, position, target_line)
                except ValueError:
                    print("Invalid command. Please use 'before [line number]' or 'after [line number]'.")
            else:
                self.add_line(command)
        elif self.mode == 'command':
            self.display()
            command = input("Command Mode - Enter line number to edit, ':i' to switch to Insert Mode, ':y [line number]' to yank a line, ':q' to quit: ").strip().lower()
            if command == ":q":
                break
            elif command == ":i":
                self.mode = 'insert'
            elif command.startswith(':y '):
                try:
                    line_number = int(command[3:])
                    self.yank_line(line_number)
                except ValueError:
                    print("Invalid line number for yank command.")
            elif command.isdigit():
                line_number = int(command)
                new_content = input(f"Editing line {line_number}. Enter new content (leave empty to delete): ").strip()
                self.edit_line(line_number, new_content)
            else:
                print("Invalid command. Please enter a line number, ':i' to switch to Insert Mode, ':y [line number]' to yank a line, or ':q' to quit.")

Explanation

  • The run method now includes the yank command, allowing users to delete lines by entering :y [line number].

Other Possible Commands

There are many additional commands you could add to make the editor more powerful:

  1. Undo/Redo: Implementing an undo/redo stack to revert changes.
  2. Search and Replace: Adding functionality to search for specific text and replace it.
  3. Save to File: Allowing users to save the current lines to a text file.
  4. Load from File: Loading lines from an existing text file.
  5. Copy and Paste: Implementing copy and paste functionality for lines.
  6. Move Lines: Adding commands to move lines up or down in the list.

Conclusion

By extending our simple modal line editor with features like inserting before or after a specific line and adding a yank command, we have made it more flexible and powerful. There are numerous other features you can implement to enhance its functionality, depending on your needs. This tutorial provides a foundation for building a more sophisticated line editor in Python.

Leave a Reply

Your email address will not be published. Required fields are marked *