mirror of https://github.com/lapce/lapce.git
Add option to open files at specific line/column (#1964)
Co-authored-by: JustForFun88 <alishergaliev88@gmail.com>
This commit is contained in:
parent
0ded46c988
commit
c9e3544fbd
|
@ -10,6 +10,8 @@
|
|||
|
||||
### Features/Changes
|
||||
|
||||
- [#2407](https://github.com/lapce/lapce/pull/2407): Add option to open files at line/column
|
||||
|
||||
### Bug Fixes
|
||||
- [#2209](https://github.com/lapce/lapce/pull/2209): Fix macOS crashes
|
||||
- [#2228](https://github.com/lapce/lapce/pull/2228): Fix `.desktop` entry to properly associate with Lapce on Wayland
|
||||
|
|
|
@ -144,3 +144,10 @@ fn test_non_wrapping() {
|
|||
assert_eq!(2, Movement::Down.update_index(0, 5, 2, false));
|
||||
}
|
||||
}
|
||||
|
||||
/// UTF8 line and column-offset
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct LineCol {
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
EditCommand, FocusCommand, MotionModeCommand, MoveCommand,
|
||||
MultiSelectionCommand,
|
||||
},
|
||||
movement::LineCol,
|
||||
syntax::Syntax,
|
||||
};
|
||||
use lapce_rpc::{
|
||||
|
@ -39,7 +40,7 @@
|
|||
SplitContent,
|
||||
},
|
||||
document::BufferContent,
|
||||
editor::{EditorLocation, EditorPosition, Line, LineCol},
|
||||
editor::{EditorLocation, EditorPosition, Line},
|
||||
images,
|
||||
keypress::{KeyMap, KeyPress},
|
||||
markdown::Content,
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
register::Register,
|
||||
selection::Selection,
|
||||
};
|
||||
use lapce_proxy::cli::PathObject;
|
||||
use lapce_rpc::{
|
||||
buffer::BufferId,
|
||||
core::{CoreMessage, CoreNotification},
|
||||
|
@ -118,7 +119,7 @@ impl LapceData {
|
|||
/// previously written to the Lapce database.
|
||||
pub fn load(
|
||||
event_sink: ExtEventSink,
|
||||
paths: Vec<PathBuf>,
|
||||
paths: Vec<PathObject>,
|
||||
log_file: Option<PathBuf>,
|
||||
) -> Self {
|
||||
let _ = lapce_proxy::register_lapce_path();
|
||||
|
@ -133,8 +134,13 @@ pub fn load(
|
|||
.unwrap_or_else(|_| Self::default_panel_orders());
|
||||
let latest_release = Arc::new(None);
|
||||
|
||||
let dirs: Vec<&PathBuf> = paths.iter().filter(|p| p.is_dir()).collect();
|
||||
let files: Vec<&PathBuf> = paths.iter().filter(|p| p.is_file()).collect();
|
||||
let pwd = std::env::current_dir().unwrap_or_default();
|
||||
|
||||
// Split user input into known existing directors and
|
||||
// file paths that exist or not
|
||||
let (dirs, files): (Vec<&PathObject>, Vec<&PathObject>) =
|
||||
paths.iter().partition(|p| p.path.is_dir());
|
||||
|
||||
if !dirs.is_empty() {
|
||||
let (size, mut pos) = db
|
||||
.get_last_window_info()
|
||||
|
@ -162,7 +168,7 @@ pub fn load(
|
|||
active_tab: 0,
|
||||
workspaces: vec![LapceWorkspace {
|
||||
kind: workspace_type,
|
||||
path: Some(dir.to_path_buf()),
|
||||
path: Some(dir.path.to_owned()),
|
||||
last_open: 0,
|
||||
}],
|
||||
},
|
||||
|
@ -228,13 +234,46 @@ pub fn load(
|
|||
windows.insert(window.window_id, window);
|
||||
}
|
||||
|
||||
if let Some((window_id, _)) = windows.iter().next() {
|
||||
if let Some((window_id, data)) = windows.iter().next() {
|
||||
for file in files {
|
||||
let _ = event_sink.submit_command(
|
||||
LAPCE_UI_COMMAND,
|
||||
LapceUICommand::OpenFile(file.to_path_buf(), false),
|
||||
Target::Window(*window_id),
|
||||
);
|
||||
let file_path = match file.path.canonicalize() {
|
||||
Ok(v) => v,
|
||||
_ => pwd.join(&file.path),
|
||||
};
|
||||
for (widget_id, _) in &data.tabs {
|
||||
if let Some(pos) = file.linecol {
|
||||
// jump to line and column
|
||||
if let Err(err) = event_sink.submit_command(
|
||||
LAPCE_UI_COMMAND,
|
||||
LapceUICommand::JumpToLineColLocation(
|
||||
None,
|
||||
EditorLocation {
|
||||
path: file_path.clone(),
|
||||
position: Some(pos), // line info is included in column variable
|
||||
scroll_offset: None,
|
||||
history: None,
|
||||
},
|
||||
false,
|
||||
),
|
||||
Target::Widget(*widget_id),
|
||||
) {
|
||||
log::warn!("Failed to lauch: {err}");
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
} else {
|
||||
// open the file
|
||||
if let Err(err) = event_sink.submit_command(
|
||||
LAPCE_UI_COMMAND,
|
||||
LapceUICommand::OpenFile(file_path.clone(), false),
|
||||
Target::Window(*window_id),
|
||||
) {
|
||||
log::warn!("Failed to lauch: {err}");
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -355,13 +394,39 @@ fn listen_local_socket(event_sink: ExtEventSink) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn try_open_in_existing_process(paths: &[PathBuf]) -> Result<()> {
|
||||
pub fn get_socket() -> Result<interprocess::local_socket::LocalSocketStream> {
|
||||
let local_socket = Directory::local_socket()
|
||||
.ok_or_else(|| anyhow!("can't get local socket folder"))?;
|
||||
let mut socket =
|
||||
let socket =
|
||||
interprocess::local_socket::LocalSocketStream::connect(local_socket)?;
|
||||
let folders: Vec<_> = paths.iter().filter(|p| p.is_dir()).cloned().collect();
|
||||
let files: Vec<_> = paths.iter().filter(|p| p.is_file()).cloned().collect();
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
pub fn try_open_in_existing_process(
|
||||
mut socket: interprocess::local_socket::LocalSocketStream,
|
||||
paths: &[PathObject],
|
||||
) -> Result<()> {
|
||||
let folders: Vec<PathBuf> = paths
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
if p.path.is_dir() {
|
||||
Some(p.path.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let files = paths
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
if p.path.is_file() {
|
||||
Some(p.path.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<PathBuf>>();
|
||||
let msg: CoreMessage =
|
||||
RpcMessage::Notification(CoreNotification::OpenPaths {
|
||||
window_tab_id: None,
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
command::{EditCommand, FocusCommand, MotionModeCommand, MultiSelectionCommand},
|
||||
editor::EditType,
|
||||
mode::{Mode, MotionMode},
|
||||
movement::LineCol,
|
||||
selection::{InsertDrift, Selection},
|
||||
syntax::edit::SyntaxEdit,
|
||||
};
|
||||
|
@ -134,13 +135,6 @@ fn init_buffer_content_cmd(
|
|||
}
|
||||
}
|
||||
|
||||
/// UTF8 line and column-offset
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LineCol {
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl EditorPosition for LineCol {
|
||||
fn to_utf8_offset(&self, buffer: &Buffer) -> usize {
|
||||
buffer.offset_of_line_col(self.line, self.column)
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use core::fmt;
|
||||
use lapce_core::{directory::Directory, movement::LineCol};
|
||||
use lapce_rpc::{
|
||||
proxy::{ProxyMessage, ProxyNotification},
|
||||
RpcMessage,
|
||||
};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Component, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct PathObject {
|
||||
pub path: PathBuf,
|
||||
pub linecol: Option<LineCol>,
|
||||
}
|
||||
|
||||
impl PathObject {
|
||||
pub fn new(path: PathBuf, line: usize, column: usize) -> PathObject {
|
||||
PathObject {
|
||||
path,
|
||||
linecol: Some(LineCol { line, column }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_path(path: PathBuf) -> PathObject {
|
||||
PathObject {
|
||||
path,
|
||||
linecol: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_file_line_column(path: &str) -> Result<PathObject, Error> {
|
||||
let path = PathBuf::from(path);
|
||||
// Bail out quickly on existing path
|
||||
if let Ok(meta) = fs::metadata(&path) {
|
||||
if meta.is_file() || meta.is_dir() {
|
||||
return Ok(PathObject::from_path(path));
|
||||
}
|
||||
};
|
||||
let components = path.components();
|
||||
// Verify that last component is what could be a filename
|
||||
// otherwise bail out since it's an actual path
|
||||
match components.last() {
|
||||
Some(Component::Normal(_)) => {}
|
||||
_ => {
|
||||
return Ok(PathObject::from_path(path));
|
||||
}
|
||||
};
|
||||
if let Some(str) = path.to_str() {
|
||||
let mut splits = str.rsplit(':');
|
||||
if let Some(first_rhs) = splits.next() {
|
||||
if let Ok(first_rhs_num) = first_rhs.parse::<usize>() {
|
||||
if let Some(second_rhs) = splits.next() {
|
||||
if let Ok(second_rhs_num) = second_rhs.parse::<usize>() {
|
||||
let mut str = String::new();
|
||||
write_text_with_sep_to(splits.rev(), &mut str, ":")?;
|
||||
// NOTE: The last element is ":", and its ok, because even if we use &[&str] we need
|
||||
// to check the length of a slice on each iteration
|
||||
let left_path = PathBuf::from(&str[..str.len() - 1]);
|
||||
if left_path.is_file() {
|
||||
return Ok(PathObject::new(
|
||||
left_path,
|
||||
second_rhs_num,
|
||||
first_rhs_num,
|
||||
));
|
||||
}
|
||||
|
||||
str.push_str(second_rhs);
|
||||
let left_path = PathBuf::from(&str);
|
||||
if left_path.is_file() {
|
||||
return Ok(PathObject::new(left_path, first_rhs_num, 1));
|
||||
} else if path.is_file() {
|
||||
return Ok(PathObject::from_path(path));
|
||||
}
|
||||
} else {
|
||||
let mut str = String::new();
|
||||
write_text_with_sep_to(splits.rev(), &mut str, ":")?;
|
||||
// Last char of `str` is ":", so we neen to push only `second_rhs`
|
||||
str.push_str(second_rhs);
|
||||
|
||||
return Ok(PathObject::new(
|
||||
PathBuf::from(str),
|
||||
first_rhs_num,
|
||||
1,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Ok(PathObject::from_path(path));
|
||||
}
|
||||
} else {
|
||||
return Ok(PathObject::from_path(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(PathObject::from_path(path))
|
||||
}
|
||||
|
||||
// FIXME: Unfortunately the last element will be ":", we need to think about how to handle
|
||||
// it without having to allocate an unnecessary vector
|
||||
fn write_text_with_sep_to<I, T, Buf>(
|
||||
mut iter: I,
|
||||
buf: &mut Buf,
|
||||
sep: T,
|
||||
) -> fmt::Result
|
||||
where
|
||||
Buf: fmt::Write,
|
||||
I: Iterator<Item = T>,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
if let Some(str) = iter.next() {
|
||||
buf.write_str(str.as_ref())?;
|
||||
buf.write_str(sep.as_ref())?;
|
||||
// call ourselves recursively
|
||||
write_text_with_sep_to(iter, buf, sep)?
|
||||
}
|
||||
fmt::Result::Ok(())
|
||||
}
|
||||
|
||||
pub fn try_open_in_existing_process(paths: &[PathObject]) -> Result<()> {
|
||||
let local_socket = Directory::local_socket()
|
||||
.ok_or_else(|| anyhow!("can't get local socket folder"))?;
|
||||
let mut socket =
|
||||
interprocess::local_socket::LocalSocketStream::connect(local_socket)?;
|
||||
|
||||
// Split user input into known existing directors and
|
||||
// file paths that exist or not
|
||||
let (folders, files): (Vec<PathBuf>, Vec<PathBuf>) = paths
|
||||
.iter()
|
||||
.map(|p| p.path.to_owned())
|
||||
.partition(|p| p.is_dir());
|
||||
|
||||
let msg: ProxyMessage =
|
||||
RpcMessage::Notification(ProxyNotification::OpenPaths { folders, files });
|
||||
lapce_rpc::stdio::write_msg(&mut socket, msg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::cli::PathObject;
|
||||
|
||||
use super::parse_file_line_column;
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn test_absolute_path() {
|
||||
assert_eq!(
|
||||
parse_file_line_column("C:\\Cargo.toml:55").unwrap(),
|
||||
PathObject::new(PathBuf::from("C:\\Cargo.toml"), 55, 1),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn test_relative_path() {
|
||||
assert_eq!(
|
||||
parse_file_line_column(".\\..\\Cargo.toml:55").unwrap(),
|
||||
PathObject::new(PathBuf::from(".\\..\\Cargo.toml"), 55, 1),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_absolute_path() {
|
||||
assert_eq!(
|
||||
parse_file_line_column("/tmp/Cargo.toml:55").unwrap(),
|
||||
PathObject::new(PathBuf::from("/tmp/Cargo.toml"), 55, 1),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_relative_path() {
|
||||
assert_eq!(
|
||||
parse_file_line_column("./lapce-core/../Cargo.toml").unwrap(),
|
||||
PathObject::from_path(PathBuf::from("./lapce-core/../Cargo.toml")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_path_with_line() {
|
||||
assert_eq!(
|
||||
parse_file_line_column("Cargo.toml:55").unwrap(),
|
||||
PathObject::new(PathBuf::from("Cargo.toml"), 55, 1),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_path_with_linecol() {
|
||||
assert_eq!(
|
||||
parse_file_line_column("Cargo.toml:55:3").unwrap(),
|
||||
PathObject::new(PathBuf::from("Cargo.toml"), 55, 3),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_path_with_none() {
|
||||
assert_eq!(
|
||||
parse_file_line_column("Cargo.toml:12:623:352").unwrap(),
|
||||
PathObject::from_path(PathBuf::from("Cargo.toml:12:623:352")),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
#![allow(clippy::manual_clamp)]
|
||||
|
||||
pub mod buffer;
|
||||
pub mod cli;
|
||||
pub mod dispatch;
|
||||
pub mod plugin;
|
||||
pub mod terminal;
|
||||
|
@ -9,11 +10,13 @@
|
|||
use std::{
|
||||
io::{stdin, stdout, BufReader},
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
sync::Arc,
|
||||
thread,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use clap::Parser;
|
||||
use dispatch::Dispatcher;
|
||||
use lapce_core::{directory::Directory, meta};
|
||||
|
@ -28,22 +31,25 @@
|
|||
#[clap(name = "Lapce")]
|
||||
#[clap(version=*meta::VERSION)]
|
||||
struct Cli {
|
||||
#[clap(short, long, action)]
|
||||
#[clap(short, long, action, hide = true)]
|
||||
proxy: bool,
|
||||
paths: Vec<PathBuf>,
|
||||
|
||||
/// Paths to file(s) and/or folder(s) to open.
|
||||
/// When path is a file (that exists or not),
|
||||
/// it accepts `path:line:column` syntax
|
||||
/// to specify line and column at which it should open the file
|
||||
#[clap(value_parser = cli::parse_file_line_column)]
|
||||
#[clap(value_hint = clap::ValueHint::AnyPath)]
|
||||
paths: Vec<cli::PathObject>,
|
||||
}
|
||||
|
||||
pub fn mainloop() {
|
||||
let cli = Cli::parse();
|
||||
if !cli.proxy {
|
||||
let pwd = std::env::current_dir().unwrap_or_default();
|
||||
let paths: Vec<_> = cli
|
||||
.paths
|
||||
.iter()
|
||||
.map(|p| pwd.join(p).canonicalize().unwrap_or_default())
|
||||
.collect();
|
||||
let _ = try_open_in_existing_process(&paths);
|
||||
return;
|
||||
if let Err(e) = cli::try_open_in_existing_process(&cli.paths) {
|
||||
log::error!("failed to open path(s): {e}");
|
||||
};
|
||||
exit(1);
|
||||
}
|
||||
let core_rpc = CoreRpcHandler::new();
|
||||
let proxy_rpc = ProxyRpcHandler::new();
|
||||
|
@ -128,19 +134,6 @@ pub fn register_lapce_path() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn try_open_in_existing_process(paths: &[PathBuf]) -> Result<()> {
|
||||
let local_socket = Directory::local_socket()
|
||||
.ok_or_else(|| anyhow!("can't get local socket folder"))?;
|
||||
let mut socket =
|
||||
interprocess::local_socket::LocalSocketStream::connect(local_socket)?;
|
||||
let folders: Vec<_> = paths.iter().filter(|p| p.is_dir()).cloned().collect();
|
||||
let files: Vec<_> = paths.iter().filter(|p| p.is_file()).cloned().collect();
|
||||
let msg: ProxyMessage =
|
||||
RpcMessage::Notification(ProxyNotification::OpenPaths { folders, files });
|
||||
lapce_rpc::stdio::write_msg(&mut socket, msg)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn listen_local_socket(proxy_rpc: ProxyRpcHandler) -> Result<()> {
|
||||
let local_socket = Directory::local_socket()
|
||||
.ok_or_else(|| anyhow!("can't get local socket folder"))?;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::{path::PathBuf, process::Stdio, sync::Arc};
|
||||
use std::{process::Stdio, sync::Arc};
|
||||
|
||||
use clap::Parser;
|
||||
use druid::{
|
||||
|
@ -37,10 +37,18 @@ struct Cli {
|
|||
/// Launch new window even if Lapce is already running
|
||||
#[clap(short, long, action)]
|
||||
new: bool,
|
||||
|
||||
/// Don't return instantly when opened in terminal
|
||||
#[clap(short, long, action)]
|
||||
wait: bool,
|
||||
paths: Vec<PathBuf>,
|
||||
|
||||
/// Paths to file(s) and/or folder(s) to open.
|
||||
/// When path is a file (that exists or not),
|
||||
/// it accepts `path:line:column` syntax
|
||||
/// to specify line and column at which it should open the file
|
||||
#[clap(value_parser = lapce_proxy::cli::parse_file_line_column)]
|
||||
#[clap(value_hint = clap::ValueHint::AnyPath)]
|
||||
paths: Vec<lapce_proxy::cli::PathObject>,
|
||||
}
|
||||
|
||||
pub fn build_window(data: &mut LapceWindowData) -> impl Widget<LapceData> {
|
||||
|
@ -63,24 +71,26 @@ pub fn launch() {
|
|||
let mut cmd = std::process::Command::new(&args[0]);
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||
if let Err(why) = cmd
|
||||
if let Err(e) = cmd
|
||||
.args(&args[1..])
|
||||
.stderr(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("Failed to launch lapce: {why}");
|
||||
eprintln!("Failed to launch lapce: {e}");
|
||||
};
|
||||
return;
|
||||
}
|
||||
let pwd = std::env::current_dir().unwrap_or_default();
|
||||
let paths: Vec<PathBuf> = cli
|
||||
.paths
|
||||
.iter()
|
||||
.map(|p| pwd.join(p).canonicalize().unwrap_or_default())
|
||||
.collect();
|
||||
if !cli.new && LapceData::try_open_in_existing_process(&paths).is_ok() {
|
||||
return;
|
||||
|
||||
if !cli.new {
|
||||
if let Ok(socket) = LapceData::get_socket() {
|
||||
if let Err(e) =
|
||||
LapceData::try_open_in_existing_process(socket, &cli.paths)
|
||||
{
|
||||
log::error!("failed to open path(s): {e}");
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
|
@ -136,7 +146,8 @@ pub fn launch() {
|
|||
.install_panic_hook();
|
||||
|
||||
let mut launcher = AppLauncher::new().delegate(LapceAppDelegate::new());
|
||||
let mut data = LapceData::load(launcher.get_external_handle(), paths, log_file);
|
||||
let mut data =
|
||||
LapceData::load(launcher.get_external_handle(), cli.paths, log_file);
|
||||
|
||||
for (_window_id, window_data) in data.windows.iter_mut() {
|
||||
let root = build_window(window_data);
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
RenderContext, Size, Target, UpdateCtx, Widget, WidgetExt, WidgetId, WidgetPod,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use lapce_core::command::FocusCommand;
|
||||
use lapce_core::{command::FocusCommand, movement::LineCol};
|
||||
use lapce_data::{
|
||||
command::{
|
||||
CommandKind, LapceCommand, LapceUICommand, LAPCE_COMMAND, LAPCE_UI_COMMAND,
|
||||
},
|
||||
config::{LapceIcons, LapceTheme},
|
||||
data::LapceTabData,
|
||||
editor::{EditorLocation, LineCol},
|
||||
editor::EditorLocation,
|
||||
panel::PanelKind,
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue