Compare commits

...

5 Commits

Author SHA1 Message Date
Ashkan Kiani b6dcd1ab31
Merge 567a8c77f5 into 4281228da3 2025-07-24 20:22:33 +02:00
Valtteri Koskivuori 4281228da3
fix(queries): Fix filesystem permissions for snakemake (#14061) 2025-07-24 13:09:40 -04:00
Ashkan Kiani 567a8c77f5 Add socket listener and commands 2025-06-19 20:19:25 +08:00
Ashkan Kiani b92e1d15fe Change shell command output formatting
Don't prepend markdown code block if the shell
command has one already.

This allows it to render however it wants instead
of strictly sticking to [sh].

There is already the workaround of echoing [```]
to close the existing block first, but that produces
an extra blank line at the top that is ugly.
2025-06-19 20:19:25 +08:00
Ashkan Kiani ac83b7709a Add exporting HELIX_FILE_PATH to shell commands 2025-06-18 21:07:27 +08:00
10 changed files with 582 additions and 25 deletions

4
Cargo.lock generated
View File

@ -126,9 +126,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]] [[package]]
name = "byteorder" name = "byteorder"

View File

@ -1,6 +1,6 @@
use arc_swap::{access::Map, ArcSwap}; use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream; 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::{ use helix_lsp::{
lsp::{self, notification::Notification}, lsp::{self, notification::Notification},
util::lsp_range_to_range, util::lsp_range_to_range,
@ -16,7 +16,12 @@ use helix_view::{
tree::Layout, tree::Layout,
Align, Editor, Align, Editor,
}; };
use once_cell::sync::Lazy;
use serde_json::json; use serde_json::json;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
sync::mpsc::{Receiver, Sender},
};
use tui::backend::Backend; use tui::backend::Backend;
use crate::{ use crate::{
@ -25,18 +30,23 @@ use crate::{
config::Config, config::Config,
handlers, handlers,
job::Jobs, job::Jobs,
keymap::Keymaps, keymap::{Keymaps, MappableCommand},
ui::{self, overlay::overlaid}, ui::{self, overlay::overlaid},
}; };
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
#[cfg(not(feature = "integration"))] #[cfg(not(feature = "integration"))]
use std::io::stdout; 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))] #[cfg(not(windows))]
use anyhow::Context; use anyhow::Context;
use anyhow::Error; use anyhow::{anyhow, Error};
use crossterm::{event::Event as CrosstermEvent, tty::IsTty}; use crossterm::{event::Event as CrosstermEvent, tty::IsTty};
#[cfg(not(windows))] #[cfg(not(windows))]
@ -68,6 +78,8 @@ pub struct Application {
signals: Signals, signals: Signals,
jobs: Jobs, jobs: Jobs,
lsp_progress: LspProgressMap, lsp_progress: LspProgressMap,
#[cfg(not(windows))]
command_listener: CommandListener,
} }
#[cfg(feature = "integration")] #[cfg(feature = "integration")]
@ -92,6 +104,284 @@ fn setup_integration_logging() {
.apply(); .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 { impl Application {
pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Self, Error> { pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Self, Error> {
#[cfg(feature = "integration")] #[cfg(feature = "integration")]
@ -151,7 +441,7 @@ impl Application {
for (file, pos) in files_it { for (file, pos) in files_it {
nr_of_files += 1; nr_of_files += 1;
if file.is_dir() { 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)" "expected a path to file, but found a directory: {file:?}. (to open a directory pass it as first argument)"
)); ));
} else { } else {
@ -173,7 +463,7 @@ impl Application {
nr_of_files -= 1; nr_of_files -= 1;
continue; 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 // 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) => { Ok(doc_id) if old_id == Some(doc_id) => {
nr_of_files -= 1; nr_of_files -= 1;
@ -234,6 +524,13 @@ impl Application {
]) ])
.context("build signal handler")?; .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 { let app = Self {
compositor, compositor,
terminal, terminal,
@ -242,6 +539,7 @@ impl Application {
signals, signals,
jobs: Jobs::new(), jobs: Jobs::new(),
lsp_progress: LspProgressMap::new(), lsp_progress: LspProgressMap::new(),
command_listener,
}; };
Ok(app) Ok(app)
@ -307,6 +605,19 @@ impl Application {
tokio::select! { tokio::select! {
biased; 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() => { Some(signal) = self.signals.next() => {
if !self.handle_signals(signal).await { if !self.handle_signals(signal).await {
return false; return false;
@ -1174,3 +1485,177 @@ impl Application {
errs 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
}
}

View File

@ -211,7 +211,7 @@ use helix_view::{align_view, Align};
pub enum MappableCommand { pub enum MappableCommand {
Typable { Typable {
name: String, name: String,
args: String, args: Vec<String>,
doc: String, doc: String,
}, },
Static { Static {
@ -252,9 +252,12 @@ impl MappableCommand {
jobs: cx.jobs, jobs: cx.jobs,
scroll: None, scroll: None,
}; };
if let Err(e) = if let Err(e) = typed::execute_command(
typed::execute_command(&mut cx, command, args, PromptEvent::Validate) &mut cx,
{ command,
&args.join(" "),
PromptEvent::Validate,
) {
cx.editor.set_error(format!("{}", e)); cx.editor.set_error(format!("{}", e));
} }
} else { } else {
@ -282,11 +285,11 @@ impl MappableCommand {
} }
} }
pub fn name(&self) -> &str { pub fn name<'a>(&'a self) -> &'a str {
match &self { match self {
Self::Typable { name, .. } => name, Self::Typable { name, .. } => name.as_str(),
Self::Static { name, .. } => name, 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 { impl fmt::Debug for MappableCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -658,7 +692,7 @@ impl std::str::FromStr for MappableCommand {
MappableCommand::Typable { MappableCommand::Typable {
name: cmd.name.to_owned(), name: cmd.name.to_owned(),
doc, doc,
args: args.to_string(), args: vec![args.to_string()],
} }
}) })
.ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
@ -3411,7 +3445,7 @@ pub fn command_palette(cx: &mut Context) {
.iter() .iter()
.map(|cmd| MappableCommand::Typable { .map(|cmd| MappableCommand::Typable {
name: cmd.name.to_owned(), name: cmd.name.to_owned(),
args: String::new(), args: Vec::new(),
doc: cmd.doc.to_owned(), doc: cmd.doc.to_owned(),
}), }),
); );
@ -6227,7 +6261,12 @@ fn shell_keep_pipe(cx: &mut Context) {
for (i, range) in selection.ranges().iter().enumerate() { for (i, range) in selection.ranges().iter().enumerate() {
let fragment = range.slice(text); 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); log::debug!("Shell command failed: {}", err);
} else { } else {
ranges.push(*range); 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> { fn shell_impl(
tokio::task::block_in_place(|| helix_lsp::block_on(shell_impl_async(shell, cmd, input))) 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( async fn shell_impl_async(
shell: &[String], shell: &[String],
cmd: &str, cmd: &str,
input: Option<Rope>, input: Option<Rope>,
file_path: Option<&Path>,
) -> anyhow::Result<Tendril> { ) -> anyhow::Result<Tendril> {
use std::process::Stdio; use std::process::Stdio;
use tokio::process::Command; use tokio::process::Command;
@ -6274,6 +6321,19 @@ async fn shell_impl_async(
process.stdin(Stdio::null()); 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() { let mut process = match process.spawn() {
Ok(process) => process, Ok(process) => process,
Err(e) => { Err(e) => {
@ -6341,7 +6401,12 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
output.clone() output.clone()
} else { } else {
let input = range.slice(text); 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) => { Ok(mut output) => {
if !input.ends_with("\n") && output.ends_with('\n') { if !input.ends_with("\n") && output.ends_with('\n') {
output.pop(); output.pop();

View File

@ -2393,13 +2393,20 @@ fn run_shell_command(
let shell = cx.editor.config().shell.clone(); let shell = cx.editor.config().shell.clone();
let args = args.join(" "); let args = args.join(" ");
let (_view, doc) = current!(cx.editor);
let current_file_path = doc.path().cloned();
let callback = async move { 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( let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| { move |editor: &mut Editor, compositor: &mut Compositor| {
if !output.is_empty() { if !output.is_empty() {
let contents = ui::Markdown::new( 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(), editor.syn_loader.clone(),
); );
let popup = Popup::new("shell", contents).position(Some( let popup = Popup::new("shell", contents).position(Some(
@ -2407,7 +2414,7 @@ fn run_shell_command(
)); ));
compositor.replace_or_push("shell", popup); compositor.replace_or_push("shell", popup);
} }
editor.set_status("Command run"); // editor.set_status("Command run");
}, },
)); ));
Ok(call) Ok(call)

0
runtime/queries/snakemake/LICENSE 100755 → 100644
View File

View File

View File

View File

View File

View File