diff --git a/CHANGELOG.md b/CHANGELOG.md index 46849b44..db95f778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lapce-core/src/movement.rs b/lapce-core/src/movement.rs index ab9ec842..90df64ae 100644 --- a/lapce-core/src/movement.rs +++ b/lapce-core/src/movement.rs @@ -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, +} diff --git a/lapce-data/src/command.rs b/lapce-data/src/command.rs index 71a0eaee..6949ea31 100644 --- a/lapce-data/src/command.rs +++ b/lapce-data/src/command.rs @@ -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, diff --git a/lapce-data/src/data.rs b/lapce-data/src/data.rs index 47d59c1a..c83562b9 100644 --- a/lapce-data/src/data.rs +++ b/lapce-data/src/data.rs @@ -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, + paths: Vec, log_file: Option, ) -> 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 { 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 = 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::>(); let msg: CoreMessage = RpcMessage::Notification(CoreNotification::OpenPaths { window_tab_id: None, diff --git a/lapce-data/src/editor.rs b/lapce-data/src/editor.rs index 3de12b70..90aa8608 100644 --- a/lapce-data/src/editor.rs +++ b/lapce-data/src/editor.rs @@ -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) diff --git a/lapce-proxy/src/cli.rs b/lapce-proxy/src/cli.rs new file mode 100644 index 00000000..2a059c48 --- /dev/null +++ b/lapce-proxy/src/cli.rs @@ -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, +} + +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 { + 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::() { + if let Some(second_rhs) = splits.next() { + if let Ok(second_rhs_num) = second_rhs.parse::() { + 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( + mut iter: I, + buf: &mut Buf, + sep: T, +) -> fmt::Result +where + Buf: fmt::Write, + I: Iterator, + T: AsRef, +{ + 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, Vec) = 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")), + ); + } +} diff --git a/lapce-proxy/src/lib.rs b/lapce-proxy/src/lib.rs index eb7f537c..2666a735 100644 --- a/lapce-proxy/src/lib.rs +++ b/lapce-proxy/src/lib.rs @@ -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, + + /// 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, } 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"))?; diff --git a/lapce-ui/src/app.rs b/lapce-ui/src/app.rs index c1dbd09f..c2e374f8 100644 --- a/lapce-ui/src/app.rs +++ b/lapce-ui/src/app.rs @@ -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, + + /// 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, } pub fn build_window(data: &mut LapceWindowData) -> impl Widget { @@ -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 = 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); diff --git a/lapce-ui/src/search.rs b/lapce-ui/src/search.rs index a0e67bc9..cd0176f0 100644 --- a/lapce-ui/src/search.rs +++ b/lapce-ui/src/search.rs @@ -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, };