From 00b3a07efeead7a6728c2af17c76267f864cce3c Mon Sep 17 00:00:00 2001 From: Devyn Cairns Date: Tue, 9 Apr 2024 07:02:17 -0700 Subject: [PATCH] Add `GetSpanContents` engine call (#12439) # Description This allows plugins to view the source code of spans. Requested by @ayax79 for implementing `polars ls`. Note that this won't really help you find the location of the span. I'm planning to add another engine call that will return information more similar to what shows up in the miette diagnostics, with filename / line number / some context, but I'll want to refactor some of the existing logic to make that happen, so it was easier to just do this first. I hope this is enough to at least have something somewhat useful show up for `polars ls`. # User-Facing Changes - Example plugin: added `example view span` command # Tests + Formatting - :green_circle: `toolkit fmt` - :green_circle: `toolkit clippy` - :green_circle: `toolkit test` - :green_circle: `toolkit test stdlib` # After Submitting - [ ] Add to plugin protocol reference --- crates/nu-plugin/src/plugin/context.rs | 18 +++++- .../nu-plugin/src/plugin/interface/engine.rs | 18 +++++- .../src/plugin/interface/engine/tests.rs | 18 ++++++ .../nu-plugin/src/plugin/interface/plugin.rs | 7 +++ crates/nu-plugin/src/protocol/mod.rs | 4 ++ crates/nu_plugin_example/src/commands/mod.rs | 2 + .../src/commands/view_span.rs | 58 +++++++++++++++++++ crates/nu_plugin_example/src/lib.rs | 1 + 8 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 crates/nu_plugin_example/src/commands/view_span.rs 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),