mirror of https://github.com/helix-editor/helix
Compare commits
4 Commits
b6dcd1ab31
...
0d2d3dce23
Author | SHA1 | Date |
---|---|---|
|
0d2d3dce23 | |
|
567a8c77f5 | |
|
b92e1d15fe | |
|
ac83b7709a |
|
@ -126,9 +126,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use arc_swap::{access::Map, ArcSwap};
|
||||
use futures_util::Stream;
|
||||
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Range, Selection};
|
||||
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Range, RopeSlice, Selection};
|
||||
use helix_lsp::{
|
||||
lsp::{self, notification::Notification},
|
||||
util::lsp_range_to_range,
|
||||
|
@ -16,7 +16,12 @@ use helix_view::{
|
|||
tree::Layout,
|
||||
Align, Editor,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde_json::json;
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
};
|
||||
use tui::backend::Backend;
|
||||
|
||||
use crate::{
|
||||
|
@ -25,18 +30,23 @@ use crate::{
|
|||
config::Config,
|
||||
handlers,
|
||||
job::Jobs,
|
||||
keymap::Keymaps,
|
||||
keymap::{Keymaps, MappableCommand},
|
||||
ui::{self, overlay::overlaid},
|
||||
};
|
||||
|
||||
use log::{debug, error, info, warn};
|
||||
#[cfg(not(feature = "integration"))]
|
||||
use std::io::stdout;
|
||||
use std::{io::stdin, path::Path, sync::Arc};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
io::stdin,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
use anyhow::Context;
|
||||
use anyhow::Error;
|
||||
use anyhow::{anyhow, Error};
|
||||
|
||||
use crossterm::{event::Event as CrosstermEvent, tty::IsTty};
|
||||
#[cfg(not(windows))]
|
||||
|
@ -68,6 +78,8 @@ pub struct Application {
|
|||
signals: Signals,
|
||||
jobs: Jobs,
|
||||
lsp_progress: LspProgressMap,
|
||||
#[cfg(not(windows))]
|
||||
command_listener: CommandListener,
|
||||
}
|
||||
|
||||
#[cfg(feature = "integration")]
|
||||
|
@ -92,6 +104,284 @@ fn setup_integration_logging() {
|
|||
.apply();
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SocketResponse {
|
||||
Empty,
|
||||
Bytes(Vec<u8>),
|
||||
Json(serde_json::Value),
|
||||
}
|
||||
|
||||
impl<T: serde::Serialize> From<anyhow::Result<T>> for SocketResponse {
|
||||
fn from(value: anyhow::Result<T>) -> Self {
|
||||
SocketResponse::Bytes(serde_json::to_vec(&value.map_err(|s| s.to_string())).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for SocketResponse {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
Self::from(anyhow::Result::<()>::Err(value))
|
||||
}
|
||||
}
|
||||
|
||||
type SocketChannelMessageType = (usize, SocketResponse);
|
||||
|
||||
impl SocketResponse {
|
||||
async fn respond(self, index: usize, responder: &Sender<SocketChannelMessageType>) -> () {
|
||||
let _ = responder.send((index, self)).await;
|
||||
}
|
||||
|
||||
pub fn tag(&self) -> &[u8; 1] {
|
||||
match self {
|
||||
SocketResponse::Empty => b"0",
|
||||
SocketResponse::Bytes { .. } => b"b",
|
||||
SocketResponse::Json { .. } => b"j",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
match self {
|
||||
SocketResponse::Empty => Vec::new(),
|
||||
SocketResponse::Bytes(bytes) => bytes,
|
||||
SocketResponse::Json(json) => json.to_string().into_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn socket_read_buffer(editor: &mut Editor) -> SocketResponse {
|
||||
let (_view, doc) = current!(editor);
|
||||
SocketResponse::Bytes(doc.text().slice(..).bytes().collect())
|
||||
}
|
||||
|
||||
fn slice_as_cow_str<'a>(text: RopeSlice<'a>) -> Cow<'a, str> {
|
||||
match text.as_str() {
|
||||
Some(x) => Cow::Borrowed(x),
|
||||
None => Cow::Owned(text.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn socket_read_selections(editor: &mut Editor) -> SocketResponse {
|
||||
let (view, doc) = current!(editor);
|
||||
let text = doc.text().slice(..);
|
||||
|
||||
let selection = doc.selection(view.id);
|
||||
|
||||
fn sort_range(a: usize, b: usize) -> (usize, usize) {
|
||||
if a < b {
|
||||
(a, b)
|
||||
} else {
|
||||
(b, a)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct Lr {
|
||||
left: usize,
|
||||
right: usize,
|
||||
}
|
||||
|
||||
impl Lr {
|
||||
fn new(a: usize, b: usize) -> Self {
|
||||
let (left, right) = sort_range(a, b);
|
||||
Self { left, right }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct X<'a> {
|
||||
text: Cow<'a, str>,
|
||||
line: usize,
|
||||
bytes: Lr,
|
||||
// TODO char left/right?
|
||||
}
|
||||
|
||||
let lines: Vec<X> = selection
|
||||
.line_ranges(text)
|
||||
.flat_map(|(left, right)| {
|
||||
let mut res = Vec::new();
|
||||
res.push({
|
||||
let line = left;
|
||||
let byte_left = text.line_to_byte(line);
|
||||
let byte_right = text.line_to_byte(line + 1);
|
||||
X {
|
||||
text: slice_as_cow_str(text.byte_slice(byte_left..byte_right)),
|
||||
line,
|
||||
bytes: Lr::new(byte_left, byte_right),
|
||||
}
|
||||
});
|
||||
for line in (left + 1)..=right {
|
||||
let byte_left = res.last().unwrap().bytes.right;
|
||||
let byte_right = text.line_to_byte(line + 1);
|
||||
res.push(X {
|
||||
text: slice_as_cow_str(text.byte_slice(byte_left..byte_right)),
|
||||
line,
|
||||
bytes: Lr::new(byte_left, byte_right),
|
||||
});
|
||||
}
|
||||
res
|
||||
})
|
||||
.collect();
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct AnchorHead {
|
||||
anchor: usize,
|
||||
head: usize,
|
||||
#[serde(flatten)]
|
||||
lr: Lr,
|
||||
}
|
||||
|
||||
impl AnchorHead {
|
||||
fn new(anchor: usize, head: usize) -> Self {
|
||||
Self {
|
||||
anchor,
|
||||
head,
|
||||
lr: Lr::new(anchor, head),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct Fragment<'a> {
|
||||
text: Cow<'a, str>,
|
||||
chars: AnchorHead,
|
||||
bytes: AnchorHead,
|
||||
lines: Lr,
|
||||
}
|
||||
|
||||
let ranges: Vec<_> = selection
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let &Range { anchor, head, .. } = r;
|
||||
let chars = AnchorHead::new(anchor, head);
|
||||
let bytes = AnchorHead::new(text.char_to_byte(anchor), text.char_to_byte(head));
|
||||
let lines = Lr::new(
|
||||
text.char_to_line(chars.lr.left),
|
||||
text.char_to_line(chars.lr.left.max(chars.lr.right.saturating_sub(1))),
|
||||
);
|
||||
let text = r.fragment(text);
|
||||
Fragment {
|
||||
text,
|
||||
chars,
|
||||
bytes,
|
||||
lines,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
SocketResponse::Json(serde_json::json!({
|
||||
"primary": selection.primary_index(),
|
||||
"ranges": ranges,
|
||||
"lines": lines,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn execute_socket_commands(
|
||||
ctx: &mut super::commands::Context<'_>,
|
||||
commands: Vec<MappableCommand>,
|
||||
responder: &Sender<SocketChannelMessageType>,
|
||||
) {
|
||||
let mut selection_stash = vec![];
|
||||
let mut register_stash = vec![];
|
||||
for (index, command) in commands.into_iter().enumerate() {
|
||||
match command.name() {
|
||||
"socket-read-buffer" => {
|
||||
socket_read_buffer(ctx.editor)
|
||||
.respond(index, &responder)
|
||||
.await;
|
||||
}
|
||||
"socket-read-selections" => {
|
||||
socket_read_selections(ctx.editor)
|
||||
.respond(index, &responder)
|
||||
.await;
|
||||
}
|
||||
"socket-push-selection" => {
|
||||
let (view, doc) = current!(ctx.editor);
|
||||
let selection = doc.selection(view.id);
|
||||
selection_stash.push(selection.clone())
|
||||
}
|
||||
"socket-pop-selection" => {
|
||||
let res: SocketResponse = match selection_stash.pop() {
|
||||
Some(selection) => {
|
||||
let (view, doc) = current!(ctx.editor);
|
||||
doc.set_selection(view.id, selection);
|
||||
Ok(())
|
||||
}
|
||||
None => Err(anyhow!("No selection in stash")),
|
||||
}
|
||||
.into();
|
||||
res.respond(index, responder).await;
|
||||
}
|
||||
"socket-register" => {
|
||||
if let MappableCommand::Typable { mut args, .. } = command {
|
||||
let res: SocketResponse = match args.as_mut_slice() {
|
||||
[action, name, value] if action == "push" && name.len() == 1 => {
|
||||
let name = name.chars().next().unwrap();
|
||||
ctx.editor
|
||||
.registers
|
||||
.push(name, std::mem::take(value))
|
||||
.into()
|
||||
}
|
||||
[action, name, ..] if action == "write" && name.len() == 1 => {
|
||||
let name = name.chars().next().unwrap();
|
||||
ctx.editor
|
||||
.registers
|
||||
.write(name, args.drain(2..).collect())
|
||||
.into()
|
||||
}
|
||||
[action, name] if action == "read" && name.len() == 1 => {
|
||||
let name = name.chars().next().unwrap();
|
||||
Ok(match ctx.editor.registers.read(name, &ctx.editor) {
|
||||
None => vec![],
|
||||
Some(vals) => vals.collect::<Vec<_>>(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
[action] if action == "read" => {
|
||||
let name = ctx
|
||||
.register
|
||||
.unwrap_or_else(|| ctx.editor.config.load().default_yank_register);
|
||||
Ok(match ctx.editor.registers.read(name, &ctx.editor) {
|
||||
None => vec![],
|
||||
Some(vals) => vals.collect::<Vec<_>>(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
[action, name] if action == "!remove" && name.len() == 1 => {
|
||||
let name = name.chars().next().unwrap();
|
||||
Ok(ctx.editor.registers.remove(name)).into()
|
||||
}
|
||||
[action] if action == "!clear" => Ok(ctx.register.take()).into(),
|
||||
[action, name] if action == "!set" && name.len() == 1 => {
|
||||
let name = name.chars().next().unwrap();
|
||||
Ok(ctx.register.replace(name)).into()
|
||||
}
|
||||
[action, name] if action == "!push" && name.len() == 1 => {
|
||||
let name = name.chars().next().unwrap();
|
||||
let old = ctx.register.replace(name);
|
||||
register_stash.push(old);
|
||||
Ok(old).into()
|
||||
}
|
||||
[action] if action == "!pop" => match register_stash.pop() {
|
||||
Some(reg) => Ok(std::mem::replace(&mut ctx.register, reg)),
|
||||
None => Err(anyhow!("No register in stash")),
|
||||
}
|
||||
.into(),
|
||||
args => anyhow!("Invalid register command: {args:?}").into(),
|
||||
};
|
||||
res.respond(index, responder).await;
|
||||
} else {
|
||||
panic!("?")
|
||||
}
|
||||
}
|
||||
// "socket-set-selection" => {
|
||||
// ctx.editor.registers.push(name, value)
|
||||
// socket_read_selections(ctx.editor).respond(&responder).await;
|
||||
// }
|
||||
_ => {
|
||||
command.execute(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Application {
|
||||
pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Self, Error> {
|
||||
#[cfg(feature = "integration")]
|
||||
|
@ -151,7 +441,7 @@ impl Application {
|
|||
for (file, pos) in files_it {
|
||||
nr_of_files += 1;
|
||||
if file.is_dir() {
|
||||
return Err(anyhow::anyhow!(
|
||||
return Err(anyhow!(
|
||||
"expected a path to file, but found a directory: {file:?}. (to open a directory pass it as first argument)"
|
||||
));
|
||||
} else {
|
||||
|
@ -173,7 +463,7 @@ impl Application {
|
|||
nr_of_files -= 1;
|
||||
continue;
|
||||
}
|
||||
Err(err) => return Err(anyhow::anyhow!(err)),
|
||||
Err(err) => return Err(anyhow!(err)),
|
||||
// We can't open more than 1 buffer for 1 file, in this case we already have opened this file previously
|
||||
Ok(doc_id) if old_id == Some(doc_id) => {
|
||||
nr_of_files -= 1;
|
||||
|
@ -234,6 +524,13 @@ impl Application {
|
|||
])
|
||||
.context("build signal handler")?;
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let command_listener = {
|
||||
let pid = std::process::id();
|
||||
let file_path = std::env::temp_dir().join(format!("helix.{pid}.sock"));
|
||||
CommandListener::new(file_path)
|
||||
};
|
||||
|
||||
let app = Self {
|
||||
compositor,
|
||||
terminal,
|
||||
|
@ -242,6 +539,7 @@ impl Application {
|
|||
signals,
|
||||
jobs: Jobs::new(),
|
||||
lsp_progress: LspProgressMap::new(),
|
||||
command_listener,
|
||||
};
|
||||
|
||||
Ok(app)
|
||||
|
@ -307,6 +605,19 @@ impl Application {
|
|||
tokio::select! {
|
||||
biased;
|
||||
|
||||
Some((commands, responder)) = self.command_listener.rx.recv() => {
|
||||
let mut ctx = super::commands::Context {
|
||||
register: None,
|
||||
count: None,
|
||||
editor: &mut self.editor,
|
||||
callback: Vec::new(),
|
||||
on_next_key_callback: None,
|
||||
jobs: &mut self.jobs,
|
||||
};
|
||||
execute_socket_commands(&mut ctx, commands, &responder).await;
|
||||
self.render().await;
|
||||
}
|
||||
|
||||
Some(signal) = self.signals.next() => {
|
||||
if !self.handle_signals(signal).await {
|
||||
return false;
|
||||
|
@ -1174,3 +1485,177 @@ impl Application {
|
|||
errs
|
||||
}
|
||||
}
|
||||
|
||||
struct CommandListener {
|
||||
socket_path: PathBuf,
|
||||
rx: Receiver<(Vec<MappableCommand>, Sender<SocketChannelMessageType>)>,
|
||||
}
|
||||
|
||||
impl Drop for CommandListener {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = std::fs::remove_file(&self.socket_path) {
|
||||
log::error!(
|
||||
"Error removing command socket {}: {err}",
|
||||
self.socket_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandListener {
|
||||
pub fn new(socket_path: PathBuf) -> Self {
|
||||
let rx = spawn_command_listener(socket_path.clone());
|
||||
Self { rx, socket_path }
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_string(len: usize, buf: &mut std::io::Cursor<Vec<u8>>) -> anyhow::Result<String> {
|
||||
let mut res = Vec::with_capacity(len);
|
||||
AsyncReadExt::read_buf(buf, &mut res).await?;
|
||||
Ok(String::from_utf8(res)?)
|
||||
}
|
||||
|
||||
async fn read_u16_le_string(buf: &mut std::io::Cursor<Vec<u8>>) -> anyhow::Result<String> {
|
||||
read_string(buf.read_u16_le().await? as usize, buf).await
|
||||
}
|
||||
|
||||
async fn parse_mappable_command(
|
||||
buf: &mut std::io::Cursor<Vec<u8>>,
|
||||
) -> anyhow::Result<MappableCommand> {
|
||||
match buf.read_u8().await? {
|
||||
b':' => {
|
||||
let name = read_u16_le_string(buf).await?;
|
||||
let arg_count = buf.read_u16_le().await? as usize;
|
||||
let mut args = vec![];
|
||||
for _ in 0..arg_count {
|
||||
args.push(read_u16_le_string(buf).await?);
|
||||
}
|
||||
Ok(MappableCommand::Typable {
|
||||
name,
|
||||
args,
|
||||
doc: String::new(),
|
||||
})
|
||||
}
|
||||
b'@' => {
|
||||
let command = read_u16_le_string(buf).await?;
|
||||
helix_view::input::parse_macro(&command).map(|keys| MappableCommand::Macro {
|
||||
name: command,
|
||||
keys,
|
||||
})
|
||||
}
|
||||
b'!' => {
|
||||
static SORTED_STATIC_COMMANDS: Lazy<Vec<MappableCommand>> = Lazy::new(|| {
|
||||
let mut res = MappableCommand::STATIC_COMMAND_LIST.to_vec();
|
||||
res.sort_by(|a, b| a.name().cmp(&b.name()));
|
||||
res
|
||||
});
|
||||
|
||||
let name = read_u16_le_string(buf).await?;
|
||||
SORTED_STATIC_COMMANDS
|
||||
.binary_search_by_key(&name.as_str(), |a| a.name())
|
||||
.ok()
|
||||
.map(|x| SORTED_STATIC_COMMANDS[x].clone())
|
||||
.ok_or_else(|| anyhow!("No command named {name:?}"))
|
||||
}
|
||||
code => {
|
||||
anyhow::bail!("Invalid command kind: {}", code as char)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_command_listener(
|
||||
socket_path: PathBuf,
|
||||
) -> Receiver<(Vec<MappableCommand>, Sender<SocketChannelMessageType>)> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(100);
|
||||
tokio::spawn(async move {
|
||||
if let Ok(listener) = tokio::net::UnixListener::bind(&socket_path) {
|
||||
// 'accept_clients:
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((stream, addr)) => {
|
||||
info!("Got a connection at {addr:?}");
|
||||
let tx = tx.clone();
|
||||
let _handle: tokio::task::JoinHandle<anyhow::Result<()>> =
|
||||
tokio::spawn(async move {
|
||||
let (mut read, mut write) = stream.into_split();
|
||||
// 'read_commands:
|
||||
loop {
|
||||
let (response_tx, mut response_rx) =
|
||||
tokio::sync::mpsc::channel::<SocketChannelMessageType>(100);
|
||||
let result = (async {
|
||||
let total_message_length =
|
||||
read.read_u16_le().await? as usize;
|
||||
let commands = {
|
||||
let mut buf = vec![0u8; total_message_length];
|
||||
read.read_exact(&mut buf).await?;
|
||||
let mut cursor = std::io::Cursor::new(buf);
|
||||
let count = cursor.read_u16_le().await? as usize;
|
||||
let mut commands = vec![];
|
||||
for _ in 0..count {
|
||||
commands.push(
|
||||
parse_mappable_command(&mut cursor).await?,
|
||||
);
|
||||
}
|
||||
commands
|
||||
};
|
||||
info!("Got a command from {addr:?}: {commands:?}");
|
||||
let count = commands.len();
|
||||
tx.send((commands, response_tx)).await?;
|
||||
anyhow::Ok(count as u16)
|
||||
})
|
||||
.await;
|
||||
match result {
|
||||
Ok(count) => {
|
||||
write.write(b"o").await?;
|
||||
write.write_u16_le(count).await?;
|
||||
let mut responses =
|
||||
vec![SocketResponse::Empty; count as usize];
|
||||
while let Some((i, response)) = response_rx.recv().await
|
||||
{
|
||||
responses[i] = response;
|
||||
}
|
||||
for response in responses.into_iter() {
|
||||
write.write(response.tag()).await?;
|
||||
let bytes = response.into_bytes();
|
||||
debug!("Sending response of {} bytes", bytes.len());
|
||||
write.write_u32_le(bytes.len() as u32).await?;
|
||||
write.write(&bytes).await?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
write.write(b"e").await?;
|
||||
let errs = err.to_string();
|
||||
let sliced = truncate_str_to_u16(errs.as_str());
|
||||
write.write_u16_le(sliced.len() as u16).await?;
|
||||
write.write(sliced.as_bytes()).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to accept listener {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
fn truncate_str_to_u16(sliced: &str) -> &str {
|
||||
if sliced.len() > u16::MAX as usize {
|
||||
for i in 0..sliced.len() {
|
||||
if let Some((head, _tail)) = sliced.split_at_checked(sliced.len() - i) {
|
||||
return head;
|
||||
};
|
||||
}
|
||||
// This should be unreachable, but it would be odd to panic here.
|
||||
error!(
|
||||
"Somehow we sliced a string down to nothing...? Is none of it valid utf8?: {sliced:?}"
|
||||
);
|
||||
sliced
|
||||
} else {
|
||||
sliced
|
||||
}
|
||||
}
|
||||
|
|
|
@ -211,7 +211,7 @@ use helix_view::{align_view, Align};
|
|||
pub enum MappableCommand {
|
||||
Typable {
|
||||
name: String,
|
||||
args: String,
|
||||
args: Vec<String>,
|
||||
doc: String,
|
||||
},
|
||||
Static {
|
||||
|
@ -252,9 +252,12 @@ impl MappableCommand {
|
|||
jobs: cx.jobs,
|
||||
scroll: None,
|
||||
};
|
||||
if let Err(e) =
|
||||
typed::execute_command(&mut cx, command, args, PromptEvent::Validate)
|
||||
{
|
||||
if let Err(e) = typed::execute_command(
|
||||
&mut cx,
|
||||
command,
|
||||
&args.join(" "),
|
||||
PromptEvent::Validate,
|
||||
) {
|
||||
cx.editor.set_error(format!("{}", e));
|
||||
}
|
||||
} else {
|
||||
|
@ -282,11 +285,11 @@ impl MappableCommand {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
match &self {
|
||||
Self::Typable { name, .. } => name,
|
||||
pub fn name<'a>(&'a self) -> &'a str {
|
||||
match self {
|
||||
Self::Typable { name, .. } => name.as_str(),
|
||||
Self::Static { name, .. } => name,
|
||||
Self::Macro { name, .. } => name,
|
||||
Self::Macro { name, .. } => name.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -614,6 +617,37 @@ impl MappableCommand {
|
|||
);
|
||||
}
|
||||
|
||||
/* const _: () = {
|
||||
let mut i = 1usize;
|
||||
let arr = MappableCommand::STATIC_COMMAND_LIST;
|
||||
loop {
|
||||
if i >= arr.len() {
|
||||
break;
|
||||
}
|
||||
match (&arr[i - 1], &arr[i]) {
|
||||
(MappableCommand::Static { name: a, .. }, MappableCommand::Static { name: b, .. }) => {
|
||||
// assert!(a.len() <= b.len(), "Unsorted static command list");
|
||||
let n = if a.len() < b.len() { a.len() } else { b.len() };
|
||||
let mut j = 0;
|
||||
loop {
|
||||
if j >= n {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
a.as_bytes()[j] <= b.as_bytes()[j],
|
||||
"Unsorted static command list"
|
||||
);
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!("nonstatic in static command list")
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}; */
|
||||
|
||||
impl fmt::Debug for MappableCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
@ -658,7 +692,7 @@ impl std::str::FromStr for MappableCommand {
|
|||
MappableCommand::Typable {
|
||||
name: cmd.name.to_owned(),
|
||||
doc,
|
||||
args: args.to_string(),
|
||||
args: vec![args.to_string()],
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
|
||||
|
@ -3411,7 +3445,7 @@ pub fn command_palette(cx: &mut Context) {
|
|||
.iter()
|
||||
.map(|cmd| MappableCommand::Typable {
|
||||
name: cmd.name.to_owned(),
|
||||
args: String::new(),
|
||||
args: Vec::new(),
|
||||
doc: cmd.doc.to_owned(),
|
||||
}),
|
||||
);
|
||||
|
@ -6227,7 +6261,12 @@ fn shell_keep_pipe(cx: &mut Context) {
|
|||
|
||||
for (i, range) in selection.ranges().iter().enumerate() {
|
||||
let fragment = range.slice(text);
|
||||
if let Err(err) = shell_impl(shell, input, Some(fragment.into())) {
|
||||
if let Err(err) = shell_impl(
|
||||
shell,
|
||||
input,
|
||||
Some(fragment.into()),
|
||||
doc.path().map(|x| x.as_path()),
|
||||
) {
|
||||
log::debug!("Shell command failed: {}", err);
|
||||
} else {
|
||||
ranges.push(*range);
|
||||
|
@ -6248,14 +6287,22 @@ fn shell_keep_pipe(cx: &mut Context) {
|
|||
);
|
||||
}
|
||||
|
||||
fn shell_impl(shell: &[String], cmd: &str, input: Option<Rope>) -> anyhow::Result<Tendril> {
|
||||
tokio::task::block_in_place(|| helix_lsp::block_on(shell_impl_async(shell, cmd, input)))
|
||||
fn shell_impl(
|
||||
shell: &[String],
|
||||
cmd: &str,
|
||||
input: Option<Rope>,
|
||||
file_path: Option<&Path>,
|
||||
) -> anyhow::Result<Tendril> {
|
||||
tokio::task::block_in_place(|| {
|
||||
helix_lsp::block_on(shell_impl_async(shell, cmd, input, file_path))
|
||||
})
|
||||
}
|
||||
|
||||
async fn shell_impl_async(
|
||||
shell: &[String],
|
||||
cmd: &str,
|
||||
input: Option<Rope>,
|
||||
file_path: Option<&Path>,
|
||||
) -> anyhow::Result<Tendril> {
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command;
|
||||
|
@ -6274,6 +6321,19 @@ async fn shell_impl_async(
|
|||
process.stdin(Stdio::null());
|
||||
}
|
||||
|
||||
if let Some(file_path) = file_path {
|
||||
process.env("HELIX_FILE_PATH", file_path);
|
||||
}
|
||||
|
||||
{
|
||||
// TODO get this as an arg.
|
||||
let command_socket_path = {
|
||||
let pid = std::process::id();
|
||||
std::env::temp_dir().join(format!("helix.{pid}.sock"))
|
||||
};
|
||||
process.env("HELIX_SOCKET_PATH", command_socket_path);
|
||||
}
|
||||
|
||||
let mut process = match process.spawn() {
|
||||
Ok(process) => process,
|
||||
Err(e) => {
|
||||
|
@ -6341,7 +6401,12 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
|
|||
output.clone()
|
||||
} else {
|
||||
let input = range.slice(text);
|
||||
match shell_impl(shell, cmd, pipe.then(|| input.into())) {
|
||||
match shell_impl(
|
||||
shell,
|
||||
cmd,
|
||||
pipe.then(|| input.into()),
|
||||
doc.path().map(|x| x.as_path()),
|
||||
) {
|
||||
Ok(mut output) => {
|
||||
if !input.ends_with("\n") && output.ends_with('\n') {
|
||||
output.pop();
|
||||
|
|
|
@ -2393,13 +2393,20 @@ fn run_shell_command(
|
|||
let shell = cx.editor.config().shell.clone();
|
||||
let args = args.join(" ");
|
||||
|
||||
let (_view, doc) = current!(cx.editor);
|
||||
let current_file_path = doc.path().cloned();
|
||||
let callback = async move {
|
||||
let output = shell_impl_async(&shell, &args, None).await?;
|
||||
let path = current_file_path.as_ref().map(|x| x.as_path());
|
||||
let output = shell_impl_async(&shell, &args, None, path).await?;
|
||||
let call: job::Callback = Callback::EditorCompositor(Box::new(
|
||||
move |editor: &mut Editor, compositor: &mut Compositor| {
|
||||
if !output.is_empty() {
|
||||
let contents = ui::Markdown::new(
|
||||
format!("```sh\n{}\n```", output.trim_end()),
|
||||
if output.starts_with("```") {
|
||||
output.trim_end().to_string()
|
||||
} else {
|
||||
format!("```sh\n{}\n```", output.trim_end())
|
||||
},
|
||||
editor.syn_loader.clone(),
|
||||
);
|
||||
let popup = Popup::new("shell", contents).position(Some(
|
||||
|
@ -2407,7 +2414,7 @@ fn run_shell_command(
|
|||
));
|
||||
compositor.replace_or_push("shell", popup);
|
||||
}
|
||||
editor.set_status("Command run");
|
||||
// editor.set_status("Command run");
|
||||
},
|
||||
));
|
||||
Ok(call)
|
||||
|
|
Loading…
Reference in New Issue