forked from jess/Acord
1
0
Fork 0

Added more auto-pairs types, '', "";

Added toggles in the menu for which auto-pairs to complete.
This commit is contained in:
jess 2026-04-28 22:55:27 -07:00
parent bae246f08d
commit 596482436a
12 changed files with 254 additions and 59 deletions

View File

@ -13,6 +13,7 @@ use acord_viewport::{
viewport_create, viewport_destroy, viewport_render, viewport_resize,
viewport_set_text, viewport_get_text, viewport_set_theme, viewport_set_lang,
viewport_set_line_indicator, viewport_set_gutter_rainbow,
viewport_set_auto_pair_flags,
viewport_send_command, viewport_free_string,
ViewportHandle,
};
@ -60,6 +61,7 @@ impl App {
let ind = CString::new(self.config.line_indicator()).unwrap();
viewport_set_line_indicator(self.handle, ind.as_ptr());
viewport_set_gutter_rainbow(self.handle, self.config.gutter_rainbow());
viewport_set_auto_pair_flags(self.handle, self.config.auto_pair_flags());
}
fn dispatch_menu(&mut self, action: MenuAction, event_loop: &ActiveEventLoop) {
@ -146,8 +148,8 @@ impl App {
}
fn new_note(&mut self) {
let empty = CString::new("").unwrap();
viewport_set_text(self.handle, empty.as_ptr());
let stub = CString::new("# ").unwrap();
viewport_set_text(self.handle, stub.as_ptr());
if let Some(w) = &self.window {
w.set_title("Acord");
}

View File

@ -55,6 +55,12 @@ impl Config {
.map(PathBuf::from)
.unwrap_or_else(|| config_dir().join("notes"))
}
pub fn auto_pair_flags(&self) -> u32 {
self.data.get("autoPairFlags")
.and_then(|s| s.parse().ok())
.unwrap_or(63)
}
}
/// XDG-friendly config dir with `~/.acord` fallback for parity with the

View File

@ -257,10 +257,45 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
formatItem.target = self
menu.addItem(formatItem)
menu.addItem(.separator())
menu.addItem(buildAutoPairMenu())
item.submenu = menu
return item
}
private func buildAutoPairMenu() -> NSMenuItem {
let item = NSMenuItem(title: "Auto Pair", action: nil, keyEquivalent: "")
let menu = NSMenu(title: "Auto Pair")
let pairs: [(String, UInt32)] = [
("Parens ( )", 1),
("Brackets [ ]", 2),
("Braces { }", 4),
("Single quotes ' '", 8),
("Double quotes \" \"", 16),
("Backticks ` `", 32),
]
let flags = ConfigManager.shared.autoPairFlags
for (label, bit) in pairs {
let mi = NSMenuItem(title: label, action: #selector(toggleAutoPair(_:)), keyEquivalent: "")
mi.target = self
mi.tag = Int(bit)
mi.state = (flags & bit) != 0 ? .on : .off
menu.addItem(mi)
}
item.submenu = menu
return item
}
@objc private func toggleAutoPair(_ sender: NSMenuItem) {
let bit = UInt32(sender.tag)
var flags = ConfigManager.shared.autoPairFlags
flags ^= bit
ConfigManager.shared.autoPairFlags = flags
sender.state = (flags & bit) != 0 ? .on : .off
viewport?.setAutoPairFlags(flags)
}
private func buildRenderMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "Render")
@ -610,6 +645,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
private func syncGutterPrefsToViewport() {
viewport?.setLineIndicator(ConfigManager.shared.lineIndicatorMode)
viewport?.setGutterRainbow(ConfigManager.shared.gutterRainbow)
viewport?.setAutoPairFlags(ConfigManager.shared.autoPairFlags)
}
@objc private func toggleBrowser() {

View File

@ -300,7 +300,7 @@ class AppState: ObservableObject {
let id = bridge.newDocument()
currentNoteID = id
selectedNoteIDs = [id]
documentText = ""
documentText = "# "
evalResults = [:]
modified = false
currentFileURL = nil

View File

@ -62,4 +62,9 @@ class ConfigManager {
get { CGFloat(Double(config["zoomLevel"] ?? "0") ?? 0) }
set { config["zoomLevel"] = String(Double(newValue)); save() }
}
var autoPairFlags: UInt32 {
get { UInt32(config["autoPairFlags"] ?? "63") ?? 63 }
set { config["autoPairFlags"] = String(newValue); save() }
}
}

View File

@ -265,6 +265,11 @@ class IcedViewportView: NSView {
viewport_set_gutter_rainbow(h, enabled)
}
func setAutoPairFlags(_ flags: UInt32) {
guard let h = viewportHandle else { return }
viewport_set_auto_pair_flags(h, flags)
}
/// Returns 0 = Live, 1 = Editor, 2 = View.
func renderMode() -> UInt32 {
guard let h = viewportHandle else { return 0 }

View File

@ -13,6 +13,20 @@
#include <stdint.h>
#include <stdlib.h>
#define PAREN (1 << 0)
#define BRACKET (1 << 1)
#define BRACE (1 << 2)
#define SINGLE (1 << 3)
#define DOUBLE (1 << 4)
#define BACKTICK (1 << 5)
#define ALL (((((PAREN | BRACKET) | BRACE) | SINGLE) | DOUBLE) | BACKTICK)
#define BASE_BOOST 0.30
#define THRESHOLD_PX 6.0
@ -71,6 +85,10 @@ void viewport_set_line_indicator(struct ViewportHandle *handle, const char *mode
void viewport_set_gutter_rainbow(struct ViewportHandle *handle, bool enabled);
void viewport_set_auto_pair_flags(struct ViewportHandle *handle, uint32_t flags);
uint32_t viewport_get_auto_pair_flags(void);
void viewport_send_command(struct ViewportHandle *handle, uint32_t command);
/**

View File

@ -1,7 +1,35 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::Instant;
pub mod auto_pair {
use super::{AtomicU8, Ordering};
pub const PAREN: u8 = 1 << 0;
pub const BRACKET: u8 = 1 << 1;
pub const BRACE: u8 = 1 << 2;
pub const SINGLE: u8 = 1 << 3;
pub const DOUBLE: u8 = 1 << 4;
pub const BACKTICK: u8 = 1 << 5;
pub const ALL: u8 = PAREN | BRACKET | BRACE | SINGLE | DOUBLE | BACKTICK;
static FLAGS: AtomicU8 = AtomicU8::new(ALL);
pub fn enabled(flag: u8) -> bool {
FLAGS.load(Ordering::Relaxed) & flag != 0
}
pub fn flags() -> u8 {
FLAGS.load(Ordering::Relaxed)
}
pub fn set_flags(flags: u8) {
FLAGS.store(flags, Ordering::Relaxed);
}
}
use iced_wgpu::core::keyboard::{self, Modifiers};
use iced_wgpu::core::keyboard::key;
use iced_wgpu::core::text::{Highlight, Wrapping};
@ -502,20 +530,7 @@ fn md_style() -> markdown::Style {
impl EditorState {
pub fn new() -> Self {
let sample = concat!(
"# Block Compositor\n",
"Acord renders structured documents with mixed content.\n\n",
"## Data Table\n",
"| Name | Age | Role |\n",
"|-------|-----|----------|\n",
"| Alice | 30 | Engineer |\n",
"| Bob | 25 | Designer |\n",
"| Carol | 35 | Manager |\n\n",
"---\n\n",
"### Code Section\n",
"let x = 42\n",
"/= x * 2\n",
);
let sample = "# ";
let block_vec = blocks::parse_blocks(sample, "rust");
let (registry, layout) = Self::vec_to_registry(block_vec);
Self {
@ -2235,24 +2250,21 @@ impl EditorState {
None => return eval_interp,
};
// Find which module this block belongs to
let my_module = self.modules.iter().find(|m| m.block_ids.contains(&block_id));
// Evaluate and import root module exports (unless this IS the root)
let is_root = my_module.map(|m| m.is_root).unwrap_or(false);
if !is_root {
if let Some(root) = self.modules.iter().find(|m| m.is_root) {
let root_text = self.module_source_text(root);
let mut root_interp = interp::Interpreter::new();
crate::eval::evaluate_document_with_interp(&mut root_interp, &root_text);
eval_interp.import_all(&root_interp.exports());
let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
let root_exports = self.resolve_module_exports(root, &mut visited);
eval_interp.import_all(&root_exports);
}
}
// Find use declarations in all text blocks of this module and import those modules
let use_block_ids: Vec<crate::selection::BlockId> = my_module
.map(|m| m.block_ids.clone())
.unwrap_or_default();
let my_module_name = my_module.map(|m| m.name.clone()).unwrap_or_default();
for &bid in &use_block_ids {
if let Some(block) = self.registry.get(&bid) {
if let Some(tb) = block.as_any().downcast_ref::<TextBlock>() {
@ -2260,18 +2272,11 @@ impl EditorState {
let use_decls = interp::extract_use_declarations(&text);
for decl in &use_decls {
if let Some(dep_module) = self.modules.iter().find(|m| m.name == decl.module) {
let dep_text = self.module_source_text(dep_module);
let mut dep_interp = interp::Interpreter::new();
if let Some(root) = self.modules.iter().find(|m| m.is_root) {
if !dep_module.is_root {
let root_text = self.module_source_text(root);
let mut root_interp = interp::Interpreter::new();
crate::eval::evaluate_document_with_interp(&mut root_interp, &root_text);
dep_interp.import_all(&root_interp.exports());
}
let mut visited: std::collections::HashSet<String> = std::collections::HashSet::new();
if !my_module_name.is_empty() {
visited.insert(my_module_name.clone());
}
crate::eval::evaluate_document_with_interp(&mut dep_interp, &dep_text);
let dep_exports = dep_interp.exports();
let dep_exports = self.resolve_module_exports(dep_module, &mut visited);
match &decl.item {
None => eval_interp.import_all(&dep_exports),
Some(s) if s == "*" => eval_interp.import_all(&dep_exports),
@ -2286,6 +2291,46 @@ impl EditorState {
eval_interp
}
/// Recursively evaluate a module with its `use` declarations resolved.
fn resolve_module_exports(
&self,
module: &crate::module::Module,
visited: &mut std::collections::HashSet<String>,
) -> acord_core::interp::ModuleExports {
use acord_core::interp;
if !module.name.is_empty() && !visited.insert(module.name.clone()) {
return interp::ModuleExports::default();
}
let mut interp = interp::Interpreter::new();
if !module.is_root {
if let Some(root) = self.modules.iter().find(|m| m.is_root) {
if root.name != module.name {
let root_exports = self.resolve_module_exports(root, visited);
interp.import_all(&root_exports);
}
}
}
let module_text = self.module_source_text(module);
let use_decls = interp::extract_use_declarations(&module_text);
for decl in &use_decls {
if let Some(dep) = self.modules.iter().find(|m| m.name == decl.module) {
let dep_exports = self.resolve_module_exports(dep, visited);
match &decl.item {
None => interp.import_all(&dep_exports),
Some(s) if s == "*" => interp.import_all(&dep_exports),
Some(item) => { interp.import_item(&dep_exports, item); }
}
}
}
crate::eval::evaluate_document_with_interp(&mut interp, &module_text);
interp.exports()
}
fn run_eval(&mut self) {
self.rebuild_modules();
@ -4499,12 +4544,24 @@ fn macos_key_binding(key_press: KeyPress) -> Option<Binding<Message>> {
Some(Binding::Custom(Message::ZoomOut))
}
// Cmd+0 lives in handle.rs now (FixUp); Cmd+Shift+0 resets zoom.
keyboard::Key::Character("[") if !modifiers.logo() && !modifiers.alt() && !modifiers.control() => {
keyboard::Key::Character("[") if !modifiers.logo() && !modifiers.alt() && !modifiers.control() && auto_pair::enabled(auto_pair::BRACKET) => {
Some(Binding::Custom(Message::AutoPair("[", "]")))
}
keyboard::Key::Character("{") if !modifiers.logo() && !modifiers.alt() && !modifiers.control() => {
keyboard::Key::Character("{") if !modifiers.logo() && !modifiers.alt() && !modifiers.control() && auto_pair::enabled(auto_pair::BRACE) => {
Some(Binding::Custom(Message::AutoPair("{", "}")))
}
keyboard::Key::Character("(") if !modifiers.logo() && !modifiers.alt() && !modifiers.control() && auto_pair::enabled(auto_pair::PAREN) => {
Some(Binding::Custom(Message::AutoPair("(", ")")))
}
keyboard::Key::Character("'") if !modifiers.logo() && !modifiers.alt() && !modifiers.control() && auto_pair::enabled(auto_pair::SINGLE) => {
Some(Binding::Custom(Message::AutoPair("'", "'")))
}
keyboard::Key::Character("\"") if !modifiers.logo() && !modifiers.alt() && !modifiers.control() && auto_pair::enabled(auto_pair::DOUBLE) => {
Some(Binding::Custom(Message::AutoPair("\"", "\"")))
}
keyboard::Key::Character("`") if !modifiers.logo() && !modifiers.alt() && !modifiers.control() && auto_pair::enabled(auto_pair::BACKTICK) => {
Some(Binding::Custom(Message::AutoPair("`", "`")))
}
keyboard::Key::Named(key::Named::Backspace) if modifiers.alt() => {
Some(Binding::Sequence(vec![
Binding::Select(Motion::WordLeft),
@ -4612,24 +4669,14 @@ fn count_leading_char(s: &str, c: char) -> usize {
/// `text`. column is interpreted as char count (cosmic-text convention).
fn byte_offset_for_cursor(text: &str, pos: &text_widget::Position) -> usize {
let mut byte = 0usize;
let mut line_idx = 0usize;
for line in text.split_inclusive('\n') {
for (line_idx, line) in text.split_inclusive('\n').enumerate() {
if line_idx == pos.line {
let col = pos.column;
for (i, _) in line.char_indices().take(col) {
byte += line.as_bytes()[i..i + 1].len();
for (col_idx, (ci, _)) in line.char_indices().enumerate() {
if col_idx == pos.column { return byte + ci; }
}
// Walk col chars precisely.
let mut walked = 0usize;
for (ci, _) in line.char_indices() {
if walked == col { return byte.saturating_sub(line.len()) + ci; }
walked += 1;
}
// col >= line length: clamp to end of line content (before \n).
return byte + line.trim_end_matches('\n').len();
}
byte += line.len();
line_idx += 1;
}
text.len()
}

View File

@ -287,6 +287,19 @@ pub extern "C" fn viewport_set_gutter_rainbow(handle: *mut ViewportHandle, enabl
}
}
#[unsafe(no_mangle)]
pub extern "C" fn viewport_set_auto_pair_flags(handle: *mut ViewportHandle, flags: u32) {
editor::auto_pair::set_flags(flags as u8);
if let Some(h) = unsafe { handle.as_mut() } {
h.needs_redraw = true;
}
}
#[unsafe(no_mangle)]
pub extern "C" fn viewport_get_auto_pair_flags() -> u32 {
editor::auto_pair::flags() as u32
}
#[unsafe(no_mangle)]
pub extern "C" fn viewport_send_command(handle: *mut ViewportHandle, command: u32) {
let h = match unsafe { handle.as_mut() } {

View File

@ -15,6 +15,7 @@ use acord_viewport::{
viewport_create, viewport_destroy, viewport_render, viewport_resize,
viewport_set_text, viewport_get_text, viewport_set_theme, viewport_set_lang,
viewport_set_line_indicator, viewport_set_gutter_rainbow,
viewport_set_auto_pair_flags,
viewport_send_command, viewport_free_string,
ViewportHandle,
};
@ -64,6 +65,7 @@ impl App {
let ind = CString::new(self.config.line_indicator()).unwrap();
viewport_set_line_indicator(self.handle, ind.as_ptr());
viewport_set_gutter_rainbow(self.handle, self.config.gutter_rainbow());
viewport_set_auto_pair_flags(self.handle, self.config.auto_pair_flags());
}
fn dispatch_menu(&mut self, action: MenuAction, event_loop: &ActiveEventLoop) {
@ -96,6 +98,14 @@ impl App {
MenuAction::Undo => { /* TODO */ },
MenuAction::Redo => { /* TODO */ },
MenuAction::ExportCrate => { /* TODO */ },
MenuAction::ToggleAutoPair(bit) => {
let new_flags = self.config.auto_pair_flags() ^ bit;
self.config.set_auto_pair_flags(new_flags);
viewport_set_auto_pair_flags(self.handle, new_flags);
if let Some(menu) = &self._menu {
menu.set_auto_pair_check(bit, (new_flags & bit) != 0);
}
}
}
}
@ -153,8 +163,8 @@ impl App {
}
fn new_note(&mut self) {
let empty = CString::new("").unwrap();
viewport_set_text(self.handle, empty.as_ptr());
let stub = CString::new("# ").unwrap();
viewport_set_text(self.handle, stub.as_ptr());
if let Some(w) = &self.window {
w.set_title("Acord");
}
@ -237,12 +247,15 @@ impl ApplicationHandler for App {
self.handle = viewport_create(hwnd, w, h, self.scale);
self.sync_settings();
// Set up native menu bar.
let app_menu = AppMenu::new();
let app_menu = AppMenu::new(self.config.auto_pair_flags());
#[cfg(target_os = "windows")]
{
if let raw_window_handle::RawWindowHandle::Win32(h) = raw {
unsafe { app_menu.menu.init_for_hwnd(h.hwnd.get()).ok(); }
let theme = match self.config.theme_mode() {
"light" => muda::MenuTheme::Light,
_ => muda::MenuTheme::Dark,
};
unsafe { app_menu.menu.init_for_hwnd_with_theme(h.hwnd.get(), theme).ok(); }
}
}
self._menu = Some(app_menu);

View File

@ -56,6 +56,17 @@ impl Config {
.map(PathBuf::from)
.unwrap_or_else(|| config_dir().join("notes"))
}
pub fn auto_pair_flags(&self) -> u32 {
self.data.get("autoPairFlags")
.and_then(|s| s.parse().ok())
.unwrap_or(63)
}
pub fn set_auto_pair_flags(&mut self, flags: u32) {
self.data.insert("autoPairFlags".to_string(), flags.to_string());
self.save();
}
}
fn config_dir() -> PathBuf {

View File

@ -1,9 +1,17 @@
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu, accelerator::Accelerator};
use muda::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu, accelerator::Accelerator};
use muda::accelerator::{Code, Modifiers};
pub const AP_PAREN: u32 = 1;
pub const AP_BRACKET: u32 = 2;
pub const AP_BRACE: u32 = 4;
pub const AP_SINGLE: u32 = 8;
pub const AP_DOUBLE: u32 = 16;
pub const AP_BACKTICK: u32 = 32;
pub struct AppMenu {
#[allow(dead_code)]
pub menu: Menu,
auto_pair_items: Vec<(u32, CheckMenuItem)>,
}
pub enum MenuAction {
@ -27,10 +35,11 @@ pub enum MenuAction {
Find,
Settings,
ExportCrate,
ToggleAutoPair(u32),
}
impl AppMenu {
pub fn new() -> Self {
pub fn new(auto_pair_flags: u32) -> Self {
let menu = Menu::new();
let file = Submenu::new("File", true);
@ -61,12 +70,30 @@ impl AppMenu {
edit.append(&MenuItem::with_id("italic", "Italic", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyI)))).ok();
edit.append(&MenuItem::with_id("table", "Insert Table", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyT)))).ok();
edit.append(&PredefinedMenuItem::separator()).ok();
let auto_pair_sub = Submenu::new("Auto Pair", true);
let pair_specs: [(u32, &str, &str); 6] = [
(AP_PAREN, "ap_paren", "Parens ( )"),
(AP_BRACKET, "ap_bracket", "Brackets [ ]"),
(AP_BRACE, "ap_brace", "Braces { }"),
(AP_SINGLE, "ap_single", "Single quotes ' '"),
(AP_DOUBLE, "ap_double", "Double quotes \" \""),
(AP_BACKTICK, "ap_backtick", "Backticks ` `"),
];
let mut auto_pair_items: Vec<(u32, CheckMenuItem)> = Vec::with_capacity(6);
for (bit, id, label) in pair_specs {
let item = CheckMenuItem::with_id(id, label, true, (auto_pair_flags & bit) != 0, None);
auto_pair_sub.append(&item).ok();
auto_pair_items.push((bit, item));
}
edit.append(&auto_pair_sub).ok();
let render = Submenu::new("Render", true);
render.append(&MenuItem::with_id("live", "Live", true, None)).ok();
render.append(&MenuItem::with_id("editor", "Editor", true, None)).ok();
render.append(&MenuItem::with_id("view", "View", true, None)).ok();
render.append(&PredefinedMenuItem::separator()).ok();
render.append(&MenuItem::with_id("eval", "Evaluate", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Enter)))).ok();
render.append(&MenuItem::with_id("eval", "Evaluate", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyE)))).ok();
let view = Submenu::new("View", true);
view.append(&MenuItem::with_id("zoom_in", "Zoom In", true, Some(Accelerator::new(Some(Modifiers::CONTROL), Code::Equal)))).ok();
@ -78,7 +105,13 @@ impl AppMenu {
menu.append(&render).ok();
menu.append(&view).ok();
Self { menu }
Self { menu, auto_pair_items }
}
pub fn set_auto_pair_check(&self, bit: u32, checked: bool) {
for (b, item) in &self.auto_pair_items {
if *b == bit { item.set_checked(checked); }
}
}
pub fn poll() -> Option<MenuAction> {
@ -104,6 +137,12 @@ impl AppMenu {
"find" => Some(MenuAction::Find),
"settings" => Some(MenuAction::Settings),
"export_crate" => Some(MenuAction::ExportCrate),
"ap_paren" => Some(MenuAction::ToggleAutoPair(AP_PAREN)),
"ap_bracket" => Some(MenuAction::ToggleAutoPair(AP_BRACKET)),
"ap_brace" => Some(MenuAction::ToggleAutoPair(AP_BRACE)),
"ap_single" => Some(MenuAction::ToggleAutoPair(AP_SINGLE)),
"ap_double" => Some(MenuAction::ToggleAutoPair(AP_DOUBLE)),
"ap_backtick" => Some(MenuAction::ToggleAutoPair(AP_BACKTICK)),
_ => None,
}
})