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
- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting
- [ ] Add to plugin protocol reference
This commit is contained in:
Devyn Cairns 2024-04-09 07:02:17 -07:00 committed by GitHub
parent 9a2a6ab52c
commit 00b3a07efe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 124 additions and 2 deletions

View File

@ -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<Spanned<String>, ShellError>;
/// Get the contents of a [`Span`]
fn get_span_contents(&self, span: Span) -> Result<Spanned<Vec<u8>>, 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<Spanned<Vec<u8>>, ShellError> {
Ok(self
.engine_state
.get_span_contents(span)
.to_vec()
.into_spanned(self.call.head))
}
fn eval_closure(
&self,
closure: Spanned<Closure>,
@ -271,6 +281,12 @@ impl PluginExecutionContext for PluginExecutionBogusContext {
})
}
fn get_span_contents(&self, _span: Span) -> Result<Spanned<Vec<u8>>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_span_contents not implemented on bogus".into(),
})
}
fn eval_closure(
&self,
_closure: Spanned<Closure>,

View File

@ -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<u8>` 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<Vec<u8>, 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.
///

View File

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

View File

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

View File

@ -463,6 +463,8 @@ pub enum EngineCall<D> {
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<D> EngineCall<D> {
EngineCall::GetCurrentDir => "GetCurrentDir",
EngineCall::AddEnvVar(..) => "AddEnvVar",
EngineCall::GetHelp => "GetHelp",
EngineCall::GetSpanContents(_) => "GetSpanContents",
EngineCall::EvalClosure { .. } => "EvalClosure",
}
}
@ -509,6 +512,7 @@ impl<D> EngineCall<D> {
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,

View File

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

View File

@ -0,0 +1,58 @@
use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand};
use nu_protocol::{Category, Example, LabeledError, Signature, Type, Value};
use crate::ExamplePlugin;
/// `<value> | 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<Example> {
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<Value, LabeledError> {
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)
}

View File

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