Add option to open files at specific line/column (#1964)

Co-authored-by: JustForFun88 <alishergaliev88@gmail.com>
This commit is contained in:
Jakub Panek 2023-03-25 13:19:12 +01:00 committed by GitHub
parent 0ded46c988
commit c9e3544fbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 341 additions and 60 deletions

View File

@ -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

View File

@ -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,
}

View File

@ -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,

View File

@ -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,

View File

@ -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)

208
lapce-proxy/src/cli.rs Normal file
View File

@ -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")),
);
}
}

View File

@ -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"))?;

View File

@ -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);

View File

@ -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,
};