Skip to content

Commit

Permalink
Add autocompletions for functions and variables
Browse files Browse the repository at this point in the history
  • Loading branch information
doonv committed May 25, 2024
1 parent f645e20 commit b53ec39
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 16 deletions.
11 changes: 10 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ license = "MIT OR Apache-2.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["builtin-parser"]
default = ["builtin-parser", "completions", "builtin-parser-completions"]

completions = []

builtin-parser-completions = [
"completions",
"builtin-parser",
"dep:fuzzy-matcher",
]

builtin-parser = ["dep:logos"]

Expand All @@ -24,6 +32,7 @@ web-time = "1.0.0"

# builtin-parser features
logos = { version = "0.14.0", optional = true }
fuzzy-matcher = { version = "0.3.7", optional = true }

[dev-dependencies]
bevy = "0.13.0"
Expand Down
39 changes: 38 additions & 1 deletion src/builtin_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ use logos::Span;
use crate::builtin_parser::runner::ExecutionError;
use crate::command::{CommandHints, CommandParser, DefaultCommandParser};

#[cfg(feature = "builtin-parser-completions")]
use crate::command::CompletionSuggestion;

#[cfg(feature = "builtin-parser-completions")]
pub(crate) mod completions;
pub(crate) mod lexer;
pub(crate) mod number;
pub(crate) mod parser;
Expand Down Expand Up @@ -90,7 +95,6 @@ impl CommandParser for BuiltinCommandParser {
.resource_mut::<CommandHints>()
.push(eval_error.hints());
}

error!("{error}")
}
},
Expand All @@ -100,5 +104,38 @@ impl CommandParser for BuiltinCommandParser {
error!("{err}")
}
}
#[cfg(feature = "builtin-parser-completions")]
{
*world.resource_mut() =
completions::store_in_cache(world.non_send_resource::<Environment>());
}
}

#[cfg(feature = "builtin-parser-completions")]
fn completion(&self, command: &str, world: &World) -> Vec<CompletionSuggestion> {
use fuzzy_matcher::FuzzyMatcher;

use crate::builtin_parser::completions::EnvironmentCache;

let matcher = fuzzy_matcher::skim::SkimMatcherV2::default();
let environment_cache = world.resource::<EnvironmentCache>();

let mut names: Vec<_> = environment_cache
.function_names
.iter()
.chain(environment_cache.variable_names.iter())
.map(|name| (matcher.fuzzy_indices(name, command), name.clone()))
.filter_map(|(fuzzy, name)| fuzzy.map(|v| (v, name)))
.collect();
names.sort_by_key(|((score, _), _)| std::cmp::Reverse(*score));
names.truncate(crate::ui::MAX_COMPLETION_SUGGESTIONS);

names
.into_iter()
.map(|((_, indices), name)| CompletionSuggestion {
suggestion: name,
highlighted_indices: indices,
})
.collect()
}
}
55 changes: 55 additions & 0 deletions src/builtin_parser/completions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use bevy::prelude::*;

use super::runner::environment::Variable;
use super::Environment;

/// Stores the names of variables and functions for fast async access.
#[derive(Resource)]
pub struct EnvironmentCache {
pub function_names: Vec<String>,
pub variable_names: Vec<String>,
}
impl FromWorld for EnvironmentCache {
fn from_world(world: &mut World) -> Self {
if let Some(environment) = world.get_non_send_resource::<Environment>() {
store_in_cache(environment)
} else {
Self::empty()
}
}
}
impl EnvironmentCache {
pub const fn empty() -> Self {
EnvironmentCache {
function_names: Vec::new(),
variable_names: Vec::new(),
}
}
}

pub fn store_in_cache(environment: &Environment) -> EnvironmentCache {
let mut function_names = Vec::new();
let mut variable_names = Vec::new();
store_in_cache_vec(environment, &mut function_names, &mut variable_names);

EnvironmentCache {
function_names,
variable_names,
}
}
fn store_in_cache_vec(
environment: &Environment,
function_names: &mut Vec<String>,
variable_names: &mut Vec<String>,
) {
for (name, variable) in &environment.variables {
match variable {
Variable::Function(_) => function_names.push(name.clone()),
Variable::Unmoved(_) => variable_names.push(name.clone()),
Variable::Moved => {}
}
}
if let Some(environment) = &environment.parent {
store_in_cache_vec(environment, function_names, variable_names);
}
}
10 changes: 4 additions & 6 deletions src/builtin_parser/runner/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
use std::collections::HashMap;
use std::fmt::Debug;

use crate::builtin_parser::SpanExtension;
use bevy::ecs::world::World;
use bevy::log::warn;
use bevy::reflect::TypeRegistration;
use logos::Span;

use crate::builtin_parser::SpanExtension;

use super::super::parser::Expression;
use super::super::Spanned;
use super::error::EvalError;
Expand Down Expand Up @@ -203,12 +202,10 @@ pub enum Variable {
}

/// The environment stores all variables and functions.
#[derive(Debug)]
pub struct Environment {
parent: Option<Box<Environment>>,
variables: HashMap<String, Variable>,
pub(crate) parent: Option<Box<Environment>>,
pub(crate) variables: HashMap<String, Variable>,
}

impl Default for Environment {
fn default() -> Self {
let mut env = Self {
Expand Down Expand Up @@ -352,6 +349,7 @@ impl Environment {
let name = name.into();
if self.variables.contains_key(&name) {
warn!("Function {name} declared twice.");
} else {
}
self.variables
.insert(name, Variable::Function(function.into_function()));
Expand Down
41 changes: 41 additions & 0 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,17 @@ pub struct DefaultCommandParser(pub Box<dyn CommandParser>);

impl DefaultCommandParser {
/// Shortcut method for calling `parser.0.parse(command, world)`.
#[inline]
pub fn parse(&self, command: &str, world: &mut World) {
self.0.parse(command, world)
}
/// Shortcut method for calling `parser.0.completion(command, world)`.
#[inline]
#[must_use]
#[cfg(feature = "completions")]
pub fn completion(&self, keyword: &str, world: &World) -> Vec<CompletionSuggestion> {
self.0.completion(keyword, world)
}
}
impl<Parser: CommandParser> From<Parser> for DefaultCommandParser {
fn from(value: Parser) -> Self {
Expand Down Expand Up @@ -129,6 +137,23 @@ impl CommandHints {
pub trait CommandParser: Send + Sync + 'static {
/// This method is called by the console when a command is ran.
fn parse(&self, command: &str, world: &mut World);
/// This method is called by the console when the command is changed.
#[inline]
#[must_use]
#[cfg(feature = "completions")]
fn completion(&self, keyword: &str, world: &World) -> Vec<CompletionSuggestion> {
let _ = (keyword, world);
Vec::new()
}
}

/// A suggestion for autocomplete.
#[cfg(feature = "completions")]
pub struct CompletionSuggestion {
/// The suggestion string
pub suggestion: String,
/// The character indices of the [`suggestion`](Self::suggestion) to highlight.
pub highlighted_indices: Vec<usize>,
}

pub(crate) struct ExecuteCommand(pub String);
Expand All @@ -142,3 +167,19 @@ impl Command for ExecuteCommand {
}
}
}

#[derive(Resource, Default, Deref, DerefMut)]
#[cfg(feature = "completions")]
pub(crate) struct AutoCompletions(pub Vec<CompletionSuggestion>);
#[cfg(feature = "completions")]
pub(crate) struct UpdateAutoComplete(pub String);
#[cfg(feature = "completions")]
impl Command for UpdateAutoComplete {
fn apply(self, world: &mut World) {
if let Some(parser) = world.remove_resource::<DefaultCommandParser>() {
let completions = parser.completion(&self.0, world);
world.resource_mut::<AutoCompletions>().0 = completions;
world.insert_resource(parser);
}
}
}
10 changes: 10 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ impl ConsoleTheme {
}
}

/// Returns a [`TextFormat`] with the default font and white color.
pub fn format_bold(&self) -> TextFormat {
TextFormat {
font_id: self.font.clone(),
color: Color32::WHITE,

..default()
}
}

define_text_format_method!(format_dark, dark);
define_text_format_method!(format_error, error);
define_text_format_method!(format_warning, warning);
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ impl Plugin for DevConsolePlugin {
{
app.init_non_send_resource::<builtin_parser::Environment>();
app.init_resource::<command::DefaultCommandParser>();
#[cfg(feature = "builtin-parser-completions")]
app.init_resource::<builtin_parser::completions::EnvironmentCache>();
}
#[cfg(feature = "completions")]
app.init_resource::<command::AutoCompletions>();

app.init_resource::<ConsoleUiState>()
.init_resource::<CommandHints>()
Expand Down
1 change: 1 addition & 0 deletions src/logging/log_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use bevy::utils::tracing::Subscriber;
pub use bevy::utils::tracing::{warn, Level};

use bevy::app::{App, Plugin, Update};
use bevy::utils::tracing;
use tracing_log::LogTracer;
use tracing_subscriber::field::Visit;
#[cfg(feature = "tracing-chrome")]
Expand Down
42 changes: 34 additions & 8 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ use crate::config::ToColor32;
use crate::logging::log_plugin::LogMessage;
use crate::prelude::ConsoleConfig;

#[cfg(feature = "completions")]
use crate::command::AutoCompletions;

#[cfg(feature = "completions")]
mod completions;
#[cfg(feature = "completions")]
pub use completions::MAX_COMPLETION_SUGGESTIONS;

/// Prefix for log messages that show a previous command.
pub const COMMAND_MESSAGE_PREFIX: &str = "$ ";
/// Prefix for log messages that show the result of a command.
Expand All @@ -34,6 +42,8 @@ pub(crate) struct ConsoleUiState {
pub(crate) log: Vec<(LogMessage, bool)>,
/// The command in the text bar.
pub(crate) command: String,
#[cfg(feature = "completions")]
pub(crate) selected_completion: usize,
}

fn system_time_to_chrono_utc(t: SystemTime) -> chrono::DateTime<chrono::Utc> {
Expand Down Expand Up @@ -67,24 +77,27 @@ pub(crate) fn render_ui(
key: Res<ButtonInput<KeyCode>>,
mut hints: ResMut<CommandHints>,
config: Res<ConsoleConfig>,
#[cfg(feature = "completions")] completions: Res<AutoCompletions>,
) {
let mut submit_command = |command: &mut String| {
fn submit_command(command: &mut String, commands: &mut Commands) {
if !command.trim().is_empty() {
info!(name: COMMAND_MESSAGE_NAME, "{COMMAND_MESSAGE_PREFIX}{}", command.trim());
// Get the owned command string by replacing it with an empty string
let command = std::mem::take(command);
commands.add(ExecuteCommand(command));
}
};
}

if key.just_pressed(config.submit_key) {
submit_command(&mut state.command);
submit_command(&mut state.command, &mut commands);
}

egui::Window::new("Developer Console")
.collapsible(false)
.default_width(900.)
.show(contexts.ctx_mut(), |ui| {
completions::change_selected_completion(ui, &mut state, &completions);

// A General rule when creating layouts in egui is to place elements which fill remaining space last.
// Since immediate mode ui can't predict the final sizes of widgets until they've already been drawn

Expand All @@ -101,22 +114,35 @@ pub(crate) fn render_ui(

//We can use a right to left layout, so we can place the text input last and tell it to fill all remaining space
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
// ui.button is a shorthand command, a similar command exists for text edits, but this is how to manually construct a widget.
// doing this also allows access to more options of the widget, rather than being stuck with the default the shorthand picks.
if ui.button("Submit").clicked() {
submit_command(&mut state.command);
submit_command(&mut state.command, &mut commands);

// Return keyboard focus to the text edit control.
ui.ctx().memory_mut(|mem| mem.request_focus(text_edit_id));
}
// ui.button is a shorthand command, a similar command exists for text edits, but this is how to manually construct a widget.
// doing this also allows access to more options of the widget, rather than being stuck with the default the shorthand picks.

#[cfg_attr(not(feature = "completions"), allow(unused_variables))]
let text_edit = egui::TextEdit::singleline(&mut state.command)
.id(text_edit_id)
.desired_width(ui.available_width())
.margin(egui::Vec2::splat(4.0))
.font(config.theme.font.clone())
.lock_focus(true);
.lock_focus(true)
.show(ui);

ui.add(text_edit);
// Display completions if the "completions" feature is enabled
#[cfg(feature = "completions")]
completions::completions(
text_edit,
text_edit_id,
&mut state,
ui,
commands,
&completions,
&config,
);

// Each time we open the console, we want to set focus to the text edit control.
if !state.text_focus {
Expand Down
Loading

0 comments on commit b53ec39

Please sign in to comment.