Expand Nushell's help system (#7611)

This commit is contained in:
Jakub Žádník
2022-12-30 17:44:37 +02:00
committed by GitHub
parent f3d2be7a56
commit 8bfcea8054
23 changed files with 1509 additions and 446 deletions

View File

@ -31,6 +31,51 @@ pub enum ReplOperation {
Replace(String),
}
/// Organizes usage messages for various primitives
#[derive(Debug, Clone)]
pub struct Usage {
// TODO: Move decl usages here
alias_comments: HashMap<AliasId, Vec<Span>>,
module_comments: HashMap<ModuleId, Vec<Span>>,
}
impl Usage {
pub fn new() -> Self {
Usage {
alias_comments: HashMap::new(),
module_comments: HashMap::new(),
}
}
pub fn add_alias_comments(&mut self, alias_id: AliasId, comments: Vec<Span>) {
self.alias_comments.insert(alias_id, comments);
}
pub fn add_module_comments(&mut self, module_id: ModuleId, comments: Vec<Span>) {
self.module_comments.insert(module_id, comments);
}
pub fn get_alias_comments(&self, alias_id: AliasId) -> Option<&[Span]> {
self.alias_comments.get(&alias_id).map(|v| v.as_ref())
}
pub fn get_module_comments(&self, module_id: ModuleId) -> Option<&[Span]> {
self.module_comments.get(&module_id).map(|v| v.as_ref())
}
/// Overwrite own values with the other
pub fn merge_with(&mut self, other: Usage) {
self.alias_comments.extend(other.alias_comments);
self.module_comments.extend(other.module_comments);
}
}
impl Default for Usage {
fn default() -> Self {
Self::new()
}
}
/// The core global engine state. This includes all global definitions as well as any global state that
/// will persist for the whole session.
///
@ -82,6 +127,7 @@ pub struct EngineState {
aliases: Vec<Vec<Span>>,
blocks: Vec<Block>,
modules: Vec<Module>,
usage: Usage,
pub scope: ScopeFrame,
pub ctrlc: Option<Arc<AtomicBool>>,
pub env_vars: EnvVars,
@ -125,6 +171,7 @@ impl EngineState {
aliases: vec![],
blocks: vec![],
modules: vec![Module::new()],
usage: Usage::new(),
// make sure we have some default overlay:
scope: ScopeFrame::with_empty_overlay(
DEFAULT_OVERLAY_NAME.as_bytes().to_vec(),
@ -167,6 +214,7 @@ impl EngineState {
self.vars.extend(delta.vars);
self.blocks.extend(delta.blocks);
self.modules.extend(delta.modules);
self.usage.merge_with(delta.usage);
let first = delta.scope.remove(0);
@ -553,6 +601,14 @@ impl EngineState {
None
}
pub fn get_alias_comments(&self, alias_id: AliasId) -> Option<&[Span]> {
self.usage.get_alias_comments(alias_id)
}
pub fn get_module_comments(&self, module_id: ModuleId) -> Option<&[Span]> {
self.usage.get_module_comments(module_id)
}
#[cfg(feature = "plugin")]
pub fn plugin_decls(&self) -> impl Iterator<Item = &Box<dyn Command + 'static>> {
let mut unique_plugin_decls = HashMap::new();
@ -580,24 +636,39 @@ impl EngineState {
None
}
pub fn which_module_has_decl(&self, name: &[u8]) -> Option<&[u8]> {
for (module_id, m) in self.modules.iter().enumerate() {
if m.has_decl(name) {
for overlay_frame in self.active_overlays(&[]).iter() {
let module_name = overlay_frame.modules.iter().find_map(|(key, &val)| {
if val == module_id {
Some(key)
} else {
None
}
});
if let Some(final_name) = module_name {
return Some(&final_name[..]);
}
pub fn which_module_has_decl(
&self,
decl_name: &[u8],
removed_overlays: &[Vec<u8>],
) -> Option<&[u8]> {
for overlay_frame in self.active_overlays(removed_overlays).iter().rev() {
for (module_name, module_id) in overlay_frame.modules.iter() {
let module = self.get_module(*module_id);
if module.has_decl(decl_name) {
return Some(module_name);
}
}
}
None
// for (module_id, m) in self.modules.iter().enumerate() {
// if m.has_decl(name) {
// for overlay_frame in self.active_overlays(&[]).iter() {
// let module_name = overlay_frame.modules.iter().find_map(|(key, &val)| {
// if val == module_id {
// Some(key)
// } else {
// None
// }
// });
// if let Some(final_name) = module_name {
// return Some(&final_name[..]);
// }
// }
// }
// }
// None
}
pub fn find_overlay(&self, name: &[u8]) -> Option<OverlayId> {
@ -687,8 +758,39 @@ impl EngineState {
.as_ref()
}
/// Get all IDs of all commands within scope, sorted by the commads' names
pub fn get_decl_ids_sorted(&self, include_hidden: bool) -> impl Iterator<Item = DeclId> {
/// Get all aliases within scope, sorted by the alias names
pub fn get_aliases_sorted(
&self,
include_hidden: bool,
) -> impl Iterator<Item = (Vec<u8>, DeclId)> {
let mut aliases_map = HashMap::new();
for overlay_frame in self.active_overlays(&[]) {
let new_aliases = if include_hidden {
overlay_frame.aliases.clone()
} else {
overlay_frame
.aliases
.clone()
.into_iter()
.filter(|(_, id)| overlay_frame.visibility.is_alias_id_visible(id))
.collect()
};
aliases_map.extend(new_aliases);
}
let mut aliases: Vec<(Vec<u8>, DeclId)> = aliases_map.into_iter().collect();
aliases.sort_by(|a, b| a.0.cmp(&b.0));
aliases.into_iter()
}
/// Get all commands within scope, sorted by the commads' names
pub fn get_decls_sorted(
&self,
include_hidden: bool,
) -> impl Iterator<Item = (Vec<u8>, DeclId)> {
let mut decls_map = HashMap::new();
for overlay_frame in self.active_overlays(&[]) {
@ -710,16 +812,19 @@ impl EngineState {
decls_map.into_iter().map(|(v, k)| (v.0, k)).collect();
decls.sort_by(|a, b| a.0.cmp(&b.0));
decls.into_iter().map(|(_, id)| id)
decls.into_iter()
}
/// Get signatures of all commands within scope.
pub fn get_signatures(&self, include_hidden: bool) -> Vec<Signature> {
self.get_decl_ids_sorted(include_hidden)
.map(|id| {
self.get_decls_sorted(include_hidden)
.map(|(name_bytes, id)| {
let decl = self.get_decl(id);
// the reason to create the name this way is because the command could be renamed
// during module imports but the signature still contains the old name
let name = String::from_utf8_lossy(&name_bytes).to_string();
(*decl).signature().update_from_command(decl.borrow())
(*decl).signature().update_from_command(name, decl.borrow())
})
.collect()
}
@ -733,11 +838,14 @@ impl EngineState {
&self,
include_hidden: bool,
) -> Vec<(Signature, Vec<Example>, bool, bool, bool)> {
self.get_decl_ids_sorted(include_hidden)
.map(|id| {
self.get_decls_sorted(include_hidden)
.map(|(name_bytes, id)| {
let decl = self.get_decl(id);
// the reason to create the name this way is because the command could be renamed
// during module imports but the signature still contains the old name
let name = String::from_utf8_lossy(&name_bytes).to_string();
let signature = (*decl).signature().update_from_command(decl.borrow());
let signature = (*decl).signature().update_from_command(name, decl.borrow());
(
signature,
@ -839,6 +947,24 @@ impl EngineState {
pub fn get_config_path(&self, key: &str) -> Option<&PathBuf> {
self.config_path.get(key)
}
pub fn build_usage(&self, spans: &[Span]) -> (String, String) {
let comment_lines: Vec<&[u8]> = spans
.iter()
.map(|span| self.get_span_contents(span))
.collect();
build_usage(&comment_lines)
}
pub fn build_alias_usage(&self, alias_id: AliasId) -> Option<(String, String)> {
self.get_alias_comments(alias_id)
.map(|comment_spans| self.build_usage(comment_spans))
}
pub fn build_module_usage(&self, module_id: ModuleId) -> Option<(String, String)> {
self.get_module_comments(module_id)
.map(|comment_spans| self.build_usage(comment_spans))
}
}
/// A temporary extension to the global state. This handles bridging between the global state and the
@ -915,6 +1041,7 @@ pub struct StateDelta {
aliases: Vec<Vec<Span>>, // indexed by AliasId
pub blocks: Vec<Block>, // indexed by BlockId
modules: Vec<Module>, // indexed by ModuleId
usage: Usage,
pub scope: Vec<ScopeFrame>,
#[cfg(feature = "plugin")]
plugins_changed: bool, // marks whether plugin file should be updated
@ -938,6 +1065,7 @@ impl StateDelta {
blocks: vec![],
modules: vec![],
scope: vec![scope_frame],
usage: Usage::new(),
#[cfg(feature = "plugin")]
plugins_changed: false,
}
@ -1305,12 +1433,16 @@ impl<'a> StateWorkingSet<'a> {
self.num_blocks() - 1
}
pub fn add_module(&mut self, name: &str, module: Module) -> ModuleId {
pub fn add_module(&mut self, name: &str, module: Module, comments: Vec<Span>) -> ModuleId {
let name = name.as_bytes().to_vec();
self.delta.modules.push(module);
let module_id = self.num_modules() - 1;
if !comments.is_empty() {
self.delta.usage.add_module_comments(module_id, comments);
}
self.last_overlay_mut().modules.insert(name, module_id);
module_id
@ -1633,10 +1765,14 @@ impl<'a> StateWorkingSet<'a> {
next_id
}
pub fn add_alias(&mut self, name: Vec<u8>, replacement: Vec<Span>) {
pub fn add_alias(&mut self, name: Vec<u8>, replacement: Vec<Span>, comments: Vec<Span>) {
self.delta.aliases.push(replacement);
let alias_id = self.num_aliases() - 1;
if !comments.is_empty() {
self.delta.usage.add_alias_comments(alias_id, comments);
}
let last = self.last_overlay_mut();
last.aliases.insert(name, alias_id);
@ -2051,17 +2187,13 @@ impl<'a> StateWorkingSet<'a> {
pub fn render(self) -> StateDelta {
self.delta
}
}
impl Default for Visibility {
fn default() -> Self {
Self::new()
}
}
impl Default for ScopeFrame {
fn default() -> Self {
Self::new()
pub fn build_usage(&self, spans: &[Span]) -> (String, String) {
let comment_lines: Vec<&[u8]> = spans
.iter()
.map(|span| self.get_span_contents(*span))
.collect();
build_usage(&comment_lines)
}
}
@ -2148,6 +2280,59 @@ impl<'a> miette::SourceCode for &StateWorkingSet<'a> {
}
}
fn build_usage(comment_lines: &[&[u8]]) -> (String, String) {
let mut usage = String::new();
let mut num_spaces = 0;
let mut first = true;
// Use the comments to build the usage
for contents in comment_lines {
let comment_line = if first {
// Count the number of spaces still at the front, skipping the '#'
let mut pos = 1;
while pos < contents.len() {
if let Some(b' ') = contents.get(pos) {
// continue
} else {
break;
}
pos += 1;
}
num_spaces = pos;
first = false;
String::from_utf8_lossy(&contents[pos..]).to_string()
} else {
let mut pos = 1;
while pos < contents.len() && pos < num_spaces {
if let Some(b' ') = contents.get(pos) {
// continue
} else {
break;
}
pos += 1;
}
String::from_utf8_lossy(&contents[pos..]).to_string()
};
if !usage.is_empty() {
usage.push('\n');
}
usage.push_str(&comment_line);
}
if let Some((brief_usage, extra_usage)) = usage.split_once("\n\n") {
(brief_usage.to_string(), extra_usage.to_string())
} else {
(usage, String::default())
}
}
#[cfg(test)]
mod engine_state_tests {
use super::*;

View File

@ -44,14 +44,14 @@ impl Visibility {
self.alias_ids.insert(*alias_id, true);
}
/// Overwrite own values with the other
pub fn merge_with(&mut self, other: Visibility) {
// overwrite own values with the other
self.decl_ids.extend(other.decl_ids);
self.alias_ids.extend(other.alias_ids);
}
/// Take new values from the other but keep own values
pub fn append(&mut self, other: &Visibility) {
// take new values from the other but keep own values
for (decl_id, visible) in other.decl_ids.iter() {
if !self.decl_ids.contains_key(decl_id) {
self.decl_ids.insert(*decl_id, *visible);
@ -79,10 +79,6 @@ pub struct ScopeFrame {
/// Order is significant: The last item points at the last activated overlay.
pub active_overlays: Vec<OverlayId>,
/// Deactivated overlays from permanent state.
/// ! Stores OverlayIds from the permanent state, not from this frame. !
// removed_overlays: Vec<OverlayId>,
/// Removed overlays from previous scope frames / permanent state
pub removed_overlays: Vec<Vec<u8>>,
@ -281,3 +277,15 @@ impl<'a> Borrow<dyn DeclKey + 'a> for (Vec<u8>, Type) {
self
}
}
impl Default for Visibility {
fn default() -> Self {
Self::new()
}
}
impl Default for ScopeFrame {
fn default() -> Self {
Self::new()
}
}

View File

@ -542,6 +542,15 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE
#[diagnostic(code(nu::shell::command_not_found), url(docsrs))]
CommandNotFound(#[label("command not found")] Span),
/// This alias could not be found
///
/// ## Resolution
///
/// The alias does not exist in the current scope. It might exist in another scope or overlay or be hidden.
#[error("Alias not found")]
#[diagnostic(code(nu::shell::alias_not_found), url(docsrs))]
AliasNotFound(#[label("alias not found")] Span),
/// A flag was not found.
#[error("Flag not found")]
#[diagnostic(code(nu::shell::flag_not_found), url(docsrs))]
@ -868,9 +877,6 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE
#[diagnostic(code(nu::shell::non_unicode_input), url(docsrs))]
NonUnicodeInput,
// /// Path not found.
// #[error("Path not found.")]
// PathNotFound,
/// Unexpected abbr component.
///
/// ## Resolution

View File

@ -265,7 +265,8 @@ impl Signature {
}
/// Update signature's fields from a Command trait implementation
pub fn update_from_command(mut self, command: &dyn Command) -> Signature {
pub fn update_from_command(mut self, name: String, command: &dyn Command) -> Signature {
self.name = name;
self.search_terms = command
.search_terms()
.into_iter()
@ -669,6 +670,10 @@ impl Command for Predeclaration {
&self.signature.usage
}
fn extra_usage(&self) -> &str {
&self.signature.extra_usage
}
fn run(
&self,
_engine_state: &EngineState,
@ -718,6 +723,10 @@ impl Command for BlockCommand {
&self.signature.usage
}
fn extra_usage(&self) -> &str {
&self.signature.extra_usage
}
fn run(
&self,
_engine_state: &EngineState,