diff --git a/crates/nu-plugin/src/plugin/context.rs b/crates/nu-plugin/src/plugin/context.rs index b366b37605..f590392f8b 100644 --- a/crates/nu-plugin/src/plugin/context.rs +++ b/crates/nu-plugin/src/plugin/context.rs @@ -3,7 +3,7 @@ use nu_engine::{get_eval_block_with_early_return, get_full_help}; use nu_protocol::{ ast::Call, engine::{Closure, EngineState, Redirection, Stack}, - Config, IntoSpanned, IoStream, PipelineData, PluginIdentity, ShellError, Spanned, Value, + Config, IntoSpanned, IoStream, PipelineData, PluginIdentity, ShellError, Span, Spanned, Value, }; use std::{ borrow::Cow, @@ -32,6 +32,8 @@ pub trait PluginExecutionContext: Send + Sync { fn add_env_var(&mut self, name: String, value: Value) -> Result<(), ShellError>; /// Get help for the current command fn get_help(&self) -> Result, ShellError>; + /// Get the contents of a [`Span`] + fn get_span_contents(&self, span: Span) -> Result>, ShellError>; /// Evaluate a closure passed to the plugin fn eval_closure( &self, @@ -150,6 +152,14 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> { .into_spanned(self.call.head)) } + fn get_span_contents(&self, span: Span) -> Result>, ShellError> { + Ok(self + .engine_state + .get_span_contents(span) + .to_vec() + .into_spanned(self.call.head)) + } + fn eval_closure( &self, closure: Spanned, @@ -271,6 +281,12 @@ impl PluginExecutionContext for PluginExecutionBogusContext { }) } + fn get_span_contents(&self, _span: Span) -> Result>, ShellError> { + Err(ShellError::NushellFailed { + msg: "get_span_contents not implemented on bogus".into(), + }) + } + fn eval_closure( &self, _closure: Spanned, diff --git a/crates/nu-plugin/src/plugin/interface/engine.rs b/crates/nu-plugin/src/plugin/interface/engine.rs index 90f3e051c9..b49f145bd6 100644 --- a/crates/nu-plugin/src/plugin/interface/engine.rs +++ b/crates/nu-plugin/src/plugin/interface/engine.rs @@ -11,7 +11,7 @@ use crate::protocol::{ }; use nu_protocol::{ engine::Closure, Config, IntoInterruptiblePipelineData, LabeledError, ListStream, PipelineData, - PluginSignature, ShellError, Spanned, Value, + PluginSignature, ShellError, Span, Spanned, Value, }; use std::{ collections::{btree_map, BTreeMap, HashMap}, @@ -646,6 +646,22 @@ impl EngineInterface { } } + /// Get the contents of a [`Span`] from the engine. + /// + /// This method returns `Vec` as it's possible for the matched span to not be a valid UTF-8 + /// string, perhaps because it sliced through the middle of a UTF-8 byte sequence, as the + /// offsets are byte-indexed. Use [`String::from_utf8_lossy()`] for display if necessary. + pub fn get_span_contents(&self, span: Span) -> Result, ShellError> { + match self.engine_call(EngineCall::GetSpanContents(span))? { + EngineCallResponse::PipelineData(PipelineData::Value(Value::Binary { val, .. }, _)) => { + Ok(val) + } + _ => Err(ShellError::PluginFailedToDecode { + msg: "Received unexpected response type for EngineCall::GetSpanContents".into(), + }), + } + } + /// Ask the engine to evaluate a closure. Input to the closure is passed as a stream, and the /// output is available as a stream. /// diff --git a/crates/nu-plugin/src/plugin/interface/engine/tests.rs b/crates/nu-plugin/src/plugin/interface/engine/tests.rs index 22b721cd97..8d29a6d8e6 100644 --- a/crates/nu-plugin/src/plugin/interface/engine/tests.rs +++ b/crates/nu-plugin/src/plugin/interface/engine/tests.rs @@ -978,6 +978,24 @@ fn interface_get_help() -> Result<(), ShellError> { Ok(()) } +#[test] +fn interface_get_span_contents() -> Result<(), ShellError> { + let test = TestCase::new(); + let manager = test.engine(); + let interface = manager.interface_for_context(0); + + start_fake_plugin_call_responder(manager, 1, move |_| { + EngineCallResponse::value(Value::test_binary(b"test string")) + }); + + let contents = interface.get_span_contents(Span::test_data())?; + + assert_eq!(b"test string", &contents[..]); + + assert!(test.has_unconsumed_write()); + Ok(()) +} + #[test] fn interface_eval_closure_with_stream() -> Result<(), ShellError> { let test = TestCase::new(); diff --git a/crates/nu-plugin/src/plugin/interface/plugin.rs b/crates/nu-plugin/src/plugin/interface/plugin.rs index 3e234d3a76..e8afd3a38c 100644 --- a/crates/nu-plugin/src/plugin/interface/plugin.rs +++ b/crates/nu-plugin/src/plugin/interface/plugin.rs @@ -1180,6 +1180,13 @@ pub(crate) fn handle_engine_call( help.item, help.span, ))) } + EngineCall::GetSpanContents(span) => { + let contents = context.get_span_contents(span)?; + Ok(EngineCallResponse::value(Value::binary( + contents.item, + contents.span, + ))) + } EngineCall::EvalClosure { closure, positional, diff --git a/crates/nu-plugin/src/protocol/mod.rs b/crates/nu-plugin/src/protocol/mod.rs index 432961c29d..84835c8b50 100644 --- a/crates/nu-plugin/src/protocol/mod.rs +++ b/crates/nu-plugin/src/protocol/mod.rs @@ -463,6 +463,8 @@ pub enum EngineCall { AddEnvVar(String, Value), /// Get help for the current command GetHelp, + /// Get the contents of a span. Response is a binary which may not parse to UTF-8 + GetSpanContents(Span), /// Evaluate a closure with stream input/output EvalClosure { /// The closure to call. @@ -491,6 +493,7 @@ impl EngineCall { EngineCall::GetCurrentDir => "GetCurrentDir", EngineCall::AddEnvVar(..) => "AddEnvVar", EngineCall::GetHelp => "GetHelp", + EngineCall::GetSpanContents(_) => "GetSpanContents", EngineCall::EvalClosure { .. } => "EvalClosure", } } @@ -509,6 +512,7 @@ impl EngineCall { EngineCall::GetCurrentDir => EngineCall::GetCurrentDir, EngineCall::AddEnvVar(name, value) => EngineCall::AddEnvVar(name, value), EngineCall::GetHelp => EngineCall::GetHelp, + EngineCall::GetSpanContents(span) => EngineCall::GetSpanContents(span), EngineCall::EvalClosure { closure, positional, diff --git a/crates/nu_plugin_example/src/commands/mod.rs b/crates/nu_plugin_example/src/commands/mod.rs index 0acffeb566..2d7ef4274a 100644 --- a/crates/nu_plugin_example/src/commands/mod.rs +++ b/crates/nu_plugin_example/src/commands/mod.rs @@ -16,10 +16,12 @@ pub use two::Two; mod config; mod disable_gc; mod env; +mod view_span; pub use config::Config; pub use disable_gc::DisableGc; pub use env::Env; +pub use view_span::ViewSpan; // Stream demos mod collect_external; diff --git a/crates/nu_plugin_example/src/commands/view_span.rs b/crates/nu_plugin_example/src/commands/view_span.rs new file mode 100644 index 0000000000..95f7cd1166 --- /dev/null +++ b/crates/nu_plugin_example/src/commands/view_span.rs @@ -0,0 +1,58 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; +use nu_protocol::{Category, Example, LabeledError, Signature, Type, Value}; + +use crate::ExamplePlugin; + +/// ` | example view span` +pub struct ViewSpan; + +impl SimplePluginCommand for ViewSpan { + type Plugin = ExamplePlugin; + + fn name(&self) -> &str { + "example view span" + } + + fn usage(&self) -> &str { + "Example command for looking up the contents of a parser span" + } + + fn extra_usage(&self) -> &str { + "Shows the original source code of the expression that generated the value passed as input." + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_type(Type::Any, Type::String) + .category(Category::Experimental) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["example"] + } + + fn examples(&self) -> Vec { + vec![Example { + example: "('hello ' ++ 'world') | example view span", + description: "Show the source code of the expression that generated a value", + result: Some(Value::test_string("'hello ' ++ 'world'")), + }] + } + + fn run( + &self, + _plugin: &ExamplePlugin, + engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + let contents = engine.get_span_contents(input.span())?; + Ok(Value::string(String::from_utf8_lossy(&contents), call.head)) + } +} + +#[test] +fn test_examples() -> Result<(), nu_protocol::ShellError> { + use nu_plugin_test_support::PluginTest; + PluginTest::new("example", ExamplePlugin.into())?.test_command_examples(&ViewSpan) +} diff --git a/crates/nu_plugin_example/src/lib.rs b/crates/nu_plugin_example/src/lib.rs index 1328473c1b..0c394c78aa 100644 --- a/crates/nu_plugin_example/src/lib.rs +++ b/crates/nu_plugin_example/src/lib.rs @@ -21,6 +21,7 @@ impl Plugin for ExamplePlugin { // Engine interface demos Box::new(Config), Box::new(Env), + Box::new(ViewSpan), Box::new(DisableGc), // Stream demos Box::new(CollectExternal),