Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add autocompletions for functions and variables #28

Merged
merged 2 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
9 changes: 3 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
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