← Back to DevBytes

Zed Refactoring: Complete Guide

Introduction to Zed Refactoring

Zed is a high-performance, multiplayer code editor built in Rust by the creators of Atom. While it is best known for its blazing speed and collaborative features, Zed also ships with a growing set of refactoring tools that make restructuring codebases efficient and safe. This guide walks through everything you need to know about refactoring in Zed, from basic symbol renames to multi-cursor edits and language-server-powered transformations.

What Is Refactoring in Zed?

Refactoring in Zed refers to the set of built-in editor operations and language-server integrations that allow you to restructure code without changing its external behavior. These tools leverage Zed's native Tree-sitter parsing and its integration with the Language Server Protocol (LSP) to provide accurate, context-aware transformations.

Unlike traditional find-and-replace, Zed's refactoring features understand the semantic structure of your code. This means a rename operation knows the difference between a local variable named user and an imported module named user, preventing accidental cross-scope replacements.

Why Refactoring in Zed Matters

Core Refactoring Features

1. Rename Symbol

The most common refactoring operation is renaming a symbol. Zed delegates this to the active language server, which resolves the full scope of the symbol across the project.

To rename a symbol, place your cursor on it and press F2. A small inline prompt appears where you type the new name. Press Enter to apply the change across all references.

// Before refactor
function calcTotal(items) {
  return items.reduce((sum, i) => sum + i.price, 0);
}

// After renaming `i` to `item` via F2
function calcTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

The rename propagates to every reference the language server identifies, including those in other files. This is far safer than a text-based find-and-replace.

2. Extract Variable

Extracting a variable is useful when an inline expression becomes too complex. Select the expression and use the extract command from the command palette (Cmd+Shift+P on macOS, Ctrl+Shift+P on Linux) by searching for "Extract Variable".

// Before
function getDiscount(price) {
  return price * 0.85 * 1.2;
}

// After extracting `price * 0.85`
function getDiscount(price) {
  const discounted = price * 0.85;
  return discounted * 1.2;
}

3. Extract Function

Extract Function is one of the most powerful refactoring tools. Select a block of code, open the command palette, and choose "Extract Function". Zed will generate a new function, infer parameters from the selected code's external dependencies, and replace the selection with a call to the new function.

// Before
function processOrder(order) {
  const total = order.items.reduce((s, i) => s + i.price, 0);
  const tax = total * 0.08;
  const shipping = total > 100 ? 0 : 10;
  return total + tax + shipping;
}

// After extracting the tax calculation
function calculateTax(total) {
  return total * 0.08;
}

function processOrder(order) {
  const total = order.items.reduce((s, i) => s + i.price, 0);
  const tax = calculateTax(total);
  const shipping = total > 100 ? 0 : 10;
  return total + tax + shipping;
}

4. Multi-Cursor Editing

While not strictly an LSP feature, multi-cursor editing is a cornerstone of manual refactoring in Zed. You can add cursors with Cmd+D (macOS) or Ctrl+D (Linux) to select the next match of the current selection, or use Cmd+Shift+L to select all matches in the current file.

// Selecting all `console.log` calls and replacing with a logger
// 1. Place cursor on `console.log`
// 2. Press Cmd+Shift+L to select all occurrences
// 3. Type `logger.info`

// Before
console.log("Starting server");
console.log("Connected to DB");
console.log("Listening on port 3000");

// After
logger.info("Starting server");
logger.info("Connected to DB");
logger.info("Listening on port 3000");

5. Code Actions and Quick Fixes

Zed surfaces LSP code actions through a lightbulb icon or via the command palette. These actions include language-specific refactorings such as converting function declarations to arrow functions, adding missing imports, or inlining a constant.

To trigger code actions, place your cursor on a symbol and press Cmd+. (macOS) or Ctrl+. (Linux). A menu appears with available actions.

// TypeScript example: Convert to arrow function
// Before
function add(a, b) {
  return a + b;
}

// After applying "Convert to arrow function"
const add = (a, b) => {
  return a + b;
};

Configuring Refactoring Behavior

Language Server Setup

Refactoring quality depends heavily on the language server. Zed auto-detects many servers, but you can configure them explicitly in your settings.json.

{
  "lsp": {
    "rust-analyzer": {
      "initialization_options": {
        "checkOnSave": {
          "command": "clippy"
        }
      }
    },
    "typescript-language-server": {
      "initialization_options": {
        "preferences": {
          "importModuleSpecifierPreference": "relative"
        }
      }
    }
  }
}

Custom Keybindings

You can remap refactoring commands in your keymap.json file. Here is an example that binds rename to Cmd+R and extract function to Cmd+E:

[
  {
    "context": "Editor",
    "bindings": {
      "cmd-r": "editor::Rename",
      "cmd-e": "editor::ExtractFunction",
      "cmd-shift-v": "editor::ExtractVariable"
    }
  }
]

Project-Wide Refactoring Workflows

Using Project Search for Bulk Changes

For refactoring patterns that language servers cannot handle, Zed's project-wide search supports multi-buffer editing. Open project search with Cmd+Shift+F, enter your query, and use the "Replace" field to define the new text. You can preview each match before applying.

// Search pattern (regex enabled):
const (\w+) = require\(['"](.+)['"]\)

// Replacement:
import $1 from "$2"

// Before
const fs = require("fs");
const path = require("path");

// After
import fs from "fs";
import path from "path";

Multi-Buffer Refactoring

When project search returns results across many files, Zed opens them in a multi-buffer. You can edit directly in this buffer, and changes propagate to each source file. This is ideal for large-scale mechanical refactors like updating API endpoint names or renaming a commonly used utility function that the language server cannot fully resolve.

Best Practices for Refactoring in Zed

Common Refactoring Scenarios

Scenario 1: Renaming a Public API Function

When renaming a function that is part of a public API, use Rename Symbol (F2) to update all internal references, then manually update any external consumers or documentation.

// Before
export function fetchUserData(userId) {
  return api.get(`/users/${userId}`);
}

// After F2 rename to `getUserProfile`
export function getUserProfile(userId) {
  return api.get(`/users/${userId}`);
}

Scenario 2: Extracting a Reusable Component

In a React codebase, you can select JSX markup and use Extract Function to pull it into its own component file.

// Before: inline user card
function UserList({ users }) {
  return (
    <ul>
      {users.map(u => (
        <li key={u.id}>
          <img src={u.avatar} alt={u.name} />
          <span>{u.name}</span>
          <span>{u.email}</span>
        </li>
      ))}
    </ul>
  );
}

// After extracting UserCard component
function UserCard({ user }) {
  return (
    <li>
      <img src={user.avatar} alt={user.name} />
      <span>{user.name}</span>
      <span>{user.email}</span>
    </li>
  );
}

function UserList({ users }) {
  return (
    <ul>
      {users.map(u => (
        <UserCard key={u.id} user={u} />
      ))}
    </ul>
  );
}

Scenario 3: Inlining a Constant

Sometimes a constant adds indirection without value. Use the inline code action to replace references with the literal value, then remove the constant.

// Before
const MAX_RETRIES = 3;

function callWithRetry(fn) {
  for (let i = 0; i < MAX_RETRIES; i++) {
    try { return fn(); } catch (e) { /* retry */ }
  }
  throw new Error("Max retries exceeded");
}

// After inlining
function callWithRetry(fn) {
  for (let i = 0; i < 3; i++) {
    try { return fn(); } catch (e) { /* retry */ }
  }
  throw new Error("Max retries exceeded");
}

Troubleshooting Common Issues

Refactoring Options Are Unavailable

If rename or extract options are missing, the language server may not be running or may not support that capability. Check the status bar for LSP connection indicators and verify your language server is installed and configured.

Rename Misses References

Some language servers have limitations with dynamic references, string-based lookups, or cross-project symbols. In these cases, supplement the LSP rename with a project-wide search to catch any stragglers.

Extract Function Produces Unexpected Parameters

The extracted function's parameter list depends on what the selected code references from its outer scope. If the parameters look wrong, review your selection boundaries. Expanding or shrinking the selection with Tree-sitter commands often resolves the issue.

Conclusion

Refactoring in Zed combines the speed of a native editor with the semantic precision of modern language servers. By mastering Rename Symbol, Extract Function, Extract Variable, multi-cursor editing, and project-wide search, you can restructure code confidently and rapidly. The key is to lean on LSP-powered operations for correctness, supplement them with multi-cursor and project search for mechanical changes, and always validate your work with tests and version control. As Zed continues to evolve, its refactoring toolkit will only grow richer, making it an increasingly compelling choice for developers who value both performance and code quality.

— Ad —

Google AdSense will appear here after approval

← Back to all articles