## Description of Feature or Change
Zed currently lacks a built-in way to convert a file’s indentation style
on the fly. While it's possible to change indentation behavior via
global or language-specific settings, these changes are persistent and
broad in scope as they apply to all files or all files of a given
language. We believe this could be improved for quick one-off
adjustments to specific files.
This PR introduces two new editor commands:
`Editor::convert_indentation_to_spaces` and
`Editor::convert_indentation_to_tabs`. These commands allow users to
convert the indentation of either the entire buffer or a selection of
lines, to spaces or tabs. Indentation levels are preserved, and any
mixed whitespace lines are properly normalized.
This feature is inspired by VS Code’s "Convert Indentation to
Tabs/Spaces" commands, but offers faster execution and supports
selection-based conversion, making it more flexible for quick formatting
changes.
## Implementation Details
To enable selection-based indentation conversion, we initially
considered reusing the existing `Editor::manipulate_lines` function,
which handles selections for line-based manipulations. However, this
method was designed specifically for operations like sorting or
reversing lines, and does not allow modifications to the line contents
themselves.
To address this limitation, we refactored the method into a more
flexible version: `Editor::manipulate_generic_lines`. This new method
passes a reference to the selected text directly into a callback, giving
the callback full control over how to process and construct the
resulting lines. The callback returns a `String` containing the modified
text, as well as the number of lines before and after the
transformation. These counts are computed using `.len()` on the line
vectors during manipulation, which is more efficient than calculating
them after the fact.
```rust
fn manipulate_generic_lines<M>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
mut manipulate: M,
) where
M: FnMut(&str) -> (String, usize, usize),
{
// ... Get text from buffer.text_for_range() ...
let (new_text, lines_before, lines_after) = manipulate(&text);
// ...
```
We now introduce two specialized methods:
`Editor::manipulate_mutable_lines` and
`Editor::manipulate_immutable_lines`. Each editor command selects the
appropriate method based on whether it needs to modify line contents or
simply reorder them. This distinction is important for performance: when
line contents remain unchanged, working with an immutable reference as
`&mut Vec<&str>` is both faster and more memory-efficient than using an
owned `&mut Vec<String>`.
## Demonstration
https://github.com/user-attachments/assets/e50b37ea-a128-4c2a-b252-46c3c4530d97
Release Notes:
- Added `editor::ConvertIndentationToSpaces` and
`editor::ConvertIndentationToTabs` actions to change editor indents
---------
Co-authored-by: Pedro Silveira <pedroruanosilveira@tecnico.ulisboa.pt>
469 lines
12 KiB
Rust
469 lines
12 KiB
Rust
//! This module contains all actions supported by [`Editor`].
|
|
use super::*;
|
|
use gpui::{Action, actions};
|
|
use schemars::JsonSchema;
|
|
use util::serde::default_true;
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct SelectNext {
|
|
#[serde(default)]
|
|
pub replace_newest: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct SelectPrevious {
|
|
#[serde(default)]
|
|
pub replace_newest: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MoveToBeginningOfLine {
|
|
#[serde(default = "default_true")]
|
|
pub stop_at_soft_wraps: bool,
|
|
#[serde(default)]
|
|
pub stop_at_indent: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct SelectToBeginningOfLine {
|
|
#[serde(default)]
|
|
pub(super) stop_at_soft_wraps: bool,
|
|
#[serde(default)]
|
|
pub stop_at_indent: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct DeleteToBeginningOfLine {
|
|
#[serde(default)]
|
|
pub(super) stop_at_indent: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MovePageUp {
|
|
#[serde(default)]
|
|
pub(super) center_cursor: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MovePageDown {
|
|
#[serde(default)]
|
|
pub(super) center_cursor: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MoveToEndOfLine {
|
|
#[serde(default = "default_true")]
|
|
pub stop_at_soft_wraps: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct SelectToEndOfLine {
|
|
#[serde(default)]
|
|
pub(super) stop_at_soft_wraps: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ToggleCodeActions {
|
|
// Source from which the action was deployed.
|
|
#[serde(default)]
|
|
#[serde(skip)]
|
|
pub deployed_from: Option<CodeActionSource>,
|
|
// Run first available task if there is only one.
|
|
#[serde(default)]
|
|
#[serde(skip)]
|
|
pub quick_launch: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Debug)]
|
|
pub enum CodeActionSource {
|
|
Indicator(DisplayRow),
|
|
RunMenu(DisplayRow),
|
|
QuickActionBar,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ConfirmCompletion {
|
|
#[serde(default)]
|
|
pub item_ix: Option<usize>,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ComposeCompletion {
|
|
#[serde(default)]
|
|
pub item_ix: Option<usize>,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ConfirmCodeAction {
|
|
#[serde(default)]
|
|
pub item_ix: Option<usize>,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ToggleComments {
|
|
#[serde(default)]
|
|
pub advance_downwards: bool,
|
|
#[serde(default)]
|
|
pub ignore_indent: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MoveUpByLines {
|
|
#[serde(default)]
|
|
pub(super) lines: u32,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MoveDownByLines {
|
|
#[serde(default)]
|
|
pub(super) lines: u32,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct SelectUpByLines {
|
|
#[serde(default)]
|
|
pub(super) lines: u32,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct SelectDownByLines {
|
|
#[serde(default)]
|
|
pub(super) lines: u32,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ExpandExcerpts {
|
|
#[serde(default)]
|
|
pub(super) lines: u32,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ExpandExcerptsUp {
|
|
#[serde(default)]
|
|
pub(super) lines: u32,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ExpandExcerptsDown {
|
|
#[serde(default)]
|
|
pub(super) lines: u32,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ShowCompletions {
|
|
#[serde(default)]
|
|
pub(super) trigger: Option<String>,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
pub struct HandleInput(pub String);
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct DeleteToNextWordEnd {
|
|
#[serde(default)]
|
|
pub ignore_newlines: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct DeleteToPreviousWordStart {
|
|
#[serde(default)]
|
|
pub ignore_newlines: bool,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
pub struct FoldAtLevel(pub u32);
|
|
|
|
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
|
|
#[action(namespace = editor)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct SpawnNearestTask {
|
|
#[serde(default)]
|
|
pub reveal: task::RevealStrategy,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Default)]
|
|
pub enum UuidVersion {
|
|
#[default]
|
|
V4,
|
|
V7,
|
|
}
|
|
|
|
actions!(debugger, [RunToCursor, EvaluateSelectedText]);
|
|
|
|
actions!(
|
|
go_to_line,
|
|
[
|
|
#[action(name = "Toggle")]
|
|
ToggleGoToLine
|
|
]
|
|
);
|
|
|
|
actions!(
|
|
editor,
|
|
[
|
|
AcceptEditPrediction,
|
|
AcceptPartialCopilotSuggestion,
|
|
AcceptPartialEditPrediction,
|
|
AddSelectionAbove,
|
|
AddSelectionBelow,
|
|
ApplyAllDiffHunks,
|
|
ApplyDiffHunk,
|
|
Backspace,
|
|
Cancel,
|
|
CancelFlycheck,
|
|
CancelLanguageServerWork,
|
|
ClearFlycheck,
|
|
ConfirmRename,
|
|
ConfirmCompletionInsert,
|
|
ConfirmCompletionReplace,
|
|
ContextMenuFirst,
|
|
ContextMenuLast,
|
|
ContextMenuNext,
|
|
ContextMenuPrevious,
|
|
ConvertIndentationToSpaces,
|
|
ConvertIndentationToTabs,
|
|
ConvertToKebabCase,
|
|
ConvertToLowerCamelCase,
|
|
ConvertToLowerCase,
|
|
ConvertToOppositeCase,
|
|
ConvertToSnakeCase,
|
|
ConvertToTitleCase,
|
|
ConvertToUpperCamelCase,
|
|
ConvertToUpperCase,
|
|
ConvertToRot13,
|
|
ConvertToRot47,
|
|
Copy,
|
|
CopyAndTrim,
|
|
CopyFileLocation,
|
|
CopyHighlightJson,
|
|
CopyFileName,
|
|
CopyFileNameWithoutExtension,
|
|
CopyPermalinkToLine,
|
|
Cut,
|
|
CutToEndOfLine,
|
|
Delete,
|
|
DeleteLine,
|
|
DeleteToEndOfLine,
|
|
DeleteToNextSubwordEnd,
|
|
DeleteToPreviousSubwordStart,
|
|
DisplayCursorNames,
|
|
DuplicateLineDown,
|
|
DuplicateLineUp,
|
|
DuplicateSelection,
|
|
#[action(deprecated_aliases = ["editor::ExpandAllHunkDiffs"])]
|
|
ExpandAllDiffHunks,
|
|
ExpandMacroRecursively,
|
|
FindAllReferences,
|
|
FindNextMatch,
|
|
FindPreviousMatch,
|
|
Fold,
|
|
FoldAll,
|
|
FoldFunctionBodies,
|
|
FoldRecursive,
|
|
FoldSelectedRanges,
|
|
ToggleFold,
|
|
ToggleFoldRecursive,
|
|
Format,
|
|
FormatSelections,
|
|
GoToDeclaration,
|
|
GoToDeclarationSplit,
|
|
GoToDefinition,
|
|
GoToDefinitionSplit,
|
|
GoToDiagnostic,
|
|
GoToHunk,
|
|
GoToPreviousHunk,
|
|
GoToImplementation,
|
|
GoToImplementationSplit,
|
|
GoToNextChange,
|
|
GoToParentModule,
|
|
GoToPreviousChange,
|
|
GoToPreviousDiagnostic,
|
|
GoToTypeDefinition,
|
|
GoToTypeDefinitionSplit,
|
|
HalfPageDown,
|
|
HalfPageUp,
|
|
Hover,
|
|
Indent,
|
|
InsertUuidV4,
|
|
InsertUuidV7,
|
|
JoinLines,
|
|
KillRingCut,
|
|
KillRingYank,
|
|
LineDown,
|
|
LineUp,
|
|
MoveDown,
|
|
MoveLeft,
|
|
MoveLineDown,
|
|
MoveLineUp,
|
|
MoveRight,
|
|
MoveToBeginning,
|
|
MoveToEnclosingBracket,
|
|
MoveToEnd,
|
|
MoveToEndOfParagraph,
|
|
MoveToNextSubwordEnd,
|
|
MoveToNextWordEnd,
|
|
MoveToPreviousSubwordStart,
|
|
MoveToPreviousWordStart,
|
|
MoveToStartOfParagraph,
|
|
MoveToStartOfExcerpt,
|
|
MoveToStartOfNextExcerpt,
|
|
MoveToEndOfExcerpt,
|
|
MoveToEndOfPreviousExcerpt,
|
|
MoveUp,
|
|
Newline,
|
|
NewlineAbove,
|
|
NewlineBelow,
|
|
NextEditPrediction,
|
|
NextScreen,
|
|
OpenContextMenu,
|
|
OpenExcerpts,
|
|
OpenExcerptsSplit,
|
|
OpenProposedChangesEditor,
|
|
OpenDocs,
|
|
OpenPermalinkToLine,
|
|
#[action(deprecated_aliases = ["editor::OpenFile"])]
|
|
OpenSelectedFilename,
|
|
OpenSelectionsInMultibuffer,
|
|
OpenUrl,
|
|
OrganizeImports,
|
|
Outdent,
|
|
AutoIndent,
|
|
PageDown,
|
|
PageUp,
|
|
Paste,
|
|
PreviousEditPrediction,
|
|
Redo,
|
|
RedoSelection,
|
|
Rename,
|
|
RestartLanguageServer,
|
|
RevealInFileManager,
|
|
ReverseLines,
|
|
RevertFile,
|
|
ReloadFile,
|
|
Rewrap,
|
|
RunFlycheck,
|
|
ScrollCursorBottom,
|
|
ScrollCursorCenter,
|
|
ScrollCursorCenterTopBottom,
|
|
ScrollCursorTop,
|
|
SelectAll,
|
|
SelectAllMatches,
|
|
SelectToStartOfExcerpt,
|
|
SelectToStartOfNextExcerpt,
|
|
SelectToEndOfExcerpt,
|
|
SelectToEndOfPreviousExcerpt,
|
|
SelectDown,
|
|
SelectEnclosingSymbol,
|
|
SelectLargerSyntaxNode,
|
|
SelectLeft,
|
|
SelectLine,
|
|
SelectPageDown,
|
|
SelectPageUp,
|
|
SelectRight,
|
|
SelectSmallerSyntaxNode,
|
|
SelectToBeginning,
|
|
SelectToEnd,
|
|
SelectToEndOfParagraph,
|
|
SelectToNextSubwordEnd,
|
|
SelectToNextWordEnd,
|
|
SelectToPreviousSubwordStart,
|
|
SelectToPreviousWordStart,
|
|
SelectToStartOfParagraph,
|
|
SelectUp,
|
|
ShowCharacterPalette,
|
|
ShowEditPrediction,
|
|
ShowSignatureHelp,
|
|
ShowWordCompletions,
|
|
ShuffleLines,
|
|
SortLinesCaseInsensitive,
|
|
SortLinesCaseSensitive,
|
|
SplitSelectionIntoLines,
|
|
StopLanguageServer,
|
|
SwitchSourceHeader,
|
|
Tab,
|
|
Backtab,
|
|
ToggleBreakpoint,
|
|
ToggleCase,
|
|
DisableBreakpoint,
|
|
EnableBreakpoint,
|
|
EditLogBreakpoint,
|
|
ToggleAutoSignatureHelp,
|
|
ToggleGitBlameInline,
|
|
OpenGitBlameCommit,
|
|
ToggleDiagnostics,
|
|
ToggleIndentGuides,
|
|
ToggleInlayHints,
|
|
ToggleInlineValues,
|
|
ToggleInlineDiagnostics,
|
|
ToggleEditPrediction,
|
|
ToggleLineNumbers,
|
|
ToggleMinimap,
|
|
SwapSelectionEnds,
|
|
SetMark,
|
|
ToggleRelativeLineNumbers,
|
|
#[action(deprecated_aliases = ["editor::ToggleHunkDiff"])]
|
|
ToggleSelectedDiffHunks,
|
|
ToggleSelectionMenu,
|
|
ToggleSoftWrap,
|
|
ToggleTabBar,
|
|
Transpose,
|
|
Undo,
|
|
UndoSelection,
|
|
UnfoldAll,
|
|
UnfoldLines,
|
|
UnfoldRecursive,
|
|
UniqueLinesCaseInsensitive,
|
|
UniqueLinesCaseSensitive,
|
|
]
|
|
);
|