vim: View Marks (#26885)
Closes #26884 Release Notes: - vim: Added `:marks` which brings up list of current marks - confirming on selected mark in the view jumps to that mark --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
parent
4c86cda909
commit
d82b547596
4 changed files with 381 additions and 9 deletions
|
@ -39,7 +39,7 @@ use crate::{
|
||||||
object::Object,
|
object::Object,
|
||||||
state::{Mark, Mode},
|
state::{Mark, Mode},
|
||||||
visual::VisualDeleteLine,
|
visual::VisualDeleteLine,
|
||||||
ToggleRegistersView, Vim,
|
ToggleMarksView, ToggleRegistersView, Vim,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
@ -860,6 +860,7 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
|
VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
|
||||||
|
VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
|
||||||
VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
|
VimCommand::new(("sor", "t"), SortLinesCaseSensitive).range(select_range),
|
||||||
VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
|
VimCommand::new(("sort i", ""), SortLinesCaseInsensitive).range(select_range),
|
||||||
VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
|
VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"),
|
||||||
|
|
|
@ -183,10 +183,13 @@ impl Vim {
|
||||||
&mut self,
|
&mut self,
|
||||||
text: Arc<str>,
|
text: Arc<str>,
|
||||||
line: bool,
|
line: bool,
|
||||||
|
should_pop_operator: bool,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if should_pop_operator {
|
||||||
self.pop_operator(window, cx);
|
self.pop_operator(window, cx);
|
||||||
|
}
|
||||||
let mark = self
|
let mark = self
|
||||||
.update_editor(window, cx, |vim, editor, window, cx| {
|
.update_editor(window, cx, |vim, editor, window, cx| {
|
||||||
vim.get_mark(&text, editor, window, cx)
|
vim.get_mark(&text, editor, window, cx)
|
||||||
|
|
|
@ -2,27 +2,29 @@ use crate::command::command_interceptor;
|
||||||
use crate::normal::repeat::Replayer;
|
use crate::normal::repeat::Replayer;
|
||||||
use crate::surrounds::SurroundsType;
|
use crate::surrounds::SurroundsType;
|
||||||
use crate::{motion::Motion, object::Object};
|
use crate::{motion::Motion, object::Object};
|
||||||
use crate::{ToggleRegistersView, UseSystemClipboard, Vim, VimSettings};
|
use crate::{ToggleMarksView, ToggleRegistersView, UseSystemClipboard, Vim, VimAddon, VimSettings};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
|
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
|
||||||
use db::define_connection;
|
use db::define_connection;
|
||||||
use db::sqlez_macros::sql;
|
use db::sqlez_macros::sql;
|
||||||
use editor::display_map::{is_invisible, replacement};
|
use editor::display_map::{is_invisible, replacement};
|
||||||
use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer};
|
use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, App, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, Entity, EntityId,
|
Action, App, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, DismissEvent, Entity,
|
||||||
Global, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity,
|
EntityId, Global, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity,
|
||||||
};
|
};
|
||||||
use language::{Buffer, BufferEvent, BufferId, Point};
|
use language::{Buffer, BufferEvent, BufferId, Chunk, Point};
|
||||||
|
use multi_buffer::MultiBufferRow;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{Project, ProjectItem, ProjectPath};
|
use project::{Project, ProjectItem, ProjectPath};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use std::borrow::BorrowMut;
|
use std::borrow::BorrowMut;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::{fmt::Display, ops::Range, sync::Arc};
|
use std::{fmt::Display, ops::Range, sync::Arc};
|
||||||
use text::Bias;
|
use text::{Bias, ToPoint};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
h_flex, rems, ActiveTheme, Context, Div, FluentBuilder, KeyBinding, ParentElement,
|
h_flex, rems, ActiveTheme, Context, Div, FluentBuilder, KeyBinding, ParentElement,
|
||||||
|
@ -627,6 +629,11 @@ impl VimGlobals {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
cx.observe_new(move |workspace: &mut Workspace, window, _| {
|
||||||
|
MarksView::register(workspace, window);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
let mut was_enabled = None;
|
let mut was_enabled = None;
|
||||||
|
|
||||||
cx.observe_global::<SettingsStore>(move |cx| {
|
cx.observe_global::<SettingsStore>(move |cx| {
|
||||||
|
@ -1232,6 +1239,366 @@ impl RegistersView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum MarksMatchInfo {
|
||||||
|
Path(Arc<Path>),
|
||||||
|
Title(String),
|
||||||
|
Content {
|
||||||
|
line: String,
|
||||||
|
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarksMatchInfo {
|
||||||
|
fn from_chunks<'a>(chunks: impl Iterator<Item = Chunk<'a>>, cx: &App) -> Self {
|
||||||
|
let mut line = String::new();
|
||||||
|
let mut highlights = Vec::new();
|
||||||
|
let mut offset = 0;
|
||||||
|
for chunk in chunks {
|
||||||
|
line.push_str(chunk.text);
|
||||||
|
if let Some(highlight_style) = chunk.syntax_highlight_id {
|
||||||
|
if let Some(highlight) = highlight_style.style(cx.theme().syntax()) {
|
||||||
|
highlights.push((offset..offset + chunk.text.len(), highlight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset += chunk.text.len();
|
||||||
|
}
|
||||||
|
MarksMatchInfo::Content { line, highlights }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MarksMatch {
|
||||||
|
name: String,
|
||||||
|
position: Point,
|
||||||
|
info: MarksMatchInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MarksViewDelegate {
|
||||||
|
selected_index: usize,
|
||||||
|
matches: Vec<MarksMatch>,
|
||||||
|
point_column_width: usize,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for MarksViewDelegate {
|
||||||
|
type ListItem = Div;
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||||
|
Arc::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
_: String,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> gpui::Task<()> {
|
||||||
|
let Some(workspace) = self.workspace.upgrade().clone() else {
|
||||||
|
return Task::ready(());
|
||||||
|
};
|
||||||
|
cx.spawn(async move |picker, cx| {
|
||||||
|
let mut matches = Vec::new();
|
||||||
|
let _ = workspace.update(cx, |workspace, cx| {
|
||||||
|
let entity_id = cx.entity_id();
|
||||||
|
let Some(editor) = workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.and_then(|item| item.act_as::<Editor>(cx))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let editor = editor.read(cx);
|
||||||
|
let mut has_seen = HashSet::new();
|
||||||
|
let Some(marks_state) = cx.global::<VimGlobals>().marks.get(&entity_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let marks_state = marks_state.read(cx);
|
||||||
|
|
||||||
|
if let Some(map) = marks_state
|
||||||
|
.multibuffer_marks
|
||||||
|
.get(&editor.buffer().entity_id())
|
||||||
|
{
|
||||||
|
for (name, anchors) in map {
|
||||||
|
if has_seen.contains(name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
has_seen.insert(name.clone());
|
||||||
|
let Some(anchor) = anchors.first() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
let position = anchor.to_point(&snapshot);
|
||||||
|
|
||||||
|
let chunks = snapshot.chunks(
|
||||||
|
Point::new(position.row, 0)
|
||||||
|
..Point::new(
|
||||||
|
position.row,
|
||||||
|
snapshot.line_len(MultiBufferRow(position.row)),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
matches.push(MarksMatch {
|
||||||
|
name: name.clone(),
|
||||||
|
position,
|
||||||
|
info: MarksMatchInfo::from_chunks(chunks, cx),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
if let Some(map) = marks_state.buffer_marks.get(&buffer.remote_id()) {
|
||||||
|
for (name, anchors) in map {
|
||||||
|
if has_seen.contains(name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
has_seen.insert(name.clone());
|
||||||
|
let Some(anchor) = anchors.first() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
let position = anchor.to_point(&snapshot);
|
||||||
|
let chunks = snapshot.chunks(
|
||||||
|
Point::new(position.row, 0)
|
||||||
|
..Point::new(position.row, snapshot.line_len(position.row)),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
matches.push(MarksMatch {
|
||||||
|
name: name.clone(),
|
||||||
|
position,
|
||||||
|
info: MarksMatchInfo::from_chunks(chunks, cx),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, mark_location) in marks_state.global_marks.iter() {
|
||||||
|
if has_seen.contains(name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
has_seen.insert(name.clone());
|
||||||
|
|
||||||
|
match mark_location {
|
||||||
|
MarkLocation::Buffer(entity_id) => {
|
||||||
|
if let Some(&anchor) = marks_state
|
||||||
|
.multibuffer_marks
|
||||||
|
.get(entity_id)
|
||||||
|
.and_then(|map| map.get(name))
|
||||||
|
.and_then(|anchors| anchors.first())
|
||||||
|
{
|
||||||
|
let Some((info, snapshot)) = workspace
|
||||||
|
.items(cx)
|
||||||
|
.filter_map(|item| item.act_as::<Editor>(cx))
|
||||||
|
.map(|entity| entity.read(cx).buffer())
|
||||||
|
.find(|buffer| buffer.entity_id().eq(entity_id))
|
||||||
|
.map(|buffer| {
|
||||||
|
(
|
||||||
|
MarksMatchInfo::Title(
|
||||||
|
buffer.read(cx).title(cx).to_string(),
|
||||||
|
),
|
||||||
|
buffer.read(cx).snapshot(cx),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
matches.push(MarksMatch {
|
||||||
|
name: name.clone(),
|
||||||
|
position: anchor.to_point(&snapshot),
|
||||||
|
info,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MarkLocation::Path(path) => {
|
||||||
|
if let Some(&position) = marks_state
|
||||||
|
.serialized_marks
|
||||||
|
.get(path.as_ref())
|
||||||
|
.and_then(|map| map.get(name))
|
||||||
|
.and_then(|points| points.first())
|
||||||
|
{
|
||||||
|
let info = MarksMatchInfo::Path(path.clone());
|
||||||
|
matches.push(MarksMatch {
|
||||||
|
name: name.clone(),
|
||||||
|
position,
|
||||||
|
info,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let _ = picker.update(cx, |picker, cx| {
|
||||||
|
matches.sort_by_key(|a| {
|
||||||
|
(
|
||||||
|
a.name.chars().next().map(|c| c.is_ascii_uppercase()),
|
||||||
|
a.name.clone(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let digits = matches
|
||||||
|
.iter()
|
||||||
|
.map(|m| (m.position.row + 1).ilog10() + (m.position.column + 1).ilog10())
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default();
|
||||||
|
picker.delegate.matches = matches;
|
||||||
|
picker.delegate.point_column_width = (digits + 4) as usize;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||||
|
let Some(vim) = self
|
||||||
|
.workspace
|
||||||
|
.upgrade()
|
||||||
|
.map(|w| w.read(cx))
|
||||||
|
.and_then(|w| w.focused_pane(window, cx).read(cx).active_item())
|
||||||
|
.and_then(|item| item.act_as::<Editor>(cx))
|
||||||
|
.and_then(|editor| editor.read(cx).addon::<VimAddon>().cloned())
|
||||||
|
.map(|addon| addon.entity)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(text): Option<Arc<str>> = self
|
||||||
|
.matches
|
||||||
|
.get(self.selected_index)
|
||||||
|
.map(|m| Arc::from(m.name.to_string().into_boxed_str()))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
vim.update(cx, |vim, cx| {
|
||||||
|
vim.jump(text, false, false, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _: &mut Window, _: &mut Context<Picker<Self>>) {}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let mark_match = self
|
||||||
|
.matches
|
||||||
|
.get(ix)
|
||||||
|
.expect("Invalid matches state: no element for index {ix}");
|
||||||
|
|
||||||
|
let mut left_output = String::new();
|
||||||
|
let mut left_runs = Vec::new();
|
||||||
|
left_output.push('`');
|
||||||
|
left_output.push_str(&mark_match.name);
|
||||||
|
left_runs.push((
|
||||||
|
0..left_output.len(),
|
||||||
|
HighlightStyle::color(cx.theme().colors().text_accent),
|
||||||
|
));
|
||||||
|
left_output.push(' ');
|
||||||
|
left_output.push(' ');
|
||||||
|
let point_column = format!(
|
||||||
|
"{},{}",
|
||||||
|
mark_match.position.row + 1,
|
||||||
|
mark_match.position.column + 1
|
||||||
|
);
|
||||||
|
left_output.push_str(&point_column);
|
||||||
|
if let Some(padding) = self.point_column_width.checked_sub(point_column.len()) {
|
||||||
|
left_output.push_str(&" ".repeat(padding));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (right_output, right_runs): (String, Vec<_>) = match &mark_match.info {
|
||||||
|
MarksMatchInfo::Path(path) => {
|
||||||
|
let s = path.to_string_lossy().to_string();
|
||||||
|
(
|
||||||
|
s.clone(),
|
||||||
|
vec![(0..s.len(), HighlightStyle::color(cx.theme().colors().text))],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MarksMatchInfo::Title(title) => (
|
||||||
|
title.clone(),
|
||||||
|
vec![(
|
||||||
|
0..title.len(),
|
||||||
|
HighlightStyle::color(cx.theme().colors().text),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
MarksMatchInfo::Content { line, highlights } => (line.clone(), highlights.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let theme = ThemeSettings::get_global(cx);
|
||||||
|
let text_style = TextStyle {
|
||||||
|
color: cx.theme().colors().editor_foreground,
|
||||||
|
font_family: theme.buffer_font.family.clone(),
|
||||||
|
font_features: theme.buffer_font.features.clone(),
|
||||||
|
font_fallbacks: theme.buffer_font.fallbacks.clone(),
|
||||||
|
font_size: theme.buffer_font_size(cx).into(),
|
||||||
|
line_height: (theme.line_height() * theme.buffer_font_size(cx)).into(),
|
||||||
|
font_weight: theme.buffer_font.weight,
|
||||||
|
font_style: theme.buffer_font.style,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(
|
||||||
|
h_flex()
|
||||||
|
.when(selected, |el| el.bg(cx.theme().colors().element_selected))
|
||||||
|
.font_buffer(cx)
|
||||||
|
.text_buffer(cx)
|
||||||
|
.h(theme.buffer_font_size(cx) * theme.line_height())
|
||||||
|
.px_2()
|
||||||
|
.child(StyledText::new(left_output).with_default_highlights(&text_style, left_runs))
|
||||||
|
.child(
|
||||||
|
StyledText::new(right_output).with_default_highlights(&text_style, right_runs),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MarksView {}
|
||||||
|
|
||||||
|
impl MarksView {
|
||||||
|
fn register(workspace: &mut Workspace, _window: Option<&mut Window>) {
|
||||||
|
workspace.register_action(|workspace, _: &ToggleMarksView, window, cx| {
|
||||||
|
Self::toggle(workspace, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||||
|
let handle = cx.weak_entity();
|
||||||
|
workspace.toggle_modal(window, cx, move |window, cx| {
|
||||||
|
MarksView::new(handle, window, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Picker<MarksViewDelegate>>,
|
||||||
|
) -> Picker<MarksViewDelegate> {
|
||||||
|
let matches = Vec::default();
|
||||||
|
let delegate = MarksViewDelegate {
|
||||||
|
selected_index: 0,
|
||||||
|
point_column_width: 0,
|
||||||
|
matches,
|
||||||
|
workspace,
|
||||||
|
};
|
||||||
|
Picker::nonsearchable_uniform_list(delegate, window, cx)
|
||||||
|
.width(rems(36.))
|
||||||
|
.modal(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
define_connection! (
|
define_connection! (
|
||||||
pub static ref DB: VimDb<WorkspaceDb> = &[
|
pub static ref DB: VimDb<WorkspaceDb> = &[
|
||||||
sql! (
|
sql! (
|
||||||
|
|
|
@ -144,6 +144,7 @@ actions!(
|
||||||
PushReplace,
|
PushReplace,
|
||||||
PushDeleteSurrounds,
|
PushDeleteSurrounds,
|
||||||
PushMark,
|
PushMark,
|
||||||
|
ToggleMarksView,
|
||||||
PushIndent,
|
PushIndent,
|
||||||
PushOutdent,
|
PushOutdent,
|
||||||
PushAutoIndent,
|
PushAutoIndent,
|
||||||
|
@ -1599,7 +1600,7 @@ impl Vim {
|
||||||
self.select_register(text, window, cx);
|
self.select_register(text, window, cx);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(Operator::Jump { line }) => self.jump(text, line, window, cx),
|
Some(Operator::Jump { line }) => self.jump(text, line, true, window, cx),
|
||||||
_ => {
|
_ => {
|
||||||
if self.mode == Mode::Replace {
|
if self.mode == Mode::Replace {
|
||||||
self.multi_replace(text, window, cx)
|
self.multi_replace(text, window, cx)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue