diff --git a/Cargo.lock b/Cargo.lock index a83c23c..e3716f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -560,8 +560,10 @@ dependencies = [ "embers-protocol", "embers-server", "embers-test-support", + "filetime", "libc", "predicates", + "serde_json", "tempfile", "tokio", "tracing", @@ -634,10 +636,12 @@ dependencies = [ "embers-core", "embers-protocol", "embers-server", + "fastrand", "libc", "portable-pty", "tempfile", "tokio", + "tracing", ] [[package]] @@ -673,6 +677,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "flatbuffers" version = "25.12.19" @@ -898,7 +913,10 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ + "bitflags 2.11.0", "libc", + "plain", + "redox_syscall 0.7.4", ] [[package]] @@ -1053,7 +1071,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -1118,6 +1136,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plotters" version = "0.3.7" @@ -1358,6 +1382,15 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "redox_users" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 79f1637..dda4866 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ base64 = "0.22" clap = { version = "4.5", features = ["derive"] } criterion = "0.7" directories = "6" +fastrand = "2" flatbuffers = "=25.12.19" libc = "0.2" portable-pty = "0.9" @@ -31,6 +32,7 @@ rhai = "1" rhai-autodocs = "0.11" serde = { version = "1", features = ["derive"] } serde_json = "1" +shell-words = "1" tempfile = "3" thiserror = "2" tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] } diff --git a/crates/embers-cli/Cargo.toml b/crates/embers-cli/Cargo.toml index 815c323..3a7fa1b 100644 --- a/crates/embers-cli/Cargo.toml +++ b/crates/embers-cli/Cargo.toml @@ -25,6 +25,7 @@ embers-core = { path = "../embers-core" } embers-protocol = { path = "../embers-protocol" } embers-server = { path = "../embers-server" } libc.workspace = true +serde_json.workspace = true tokio.workspace = true tracing.workspace = true unicode-width.workspace = true @@ -32,6 +33,7 @@ unicode-width.workspace = true [dev-dependencies] assert_cmd.workspace = true embers-test-support = { path = "../embers-test-support" } +filetime = "0.2" predicates.workspace = true tempfile.workspace = true diff --git a/crates/embers-cli/src/interactive.rs b/crates/embers-cli/src/interactive.rs index 05e49d2..49278ab 100644 --- a/crates/embers-cli/src/interactive.rs +++ b/crates/embers-cli/src/interactive.rs @@ -1,3 +1,4 @@ +use std::fs; use std::io::{self, Write}; use std::os::fd::AsRawFd; use std::path::{Path, PathBuf}; @@ -17,6 +18,7 @@ const DEFAULT_SESSION_NAME: &str = "main"; const KEY_SEQUENCE_TIMEOUT: Duration = Duration::from_millis(15); const KEY_SEQUENCE_CONTINUATION_TIMEOUT: Duration = Duration::from_millis(2); const EVENT_POLL_INTERVAL: Duration = Duration::from_millis(20); +const CONFIG_WATCH_POLL_INTERVAL: Duration = Duration::from_millis(250); const BRACKETED_PASTE_END: &[u8] = b"\x1b[201~"; const TERMINAL_ENTER_BASE_SEQUENCE: &str = "\x1b[?1049h\x1b[?1004h\x1b[?2004h\x1b[?25l\x1b[2J\x1b[H"; @@ -37,11 +39,13 @@ pub async fn run( let mut session_id = Some(initial_session_id); let config = ConfigManager::from_process(config_path) .map_err(|error| MuxError::invalid_input(error.to_string()))?; + let watched_config_path = config.active_source().path.clone(); let mut configured = ConfiguredClient::new(client, config); let mut terminal = TerminalGuard::enter(mouse_capture_enabled(&configured))?; let (input_tx, mut input_rx) = mpsc::unbounded_channel(); - let _input_thread = spawn_input_thread(input_tx)?; + let _input_thread = spawn_input_thread(input_tx.clone())?; + let _config_thread = spawn_config_thread(watched_config_path, input_tx)?; let mut terminal_size = terminal.size()?; let mut dirty = true; @@ -117,6 +121,16 @@ pub async fn run( dirty = true; } } + Ok(TerminalEvent::ConfigChanged) => match configured.reload_config_if_changed() { + Ok(true) => { + terminal.write_bytes(&drain_terminal_output(&mut configured))?; + dirty = true; + } + Ok(false) => {} + Err(_) => { + dirty = true; + } + }, Ok(TerminalEvent::InputClosed) => return Ok(()), Ok(TerminalEvent::InputError(message)) => { return Err(MuxError::transport(message)); @@ -133,9 +147,11 @@ pub async fn run( continue; } - match tokio::time::timeout(EVENT_POLL_INTERVAL, configured.process_next_event()).await { - Ok(result) => { - let event = result?; + match configured + .process_next_event_timeout(EVENT_POLL_INTERVAL) + .await? + { + Some(event) => { match switched_session_id(&event, attached_client_id) { SwitchedSession::Switched(next_session_id) => { ensure_root_window(configured.client_mut(), next_session_id).await?; @@ -149,7 +165,7 @@ pub async fn run( terminal.write_bytes(&drain_terminal_output(&mut configured))?; dirty = true; } - Err(_) => { + None => { continue; } } @@ -388,6 +404,7 @@ enum TerminalEvent { Mouse(MouseEvent), Paste(Vec), Focus(bool), + ConfigChanged, InputClosed, InputError(String), } @@ -424,6 +441,59 @@ fn spawn_input_thread( .map_err(|error| MuxError::internal(format!("failed to spawn input thread: {error}"))) } +fn spawn_config_thread( + config_path: Option, + tx: mpsc::UnboundedSender, +) -> Result>> { + let Some(config_path) = config_path else { + return Ok(None); + }; + + let handle = thread::Builder::new() + .name("embers-config".to_owned()) + .spawn(move || { + let mut last_modified = config_modified(&config_path); + let mut pending_change = None; + let mut missing_polls = 0usize; + loop { + thread::sleep(CONFIG_WATCH_POLL_INTERVAL); + let observed_modified = config_modified(&config_path); + if observed_modified.is_none() && last_modified.is_some() { + missing_polls = missing_polls.saturating_add(1); + if missing_polls < 2 { + continue; + } + } else { + missing_polls = 0; + } + + if observed_modified == last_modified { + pending_change = None; + continue; + } + + if pending_change == Some(observed_modified) { + last_modified = observed_modified; + pending_change = None; + if tx.send(TerminalEvent::ConfigChanged).is_err() { + break; + } + } else { + pending_change = Some(observed_modified); + } + } + }) + .map_err(|error| MuxError::internal(format!("failed to spawn config thread: {error}")))?; + + Ok(Some(handle)) +} + +fn config_modified(path: &Path) -> Option { + fs::metadata(path) + .and_then(|metadata| metadata.modified()) + .ok() +} + fn read_terminal_event(fd: libc::c_int) -> Result> { let Some(first) = read_byte(fd)? else { return Ok(None); @@ -576,10 +646,7 @@ fn mouse_button(code: u16) -> Option { fn read_bracketed_paste(fd: libc::c_int) -> Result> { let mut bytes = Vec::new(); - loop { - let Some(next) = read_byte(fd)? else { - break; - }; + while let Some(next) = read_byte(fd)? { bytes.push(next); if bytes.ends_with(BRACKETED_PASTE_END) { let new_len = bytes.len() - BRACKETED_PASTE_END.len(); diff --git a/crates/embers-cli/src/lib.rs b/crates/embers-cli/src/lib.rs index a8cea15..69de9ce 100644 --- a/crates/embers-cli/src/lib.rs +++ b/crates/embers-cli/src/lib.rs @@ -20,9 +20,11 @@ use embers_core::{ new_request_id, }; use embers_protocol::{ - BufferRequest, BufferResponse, ClientMessage, ClientRecord, ClientRequest, FloatingRecord, - FloatingRequest, FloatingResponse, NodeRequest, PingRequest, ProtocolClient, ServerResponse, - SessionRecord, SessionRequest, SessionSnapshot, SnapshotResponse, + BufferHistoryPlacement, BufferHistoryScope, BufferLocation, BufferLocationAttachment, + BufferLocationResponse, BufferRequest, BufferResponse, ClientMessage, ClientRecord, + ClientRequest, FloatingRecord, FloatingRequest, FloatingResponse, NodeBreakDestination, + NodeJoinPlacement, NodeRequest, PingRequest, ProtocolClient, ServerResponse, SessionRecord, + SessionRequest, SessionSnapshot, SnapshotResponse, }; use embers_server::{SOCKET_ENV_VAR, Server, ServerConfig}; use tokio::time::{Duration, sleep}; @@ -143,6 +145,14 @@ pub enum Command { #[arg(short = 't', long = "target")] target: String, }, + Buffer { + #[command(subcommand)] + command: BufferCommand, + }, + Node { + #[command(subcommand)] + command: NodeCommand, + }, #[command(name = "new-window")] NewWindow { #[arg(short = 't', long = "target")] @@ -243,6 +253,94 @@ pub enum Command { }, } +#[derive(Debug, Subcommand)] +pub enum BufferCommand { + Show { + buffer_id: u64, + }, + Reveal { + buffer_id: u64, + #[arg(long)] + client: Option, + }, + History { + buffer_id: u64, + #[arg(long, default_value = "full")] + scope: HistoryScopeArg, + #[arg(long, default_value = "tab")] + placement: HistoryPlacementArg, + #[arg(long)] + client: Option, + }, +} + +#[derive(Debug, Subcommand)] +pub enum NodeCommand { + Zoom { + node_id: u64, + }, + Unzoom { + #[arg(short = 't', long = "target")] + target: Option, + }, + ToggleZoom { + node_id: u64, + }, + Swap { + first_node_id: u64, + second_node_id: u64, + }, + Break { + node_id: u64, + #[arg(long = "to")] + destination: BreakDestinationArg, + }, + JoinBuffer { + node_id: u64, + buffer_id: u64, + #[arg(long = "as", default_value = "tab-after")] + placement: JoinPlacementArg, + }, + MoveBefore { + node_id: u64, + sibling_id: u64, + }, + MoveAfter { + node_id: u64, + sibling_id: u64, + }, +} + +#[derive(Clone, Copy, Debug, clap::ValueEnum)] +pub enum HistoryScopeArg { + Full, + Visible, +} + +#[derive(Clone, Copy, Debug, clap::ValueEnum)] +pub enum HistoryPlacementArg { + Tab, + Floating, +} + +#[derive(Clone, Copy, Debug, clap::ValueEnum)] +pub enum BreakDestinationArg { + Tab, + Floating, +} + +#[derive(Clone, Copy, Debug, clap::ValueEnum)] +pub enum JoinPlacementArg { + Left, + Right, + Up, + Down, + #[value(name = "tab-before")] + TabBefore, + #[value(name = "tab-after")] + TabAfter, +} + async fn execute(socket: &Path, command: Command) -> Result { let mut connection = CliConnection::connect(socket).await?; @@ -377,6 +475,177 @@ async fn execute(socket: &Path, command: Command) -> Result { ))), } } + Command::Buffer { command } => match command { + BufferCommand::Show { buffer_id } => { + let requested_buffer_id = BufferId(buffer_id); + let response = connection + .request(ClientMessage::Buffer(BufferRequest::Inspect { + request_id: new_request_id(), + buffer_id: requested_buffer_id, + })) + .await?; + let (buffer, location, _) = expect_buffer_with_location(response, "buffer show")?; + ensure_matching_buffer_id("buffer show", requested_buffer_id, buffer.id)?; + Ok(format_buffer_details(&buffer, &location)) + } + BufferCommand::Reveal { buffer_id, client } => { + let requested_buffer_id = BufferId(buffer_id); + let response = connection + .request(ClientMessage::Buffer(BufferRequest::Reveal { + request_id: new_request_id(), + buffer_id: requested_buffer_id, + client_id: client, + })) + .await?; + let location = expect_buffer_location(response, "buffer reveal")?; + ensure_matching_buffer_id( + "buffer reveal", + requested_buffer_id, + location.buffer_id, + )?; + if matches!(location.attachment, BufferLocationAttachment::Detached) { + return Err(MuxError::conflict(format!( + "buffer {} is detached; use attach-buffer or node join-buffer", + buffer_id + ))); + } + Ok(format_buffer_location_line(&location)) + } + BufferCommand::History { + buffer_id, + scope, + placement, + client, + } => { + let requested_scope = history_scope(scope); + let requested_placement = history_placement(placement); + let response = connection + .request(ClientMessage::Buffer(BufferRequest::OpenHistory { + request_id: new_request_id(), + buffer_id: BufferId(buffer_id), + scope: requested_scope, + placement: requested_placement, + client_id: client, + })) + .await?; + let (buffer, location, at_root_tab) = + expect_buffer_with_location(response, "buffer history")?; + ensure_history_response( + BufferId(buffer_id), + requested_scope, + requested_placement, + &buffer, + &location, + at_root_tab, + )?; + Ok(format_buffer_location_line(&location)) + } + }, + Command::Node { command } => match command { + NodeCommand::Zoom { node_id } => { + let response = connection + .request(ClientMessage::Node(NodeRequest::Zoom { + request_id: new_request_id(), + node_id: NodeId(node_id), + })) + .await?; + expect_ok(response, "NodeCommand::Zoom")?; + Ok(String::new()) + } + NodeCommand::Unzoom { target } => { + let session = connection.resolve_session_record(target.as_deref()).await?; + let response = connection + .request(ClientMessage::Node(NodeRequest::Unzoom { + request_id: new_request_id(), + session_id: session.id, + })) + .await?; + expect_ok(response, "NodeCommand::Unzoom")?; + Ok(String::new()) + } + NodeCommand::ToggleZoom { node_id } => { + let response = connection + .request(ClientMessage::Node(NodeRequest::ToggleZoom { + request_id: new_request_id(), + node_id: NodeId(node_id), + })) + .await?; + expect_ok(response, "NodeCommand::ToggleZoom")?; + Ok(String::new()) + } + NodeCommand::Swap { + first_node_id, + second_node_id, + } => { + let response = connection + .request(ClientMessage::Node(NodeRequest::SwapSiblings { + request_id: new_request_id(), + first_node_id: NodeId(first_node_id), + second_node_id: NodeId(second_node_id), + })) + .await?; + expect_ok(response, "NodeCommand::Swap")?; + Ok(String::new()) + } + NodeCommand::Break { + node_id, + destination, + } => { + let response = connection + .request(ClientMessage::Node(NodeRequest::BreakNode { + request_id: new_request_id(), + node_id: NodeId(node_id), + destination: break_destination(destination), + })) + .await?; + expect_ok(response, "NodeCommand::Break")?; + Ok(String::new()) + } + NodeCommand::JoinBuffer { + node_id, + buffer_id, + placement, + } => { + let response = connection + .request(ClientMessage::Node(NodeRequest::JoinBufferAtNode { + request_id: new_request_id(), + node_id: NodeId(node_id), + buffer_id: BufferId(buffer_id), + placement: join_placement(placement), + })) + .await?; + expect_ok(response, "NodeCommand::JoinBuffer")?; + Ok(String::new()) + } + NodeCommand::MoveBefore { + node_id, + sibling_id, + } => { + let response = connection + .request(ClientMessage::Node(NodeRequest::MoveNodeBefore { + request_id: new_request_id(), + node_id: NodeId(node_id), + sibling_node_id: NodeId(sibling_id), + })) + .await?; + expect_ok(response, "NodeCommand::MoveBefore")?; + Ok(String::new()) + } + NodeCommand::MoveAfter { + node_id, + sibling_id, + } => { + let response = connection + .request(ClientMessage::Node(NodeRequest::MoveNodeAfter { + request_id: new_request_id(), + node_id: NodeId(node_id), + sibling_node_id: NodeId(sibling_id), + })) + .await?; + expect_ok(response, "NodeCommand::MoveAfter")?; + Ok(String::new()) + } + }, Command::NewWindow { target, title, @@ -1327,6 +1596,59 @@ fn expect_capture(response: ServerResponse, operation: &str) -> Result Result { + match response { + ServerResponse::BufferLocation(BufferLocationResponse { location, .. }) => Ok(location), + other => Err(MuxError::protocol(format!( + "unexpected response to {operation}: {other:?}" + ))), + } +} + +fn expect_ok(response: ServerResponse, operation: &str) -> Result<()> { + match response { + ServerResponse::Ok(_) => Ok(()), + other => Err(MuxError::protocol(format!( + "unexpected response to {operation}: {other:?}" + ))), + } +} + +fn expect_buffer_with_location( + response: ServerResponse, + operation: &str, +) -> Result<(embers_protocol::BufferRecord, BufferLocation, bool)> { + match response { + ServerResponse::BufferWithLocation(response) => { + let (_, buffer, location, at_root_tab) = response.into_parts(); + if buffer.id != location.buffer_id { + return Err(MuxError::protocol(format!( + "{operation} returned buffer {} but location was for buffer {}", + buffer.id, location.buffer_id + ))); + } + Ok((buffer, location, at_root_tab)) + } + other => Err(MuxError::protocol(format!( + "unexpected response to {operation}: {other:?}" + ))), + } +} + +fn ensure_matching_buffer_id( + operation: &str, + requested_buffer_id: BufferId, + actual_buffer_id: BufferId, +) -> Result<()> { + if actual_buffer_id == requested_buffer_id { + return Ok(()); + } + + Err(MuxError::protocol(format!( + "{operation} returned buffer {actual_buffer_id} for requested buffer {requested_buffer_id}" + ))) +} + fn format_sessions(sessions: &[SessionRecord]) -> String { sessions .iter() @@ -1381,6 +1703,82 @@ fn format_clients(clients: &[ClientRecord], sessions: &[SessionRecord]) -> Strin .join("\n") } +fn format_buffer_details( + buffer: &embers_protocol::BufferRecord, + location: &BufferLocation, +) -> String { + let mut lines = vec![ + format!("id\t{}", buffer.id), + format!( + "title\t{}", + serde_json::to_string(&buffer.title).expect("buffer titles serialize to JSON") + ), + format!("state\t{}", buffer_state_label(buffer.state)), + format!("kind\t{}", buffer_kind_label(buffer.kind)), + format!("read_only\t{}", usize::from(buffer.read_only)), + format!("location\t{}", format_buffer_location_inline(location)), + ]; + if let Some(source_buffer_id) = buffer.helper_source_buffer_id { + lines.push(format!("source_buffer\t{}", source_buffer_id)); + } + if let Some(scope) = buffer.helper_scope { + lines.push(format!("history_scope\t{}", history_scope_label(scope))); + } + if !buffer.command.is_empty() { + let serialized_args = + serde_json::to_string(&buffer.command).expect("buffer commands serialize to JSON"); + lines.push(format!("command\t{serialized_args}")); + } + if let Some(cwd) = &buffer.cwd { + let serialized_cwd = + serde_json::to_string(cwd).expect("buffer working directories serialize to JSON"); + lines.push(format!("cwd\t{serialized_cwd}")); + } + lines.join("\n") +} + +fn format_buffer_location_line(location: &BufferLocation) -> String { + format!( + "{}\t{}", + location.buffer_id, + format_buffer_location_value(location) + ) +} + +fn format_buffer_location_inline(location: &BufferLocation) -> String { + match location.attachment { + BufferLocationAttachment::Floating { + session_id, + node_id, + floating_id, + } => { + format!("session:{session_id} node:{node_id} floating:{floating_id}") + } + BufferLocationAttachment::Session { + session_id, + node_id, + } => format!("session:{session_id} node:{node_id}"), + BufferLocationAttachment::Detached => "detached".to_owned(), + } +} + +fn format_buffer_location_value(location: &BufferLocation) -> String { + match location.attachment { + BufferLocationAttachment::Floating { + session_id, + node_id, + floating_id, + } => { + format!("session:{session_id}\tnode:{node_id}\tfloating:{floating_id}") + } + BufferLocationAttachment::Session { + session_id, + node_id, + } => format!("session:{session_id}\tnode:{node_id}"), + BufferLocationAttachment::Detached => "detached".to_owned(), + } +} + fn session_label(sessions: &[SessionRecord], session_id: SessionId) -> String { sessions .iter() @@ -1630,6 +2028,136 @@ fn buffer_state_label(state: embers_protocol::BufferRecordState) -> &'static str } } +fn buffer_kind_label(kind: embers_protocol::BufferRecordKind) -> &'static str { + match kind { + embers_protocol::BufferRecordKind::Pty => "pty", + embers_protocol::BufferRecordKind::Helper => "helper", + } +} + +fn history_scope_label(scope: BufferHistoryScope) -> &'static str { + match scope { + BufferHistoryScope::Full => "full", + BufferHistoryScope::Visible => "visible", + } +} + +fn history_scope(scope: HistoryScopeArg) -> BufferHistoryScope { + match scope { + HistoryScopeArg::Full => BufferHistoryScope::Full, + HistoryScopeArg::Visible => BufferHistoryScope::Visible, + } +} + +fn history_placement(placement: HistoryPlacementArg) -> BufferHistoryPlacement { + match placement { + HistoryPlacementArg::Tab => BufferHistoryPlacement::Tab, + HistoryPlacementArg::Floating => BufferHistoryPlacement::Floating, + } +} + +fn ensure_history_attachment( + buffer_id: BufferId, + requested_placement: BufferHistoryPlacement, + location: &BufferLocation, +) -> Result<()> { + match (requested_placement, &location.attachment) { + (BufferHistoryPlacement::Tab, BufferLocationAttachment::Session { .. }) + | (BufferHistoryPlacement::Floating, BufferLocationAttachment::Floating { .. }) => Ok(()), + (_, BufferLocationAttachment::Detached) => Err(MuxError::protocol(format!( + "buffer history returned detached helper location for buffer {buffer_id}" + ))), + (BufferHistoryPlacement::Tab, BufferLocationAttachment::Floating { .. }) => { + Err(MuxError::protocol(format!( + "buffer history returned unexpected attachment for buffer {buffer_id}: expected Tab" + ))) + } + (BufferHistoryPlacement::Floating, BufferLocationAttachment::Session { .. }) => { + Err(MuxError::protocol(format!( + "buffer history returned unexpected attachment for buffer {buffer_id}: expected Floating" + ))) + } + } +} + +fn ensure_history_response( + source_buffer_id: BufferId, + requested_scope: BufferHistoryScope, + requested_placement: BufferHistoryPlacement, + buffer: &embers_protocol::BufferRecord, + location: &BufferLocation, + at_root_tab: bool, +) -> Result<()> { + if buffer.kind != embers_protocol::BufferRecordKind::Helper { + return Err(MuxError::protocol(format!( + "buffer history returned buffer {} with unexpected kind {:?}", + buffer.id, buffer.kind + ))); + } + if buffer.helper_source_buffer_id != Some(source_buffer_id) { + return Err(MuxError::protocol(format!( + "buffer history returned helper {} for source {:?} instead of {source_buffer_id}", + buffer.id, buffer.helper_source_buffer_id + ))); + } + if buffer.helper_scope != Some(requested_scope) { + return Err(MuxError::protocol(format!( + "buffer history returned helper {} with scope {:?} instead of {:?}", + buffer.id, buffer.helper_scope, requested_scope + ))); + } + + let (session_id, node_id) = match &location.attachment { + BufferLocationAttachment::Session { + session_id, + node_id, + } + | BufferLocationAttachment::Floating { + session_id, + node_id, + .. + } => (*session_id, *node_id), + BufferLocationAttachment::Detached => { + ensure_history_attachment(source_buffer_id, requested_placement, location)?; + unreachable!("detached buffer history attachments should fail validation") + } + }; + if buffer.attachment_node_id != Some(node_id) { + return Err(MuxError::protocol(format!( + "buffer history returned helper {} attached to {:?} but location pointed at {node_id}", + buffer.id, buffer.attachment_node_id + ))); + } + + ensure_history_attachment(source_buffer_id, requested_placement, location)?; + + if matches!(requested_placement, BufferHistoryPlacement::Tab) && !at_root_tab { + return Err(MuxError::protocol(format!( + "buffer history returned helper node {node_id} outside session {session_id} root tabs" + ))); + } + + Ok(()) +} + +fn break_destination(destination: BreakDestinationArg) -> NodeBreakDestination { + match destination { + BreakDestinationArg::Tab => NodeBreakDestination::Tab, + BreakDestinationArg::Floating => NodeBreakDestination::Floating, + } +} + +fn join_placement(placement: JoinPlacementArg) -> NodeJoinPlacement { + match placement { + JoinPlacementArg::Left => NodeJoinPlacement::Left, + JoinPlacementArg::Right => NodeJoinPlacement::Right, + JoinPlacementArg::Up => NodeJoinPlacement::Up, + JoinPlacementArg::Down => NodeJoinPlacement::Down, + JoinPlacementArg::TabBefore => NodeJoinPlacement::TabBefore, + JoinPlacementArg::TabAfter => NodeJoinPlacement::TabAfter, + } +} + fn split_scoped_required(target: &str, label: &str) -> Result<(Option, String)> { let (session, selector) = split_scoped_target(Some(target)); let selector = @@ -1673,9 +2201,11 @@ fn default_title(command: &[String], fallback: &str) -> String { mod tests { #[cfg(windows)] use base64::Engine as _; - use clap::Parser; - use embers_core::NodeId; - use embers_protocol::{TabRecord, TabsRecord}; + use clap::{Parser, error::ErrorKind}; + use embers_core::{ActivityState, BufferId, FloatingId, NodeId, PtySize, SessionId}; + use embers_protocol::{ + BufferLocation, BufferRecord, BufferRecordKind, BufferRecordState, TabRecord, TabsRecord, + }; #[cfg(windows)] use std::ffi::OsString; #[cfg(unix)] @@ -1686,7 +2216,11 @@ mod tests { use std::os::windows::ffi::OsStringExt; use std::path::Path; - use super::{Cli, resolve_window_index, split_scoped_required, split_scoped_target}; + use super::{ + BreakDestinationArg, BufferCommand, Cli, Command, HistoryPlacementArg, HistoryScopeArg, + JoinPlacementArg, NodeCommand, ensure_history_attachment, format_buffer_details, + resolve_window_index, split_scoped_required, split_scoped_target, + }; #[test] fn parser_accepts_global_socket_after_subcommand() { @@ -1804,4 +2338,323 @@ mod tests { 0 ); } + + #[test] + fn buffer_details_location_row_uses_single_tab_delimiter() { + let details = format_buffer_details( + &BufferRecord { + id: BufferId(7), + title: "logs".to_owned(), + command: vec!["/bin/sh".to_owned()], + cwd: Some("/tmp".to_owned()), + kind: BufferRecordKind::Pty, + pid: None, + env: Default::default(), + state: BufferRecordState::Running, + attachment_node_id: Some(NodeId(3)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + }, + &BufferLocation::session(BufferId(7), SessionId(1), NodeId(3)), + ); + + let location_line = details + .lines() + .find(|line| line.starts_with("location\t")) + .expect("location row present"); + assert_eq!(location_line.matches('\t').count(), 1); + } + + #[test] + fn buffer_details_command_row_preserves_argument_boundaries() { + let details = format_buffer_details( + &BufferRecord { + id: BufferId(8), + title: "script".to_owned(), + command: vec![ + "/bin/sh".to_owned(), + "-lc".to_owned(), + "printf '%s\\n' 'hello world'".to_owned(), + ], + cwd: None, + kind: BufferRecordKind::Pty, + pid: None, + env: Default::default(), + state: BufferRecordState::Running, + attachment_node_id: Some(NodeId(4)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + }, + &BufferLocation::session(BufferId(8), SessionId(1), NodeId(4)), + ); + + let command_line = details + .lines() + .find(|line| line.starts_with("command\t")) + .expect("command row present"); + let serialized_args = command_line + .strip_prefix("command\t") + .expect("command row prefix present"); + let command: Vec = + serde_json::from_str(serialized_args).expect("command row uses JSON"); + assert_eq!( + command, + vec![ + "/bin/sh".to_owned(), + "-lc".to_owned(), + "printf '%s\\n' 'hello world'".to_owned(), + ] + ); + } + + #[test] + fn buffer_details_title_and_cwd_rows_use_json() { + let details = format_buffer_details( + &BufferRecord { + id: BufferId(9), + title: "build\tlogs\nstderr".to_owned(), + command: Vec::new(), + cwd: Some("/tmp/work\tspace\nhere".to_owned()), + kind: BufferRecordKind::Pty, + pid: None, + env: Default::default(), + state: BufferRecordState::Running, + attachment_node_id: Some(NodeId(5)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + }, + &BufferLocation::session(BufferId(9), SessionId(1), NodeId(5)), + ); + + let title_line = details + .lines() + .find(|line| line.starts_with("title\t")) + .expect("title row present"); + let cwd_line = details + .lines() + .find(|line| line.starts_with("cwd\t")) + .expect("cwd row present"); + + let title = title_line + .strip_prefix("title\t") + .expect("title row prefix present"); + let cwd = cwd_line + .strip_prefix("cwd\t") + .expect("cwd row prefix present"); + + assert_eq!( + serde_json::from_str::(title).expect("title row uses JSON"), + "build\tlogs\nstderr" + ); + assert_eq!( + serde_json::from_str::(cwd).expect("cwd row uses JSON"), + "/tmp/work\tspace\nhere" + ); + } + + #[test] + fn buffer_subcommands_parse_expected_flags_and_defaults() { + let history = + Cli::try_parse_from(["embers", "buffer", "history", "7"]).expect("history parses"); + match history.command { + Some(Command::Buffer { + command: + BufferCommand::History { + buffer_id, + scope, + placement, + client, + }, + }) => { + assert_eq!(buffer_id, 7); + assert!(matches!(scope, HistoryScopeArg::Full)); + assert!(matches!(placement, HistoryPlacementArg::Tab)); + assert_eq!(client, None); + } + other => panic!("expected buffer history command, got {other:?}"), + } + + let history_with_flags = Cli::try_parse_from([ + "embers", + "buffer", + "history", + "9", + "--scope", + "visible", + "--placement", + "floating", + "--client", + "5", + ]) + .expect("history flags parse"); + match history_with_flags.command { + Some(Command::Buffer { + command: + BufferCommand::History { + buffer_id, + scope, + placement, + client, + }, + }) => { + assert_eq!(buffer_id, 9); + assert!(matches!(scope, HistoryScopeArg::Visible)); + assert!(matches!(placement, HistoryPlacementArg::Floating)); + assert_eq!(client.map(std::num::NonZeroU64::get), Some(5)); + } + other => panic!("expected flagged buffer history command, got {other:?}"), + } + + let reveal = Cli::try_parse_from(["embers", "buffer", "reveal", "11", "--client", "6"]) + .expect("reveal parses"); + match reveal.command { + Some(Command::Buffer { + command: BufferCommand::Reveal { buffer_id, client }, + }) => { + assert_eq!(buffer_id, 11); + assert_eq!(client.map(std::num::NonZeroU64::get), Some(6)); + } + other => panic!("expected buffer reveal command, got {other:?}"), + } + } + + #[test] + fn history_attachment_validation_accepts_matching_locations() { + assert!( + ensure_history_attachment( + BufferId(7), + embers_protocol::BufferHistoryPlacement::Tab, + &BufferLocation::session(BufferId(70), SessionId(1), NodeId(3)), + ) + .is_ok() + ); + assert!( + ensure_history_attachment( + BufferId(7), + embers_protocol::BufferHistoryPlacement::Floating, + &BufferLocation::floating(BufferId(71), SessionId(1), NodeId(4), FloatingId(5)), + ) + .is_ok() + ); + } + + #[test] + fn history_attachment_validation_rejects_mismatched_locations() { + let floating_error = ensure_history_attachment( + BufferId(7), + embers_protocol::BufferHistoryPlacement::Floating, + &BufferLocation::session(BufferId(70), SessionId(1), NodeId(3)), + ) + .expect_err("floating history should reject tab helper"); + assert!(floating_error.to_string().contains("expected Floating")); + + let tab_error = ensure_history_attachment( + BufferId(7), + embers_protocol::BufferHistoryPlacement::Tab, + &BufferLocation::floating(BufferId(71), SessionId(1), NodeId(4), FloatingId(5)), + ) + .expect_err("tab history should reject floating helper"); + assert!(tab_error.to_string().contains("expected Tab")); + } + + #[test] + fn node_subcommands_parse_expected_flags_and_reject_legacy_shortcuts() { + let break_to_floating = + Cli::try_parse_from(["embers", "node", "break", "11", "--to", "floating"]) + .expect("break parses"); + match break_to_floating.command { + Some(Command::Node { + command: + NodeCommand::Break { + node_id, + destination, + }, + }) => { + assert_eq!(node_id, 11); + assert!(matches!(destination, BreakDestinationArg::Floating)); + } + other => panic!("expected node break command, got {other:?}"), + } + + let join_after = Cli::try_parse_from([ + "embers", + "node", + "join-buffer", + "21", + "34", + "--as", + "tab-after", + ]) + .expect("join-buffer tab-after parses"); + match join_after.command { + Some(Command::Node { + command: + NodeCommand::JoinBuffer { + node_id, + buffer_id, + placement, + }, + }) => { + assert_eq!(node_id, 21); + assert_eq!(buffer_id, 34); + assert!(matches!(placement, JoinPlacementArg::TabAfter)); + } + other => panic!("expected node join-buffer command, got {other:?}"), + } + + let join_default = Cli::try_parse_from(["embers", "node", "join-buffer", "21", "34"]) + .expect("join-buffer default parses"); + match join_default.command { + Some(Command::Node { + command: NodeCommand::JoinBuffer { placement, .. }, + }) => { + assert!(matches!(placement, JoinPlacementArg::TabAfter)); + } + other => panic!("expected default node join-buffer command, got {other:?}"), + } + + let join_before = Cli::try_parse_from([ + "embers", + "node", + "join-buffer", + "21", + "34", + "--as", + "tab-before", + ]) + .expect("join-buffer tab-before parses"); + match join_before.command { + Some(Command::Node { + command: NodeCommand::JoinBuffer { placement, .. }, + }) => { + assert!(matches!(placement, JoinPlacementArg::TabBefore)); + } + other => panic!("expected node join-buffer command, got {other:?}"), + } + + let legacy_tab_after = + Cli::try_parse_from(["embers", "node", "join-buffer", "21", "34", "--tab-after"]) + .expect_err("legacy --tab-after should be rejected"); + assert_eq!(legacy_tab_after.kind(), ErrorKind::UnknownArgument); + + let legacy_tab_before = + Cli::try_parse_from(["embers", "node", "join-buffer", "21", "34", "--tab-before"]) + .expect_err("legacy --tab-before should be rejected"); + assert_eq!(legacy_tab_before.kind(), ErrorKind::UnknownArgument); + } } diff --git a/crates/embers-cli/tests/interactive.rs b/crates/embers-cli/tests/interactive.rs index 0577d3b..eb34df8 100644 --- a/crates/embers-cli/tests/interactive.rs +++ b/crates/embers-cli/tests/interactive.rs @@ -1,12 +1,25 @@ +#[cfg(target_os = "macos")] +use std::ffi::CStr; +use std::ffi::{OsStr, OsString}; use std::fs; -use std::path::Path; +#[cfg(target_os = "macos")] +use std::os::unix::ffi::OsStrExt; +#[cfg(unix)] +use std::os::unix::ffi::OsStringExt; +use std::path::{Path, PathBuf}; use std::time::Duration; -use embers_core::PtySize; -use embers_test_support::{PtyHarness, TestServer, acquire_test_lock, cargo_bin, cargo_bin_path}; -use tempfile::tempdir; +use embers_core::{ActivityState, BufferId, NodeId, PtySize, new_request_id}; +use embers_protocol::{ + BufferRequest, ClientMessage, InputRequest, ServerResponse, SessionRequest, SessionSnapshot, + VisibleSnapshotResponse, +}; +use embers_test_support::{ + PtyHarness, TestConnection, TestServer, acquire_test_lock, cargo_bin, cargo_bin_path, +}; +use filetime::FileTime; -use crate::support::{run_cli, stdout}; +use crate::support::{require_pty, run_cli, session_snapshot_by_name, stdout}; const STARTUP_TIMEOUT: Duration = Duration::from_secs(15); const IO_TIMEOUT: Duration = Duration::from_secs(30); @@ -16,9 +29,249 @@ const SCROLLBACK_SETTLE_DELAY: Duration = Duration::from_millis(750); const QUIET_TIMEOUT: Duration = Duration::from_millis(500); const PAGE_UP_ATTEMPTS: usize = 4; -fn spawn_embers(args: &[&str]) -> PtyHarness { +/// A guard that owns the spawned embers process and ensures cleanup +/// of orphaned __serve processes when dropped. +struct SpawnedEmbers { + socket_path: PathBuf, + started_server: bool, +} + +impl SpawnedEmbers { + fn new(socket_path: PathBuf, started_server: bool) -> Self { + Self { + socket_path, + started_server, + } + } +} + +impl Drop for SpawnedEmbers { + fn drop(&mut self) { + if !self.started_server { + return; + } + // Kill any orphaned __serve process for our socket + kill_orphaned_server(&self.socket_path); + } +} + +/// Kill any orphaned embers __serve process for the given socket. +/// This is safe to call from Drop or any synchronous context. +fn kill_orphaned_server(socket_path: &Path) { + let pid_path = socket_path.with_extension("pid"); + + // A clean SIGTERM path returns here; failed waits fall through to the SIGKILL retry below. + let matched_pid = try_signal_server(&pid_path, socket_path, libc::SIGTERM); + if let Some(matched_pid) = matched_pid + && wait_for_server_exit(matched_pid, socket_path) + { + if read_pid(&pid_path) == Some(matched_pid) { + let _ = fs::remove_file(&pid_path); + } + return; + } + + // Wait briefly for graceful shutdown + std::thread::sleep(Duration::from_millis(50)); + + let matched_pid = try_signal_server(&pid_path, socket_path, libc::SIGKILL); + if let Some(matched_pid) = matched_pid + && wait_for_server_exit(matched_pid, socket_path) + && read_pid(&pid_path) == Some(matched_pid) + { + let _ = fs::remove_file(&pid_path); + } +} + +fn try_signal_server(pid_path: &Path, socket_path: &Path, signal: i32) -> Option { + let pid = read_pid(pid_path)?; + if !pid_matches_serve_process(pid, socket_path) { + return None; + } + // SAFETY: pid was read from our pid file and verified against the active __serve command line. + if unsafe { libc::kill(pid, signal) } != 0 { + return None; + } + Some(pid) +} + +fn wait_for_server_exit(pid: i32, socket_path: &Path) -> bool { + for _ in 0..20 { + if !pid_matches_serve_process(pid, socket_path) { + return true; + } + std::thread::sleep(Duration::from_millis(25)); + } + !pid_matches_serve_process(pid, socket_path) +} + +fn read_pid(pid_path: &Path) -> Option { + let pid = fs::read_to_string(pid_path).ok()?; + let pid = pid.trim().parse::().ok()?; + (pid > 0).then_some(pid) +} + +fn pid_matches_serve_process(pid: i32, socket_path: &Path) -> bool { + let expected_binary = cargo_bin_path("embers"); + let Some((exe_path, argv)) = process_exe_and_argv(pid) else { + return false; + }; + + same_path(&exe_path, &expected_binary) + && argv + .first() + .is_some_and(|arg| same_path(Path::new(arg.as_os_str()), &expected_binary)) + && argv.get(1).is_some_and(|arg| arg == OsStr::new("__serve")) + && argv.windows(2).any(|window| { + window[0] == OsStr::new("--socket") + && same_path(Path::new(window[1].as_os_str()), socket_path) + }) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn process_exe_and_argv(pid: i32) -> Option<(PathBuf, Vec)> { + let exe_path = fs::read_link(format!("/proc/{pid}/exe")).ok()?; + let cmdline = fs::read(format!("/proc/{pid}/cmdline")).ok()?; + let argv = split_nul_args(&cmdline)?; + Some((exe_path, argv)) +} + +#[cfg(target_os = "macos")] +fn process_exe_and_argv(pid: i32) -> Option<(PathBuf, Vec)> { + let exe_path = process_exe_path(pid)?; + let mut mib = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid]; + let mut size = 0usize; + // SAFETY: `mib` names the procargs sysctl and `size` is a valid output parameter. + let size_result = unsafe { + libc::sysctl( + mib.as_mut_ptr(), + u32::try_from(mib.len()).ok()?, + std::ptr::null_mut(), + &mut size, + std::ptr::null_mut(), + 0, + ) + }; + if size_result != 0 || size == 0 { + return None; + } + + let mut bytes = vec![0u8; size]; + // SAFETY: `bytes` is allocated to the kernel-reported size and all pointers remain valid. + let read_result = unsafe { + libc::sysctl( + mib.as_mut_ptr(), + u32::try_from(mib.len()).ok()?, + bytes.as_mut_ptr().cast(), + &mut size, + std::ptr::null_mut(), + 0, + ) + }; + if read_result != 0 || size == 0 { + return None; + } + bytes.truncate(size); + let argv = parse_macos_argv(&bytes)?; + Some((exe_path, argv)) +} + +#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))] +fn process_exe_and_argv(_pid: i32) -> Option<(PathBuf, Vec)> { + None +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn split_nul_args(bytes: &[u8]) -> Option> { + let args = bytes + .split(|byte| *byte == 0) + .filter(|arg| !arg.is_empty()) + .map(|arg| OsString::from_vec(arg.to_vec())) + .collect::>(); + (!args.is_empty()).then_some(args) +} + +#[cfg(target_os = "macos")] +fn parse_macos_argv(bytes: &[u8]) -> Option> { + let argc = i32::from_ne_bytes(bytes.get(..std::mem::size_of::())?.try_into().ok()?); + if argc < 0 { + return None; + } + let argc = usize::try_from(argc).ok()?; + if argc > bytes.len() { + return None; + } + let mut index = std::mem::size_of::(); + while bytes.get(index).is_some_and(|byte| *byte != 0) { + index += 1; + } + while bytes.get(index).is_some_and(|byte| *byte == 0) { + index += 1; + } + + let mut argv = Vec::with_capacity(argc); + for _ in 0..argc { + let start = index; + while bytes.get(index).is_some_and(|byte| *byte != 0) { + index += 1; + } + if start == index { + return None; + } + argv.push(OsString::from_vec(bytes[start..index].to_vec())); + while bytes.get(index).is_some_and(|byte| *byte == 0) { + index += 1; + } + } + + (!argv.is_empty()).then_some(argv) +} + +#[cfg(target_os = "macos")] +const PROC_PIDPATHINFO_MAXSIZE: usize = 4096; + +#[cfg(target_os = "macos")] +#[link(name = "proc")] +unsafe extern "C" { + fn proc_pidpath(pid: i32, buffer: *mut libc::c_void, buffersize: u32) -> i32; +} + +#[cfg(target_os = "macos")] +fn process_exe_path(pid: i32) -> Option { + let mut buffer = vec![0u8; PROC_PIDPATHINFO_MAXSIZE]; + // SAFETY: `buffer` is a valid writable output buffer for `proc_pidpath`. + let length = unsafe { + proc_pidpath( + pid, + buffer.as_mut_ptr().cast(), + u32::try_from(buffer.len()).ok()?, + ) + }; + if length <= 0 { + return None; + } + + // SAFETY: successful `proc_pidpath` writes a NUL-terminated path into `buffer`. + let path = unsafe { CStr::from_ptr(buffer.as_ptr().cast()) }; + Some(PathBuf::from(OsStr::from_bytes(path.to_bytes()))) +} + +fn same_path(left: &Path, right: &Path) -> bool { + left == right + || fs::canonicalize(left) + .ok() + .zip(fs::canonicalize(right).ok()) + .is_some_and(|(left, right)| left == right) +} + +/// Spawn an embers client process with the given arguments and return a guard +/// that ensures cleanup of any orphaned __serve process when dropped. +/// The socket_path should be the path to the socket - if a server needs to be +/// spawned for it, the guard will clean it up. +fn spawn_embers(args: &[&str], socket_path: PathBuf) -> (SpawnedEmbers, PtyHarness) { let binary = cargo_bin_path("embers"); let binary_dir = binary.parent().expect("binary dir"); + let started_server = !socket_path.exists() && !socket_path.with_extension("pid").exists(); let path = format!( "PATH={}:{}", binary_dir.display(), @@ -31,34 +284,9 @@ fn spawn_embers(args: &[&str]) -> PtyHarness { ]; env_and_args.extend(args.iter().map(|arg| (*arg).to_owned())); let argv = env_and_args.iter().map(String::as_str).collect::>(); - PtyHarness::spawn("/usr/bin/env", &argv, PtySize::new(80, 24)).expect("spawn embers in pty") -} - -async fn shutdown_spawned_server(socket_path: &Path) { - let pid_path = socket_path.with_extension("pid"); - let pid = wait_for_pid(&pid_path) - .await - .trim() - .parse::() - .expect("pid parses"); - assert!(pid > 0, "invalid pid: {pid}"); - - // SAFETY: pid comes from our own pid file and SIGTERM targets that specific process. - let result = unsafe { libc::kill(pid, libc::SIGTERM) }; - assert_eq!(result, 0, "failed to signal spawned server"); - - for _ in 0..FILE_WAIT_ATTEMPTS { - if !socket_path.exists() && !pid_path.exists() { - return; - } - tokio::time::sleep(FILE_WAIT_POLL).await; - } - - panic!( - "timed out waiting for spawned server shutdown (socket: {}, pid file: {})", - socket_path.display(), - pid_path.display() - ); + let harness = PtyHarness::spawn("/usr/bin/env", &argv, PtySize::new(80, 24)) + .expect("spawn embers in pty"); + (SpawnedEmbers::new(socket_path, started_server), harness) } async fn wait_for_socket(socket_path: &Path) { @@ -72,17 +300,6 @@ async fn wait_for_socket(socket_path: &Path) { panic!("timed out waiting for socket {}", socket_path.display()); } -async fn wait_for_pid(pid_path: &Path) -> String { - for _ in 0..FILE_WAIT_ATTEMPTS { - if let Ok(pid) = fs::read_to_string(pid_path) { - return pid; - } - tokio::time::sleep(FILE_WAIT_POLL).await; - } - - panic!("timed out waiting for pid file {}", pid_path.display()); -} - async fn populate_scrollback_or_wait(harness: &mut PtyHarness, lines: usize) { harness .write_all("echo READY\r") @@ -97,8 +314,8 @@ async fn populate_scrollback_or_wait(harness: &mut PtyHarness, lines: usize) { .write_all(&long_output) .expect("write scrolling command"); harness - .read_until_contains("line-1", IO_TIMEOUT) - .unwrap_or_else(|error| panic!("long output started: {error}")); + .read_until_contains("DONE", IO_TIMEOUT) + .unwrap_or_else(|error| panic!("long output completed: {error}")); harness .wait_for_quiet(QUIET_TIMEOUT, IO_TIMEOUT) .unwrap_or_else(|error| panic!("long output settled: {error}")); @@ -163,13 +380,234 @@ fn first_client_id_finds_attached_row() { assert_eq!(first_client_id(output), 42); } +fn focused_pane_id(snapshot: &SessionSnapshot) -> u64 { + snapshot + .session + .focused_leaf_id + .map(|leaf_id| leaf_id.0) + .expect("session has a focused pane") +} + +fn pane_buffer_id(snapshot: &SessionSnapshot, pane_id: u64) -> BufferId { + snapshot + .nodes + .iter() + .find(|node| node.id == NodeId(pane_id)) + .and_then(|node| node.buffer_view.as_ref()) + .map(|view| view.buffer_id) + .unwrap_or_else(|| panic!("pane {pane_id} buffer view exists")) +} + +fn root_tab_child_id(snapshot: &SessionSnapshot, title: &str) -> NodeId { + snapshot + .nodes + .iter() + .find(|node| node.id == snapshot.session.root_node_id) + .and_then(|node| node.tabs.as_ref()) + .and_then(|tabs| { + tabs.tabs + .iter() + .find(|tab| tab.title == title) + .map(|tab| tab.child_id) + }) + .unwrap_or_else(|| panic!("root tab `{title}` exists")) +} + +fn split_child_order( + snapshot: &SessionSnapshot, + first_pane_id: u64, + second_pane_id: u64, +) -> Option<[u64; 2]> { + snapshot.nodes.iter().find_map(|node| { + let split = node.split.as_ref()?; + let child_ids = split + .child_ids + .iter() + .map(|child| child.0) + .collect::>(); + if child_ids.len() == 2 + && child_ids.contains(&first_pane_id) + && child_ids.contains(&second_pane_id) + { + Some([child_ids[0], child_ids[1]]) + } else { + None + } + }) +} + +async fn disable_echo_in_pane(server: &TestServer, pane_id: u64) { + let marker = format!("__ECHO_DISABLED_{pane_id}__"); + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + let snapshot = session_snapshot_containing_pane(&mut connection, pane_id).await; + let buffer_id = pane_buffer_id(&snapshot, pane_id); + send_buffer_input( + &mut connection, + buffer_id, + format!("stty -echo; printf '{marker}\\n'\r").as_bytes(), + ) + .await; + connection + .wait_for_capture_contains(buffer_id, &marker, IO_TIMEOUT) + .await + .expect("echo-disable marker appears"); +} + +async fn send_buffer_input(connection: &mut TestConnection, buffer_id: BufferId, bytes: &[u8]) { + let response = connection + .request(&ClientMessage::Input(InputRequest::Send { + request_id: new_request_id(), + buffer_id, + bytes: bytes.to_vec(), + })) + .await + .expect("send input succeeds"); + assert!( + matches!(response, ServerResponse::Ok(_)), + "expected ok response to input send, got {response:?}" + ); +} + +async fn wait_for_buffer_activity( + connection: &mut TestConnection, + buffer_id: BufferId, + expected: ActivityState, +) { + let deadline = tokio::time::Instant::now() + IO_TIMEOUT; + loop { + let response = connection + .request(&ClientMessage::Buffer(BufferRequest::Get { + request_id: new_request_id(), + buffer_id, + })) + .await + .expect("buffer get succeeds"); + let activity = match response { + ServerResponse::Buffer(response) => response.buffer.activity, + other => panic!("expected buffer response, got {other:?}"), + }; + if activity == expected { + return; + } + + assert!( + tokio::time::Instant::now() < deadline, + "timed out waiting for buffer {buffer_id} activity {expected:?}; last activity {activity:?}" + ); + tokio::time::sleep(Duration::from_millis(50)).await; + } +} + +async fn wait_for_target_pane_buffer( + connection: &mut TestConnection, + session_name: &str, + pane_id: u64, + expected_buffer_id: BufferId, +) -> SessionSnapshot { + let deadline = tokio::time::Instant::now() + IO_TIMEOUT; + loop { + let snapshot = session_snapshot_by_name(connection, session_name).await; + if pane_buffer_id(&snapshot, pane_id) == expected_buffer_id { + return snapshot; + } + + assert!( + tokio::time::Instant::now() < deadline, + "timed out waiting for pane {pane_id} to attach buffer {expected_buffer_id}" + ); + tokio::time::sleep(Duration::from_millis(50)).await; + } +} + +async fn session_snapshot_containing_pane( + connection: &mut TestConnection, + pane_id: u64, +) -> SessionSnapshot { + let response = connection + .request(&ClientMessage::Session(SessionRequest::List { + request_id: new_request_id(), + })) + .await + .expect("list sessions succeeds"); + let sessions = match response { + ServerResponse::Sessions(response) => response.sessions, + other => panic!("expected sessions response, got {other:?}"), + }; + for session in sessions { + let snapshot = connection + .session_snapshot(session.id) + .await + .expect("session snapshot succeeds"); + if snapshot.nodes.iter().any(|node| node.id == NodeId(pane_id)) { + return snapshot; + } + } + panic!("pane {pane_id} missing from all sessions"); +} + +async fn wait_for_split_child_order( + connection: &mut TestConnection, + session_name: &str, + pane_a: u64, + pane_b: u64, + expected: [u64; 2], +) -> SessionSnapshot { + let deadline = tokio::time::Instant::now() + IO_TIMEOUT; + loop { + let snapshot = session_snapshot_by_name(connection, session_name).await; + let current_order = split_child_order(&snapshot, pane_a, pane_b); + if current_order == Some(expected) { + return snapshot; + } + + assert!( + tokio::time::Instant::now() < deadline, + "timed out waiting for split order {:?}; last order {:?}", + expected, + current_order + ); + tokio::time::sleep(Duration::from_millis(50)).await; + } +} + +async fn wait_for_visible_snapshot( + connection: &mut TestConnection, + buffer_id: BufferId, + mut predicate: F, +) -> VisibleSnapshotResponse +where + F: FnMut(&VisibleSnapshotResponse) -> bool, +{ + let deadline = tokio::time::Instant::now() + IO_TIMEOUT; + loop { + let snapshot = connection + .capture_visible_buffer(buffer_id) + .await + .expect("visible capture succeeds"); + if predicate(&snapshot) { + return snapshot; + } + + assert!( + tokio::time::Instant::now() < deadline, + "timed out waiting for visible snapshot predicate; last snapshot: {snapshot:?}" + ); + tokio::time::sleep(Duration::from_millis(50)).await; + } +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn embers_without_subcommand_starts_server_and_client() { let _guard = acquire_test_lock().await.expect("acquire test lock"); - let tempdir = tempdir().expect("tempdir"); + if !require_pty() { + return; + } + let tempdir = tempfile::tempdir().expect("tempdir"); let socket_path = tempdir.path().join("embers.sock"); let socket_arg = socket_path.to_string_lossy().into_owned(); - let mut harness = spawn_embers(&["--socket", &socket_arg]); + let (_spawned, mut harness) = spawn_embers(&["--socket", &socket_arg], socket_path.clone()); harness .read_until_contains("[main]", STARTUP_TIMEOUT) @@ -200,12 +638,15 @@ async fn embers_without_subcommand_starts_server_and_client() { harness.write_all("\x11").expect("quit client"); harness.wait().expect("client exits"); - shutdown_spawned_server(&socket_path).await; + // spawned.drop() will clean up the orphaned __serve process } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn attach_subcommand_connects_to_running_server() { let _guard = acquire_test_lock().await.expect("acquire test lock"); + if !require_pty() { + return; + } let server = TestServer::start().await.expect("start server"); let binary = cargo_bin_path("embers"); let binary_dir = binary.parent().expect("binary dir"); @@ -232,7 +673,8 @@ async fn attach_subcommand_connects_to_running_server() { ); let socket_arg = server.socket_path().to_string_lossy().into_owned(); - let mut harness = spawn_embers(&["attach", "--socket", &socket_arg]); + let socket_path = server.socket_path().to_path_buf(); + let (_spawned, mut harness) = spawn_embers(&["attach", "--socket", &socket_arg], socket_path); harness .read_until_contains("[main]", STARTUP_TIMEOUT) .expect("attach client renders"); @@ -251,6 +693,9 @@ async fn attach_subcommand_connects_to_running_server() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn client_commands_can_switch_and_detach_a_live_attached_client() { let _guard = acquire_test_lock().await.expect("acquire test lock"); + if !require_pty() { + return; + } let server = TestServer::start().await.expect("start server"); run_cli(&server, ["new-session", "main"]); @@ -281,7 +726,11 @@ async fn client_commands_can_switch_and_detach_a_live_attached_client() { ); let socket_arg = server.socket_path().to_string_lossy().into_owned(); - let mut harness = spawn_embers(&["attach", "--socket", &socket_arg, "-t", "main"]); + let socket_path = server.socket_path().to_path_buf(); + let (_spawned, mut harness) = spawn_embers( + &["attach", "--socket", &socket_arg, "-t", "main"], + socket_path, + ); harness .read_until_contains("[main]", STARTUP_TIMEOUT) .expect("attach client renders main"); @@ -304,12 +753,87 @@ async fn client_commands_can_switch_and_detach_a_live_attached_client() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn page_up_enters_local_scrollback_and_shows_indicator() { +async fn buffer_reveal_switches_the_attached_client_to_the_buffer_session() { let _guard = acquire_test_lock().await.expect("acquire test lock"); - let tempdir = tempdir().expect("tempdir"); + if !require_pty() { + return; + } + let server = TestServer::start().await.expect("start server"); + + run_cli(&server, ["new-session", "main"]); + run_cli( + &server, + [ + "new-window", + "-t", + "main", + "--title", + "shell", + "--", + "/bin/sh", + ], + ); + run_cli(&server, ["new-session", "ops"]); + run_cli( + &server, + [ + "new-window", + "-t", + "ops", + "--title", + "logs", + "--", + "/bin/sh", + ], + ); + + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + let ops_snapshot = session_snapshot_by_name(&mut connection, "ops").await; + let ops_buffer_id = ops_snapshot + .session + .focused_leaf_id + .and_then(|leaf_id| { + ops_snapshot + .nodes + .iter() + .find(|node| node.id == leaf_id) + .and_then(|node| node.buffer_view.as_ref()) + .map(|view| view.buffer_id.0) + }) + .expect("ops focused buffer id exists"); + + let socket_arg = server.socket_path().to_string_lossy().into_owned(); + let socket_path = server.socket_path().to_path_buf(); + let (_spawned, mut harness) = spawn_embers( + &["attach", "--socket", &socket_arg, "-t", "main"], + socket_path, + ); + harness + .read_until_contains("[main]", STARTUP_TIMEOUT) + .expect("attach client renders main"); + + run_cli(&server, ["buffer", "reveal", &ops_buffer_id.to_string()]); + harness + .read_until_contains("[ops]", IO_TIMEOUT) + .expect("buffer reveal retargets the live client"); + + harness.write_all("\x11").expect("quit attached client"); + harness.wait().expect("client exits"); + server.shutdown().await.expect("shutdown server"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn page_up_enters_local_scrollback() { + let _guard = acquire_test_lock().await.expect("acquire test lock"); + if !require_pty() { + return; + } + let tempdir = tempfile::tempdir().expect("tempdir"); let socket_path = tempdir.path().join("embers.sock"); let socket_arg = socket_path.to_string_lossy().into_owned(); - let mut harness = spawn_embers(&["--socket", &socket_arg]); + let (_spawned, mut harness) = spawn_embers(&["--socket", &socket_arg], socket_path.clone()); harness .read_until_contains("[main]", STARTUP_TIMEOUT) @@ -320,16 +844,20 @@ async fn page_up_enters_local_scrollback_and_shows_indicator() { harness.write_all("\x11").expect("quit client"); harness.wait().expect("client exits"); - shutdown_spawned_server(&socket_path).await; + + // spawned.drop() will clean up the orphaned __serve process } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn local_selection_yank_emits_osc52_clipboard_sequence() { let _guard = acquire_test_lock().await.expect("acquire test lock"); - let tempdir = tempdir().expect("tempdir"); + if !require_pty() { + return; + } + let tempdir = tempfile::tempdir().expect("tempdir"); let socket_path = tempdir.path().join("embers.sock"); let socket_arg = socket_path.to_string_lossy().into_owned(); - let mut harness = spawn_embers(&["--socket", &socket_arg]); + let (_spawned, mut harness) = spawn_embers(&["--socket", &socket_arg], socket_path.clone()); harness .read_until_contains("[main]", STARTUP_TIMEOUT) @@ -337,6 +865,9 @@ async fn local_selection_yank_emits_osc52_clipboard_sequence() { populate_scrollback_or_wait(&mut harness, 40).await; page_up_until_visible(&mut harness, "line-1"); + harness + .wait_for_quiet(Duration::from_millis(200), IO_TIMEOUT) + .unwrap_or_else(|error| panic!("scrollback render settled: {error}")); harness.write_all("vly").expect("select and yank"); let output = harness .read_until_contains("]52;c;", IO_TIMEOUT) @@ -345,5 +876,500 @@ async fn local_selection_yank_emits_osc52_clipboard_sequence() { harness.write_all("\x11").expect("quit client"); harness.wait().expect("client exits"); - shutdown_spawned_server(&socket_path).await; + + // spawned.drop() will clean up the orphaned __serve process +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn scripted_input_bindings_reach_the_live_terminal_in_pty() { + let _guard = acquire_test_lock().await.expect("acquire test lock"); + if !require_pty() { + return; + } + let server = TestServer::start().await.expect("start server"); + + run_cli(&server, ["new-session", "main"]); + run_cli( + &server, + [ + "new-window", + "-t", + "main", + "--title", + "shell", + "--", + "/bin/sh", + ], + ); + + let tempdir = tempfile::tempdir().expect("tempdir"); + let config_path = tempdir.path().join("config.rhai"); + fs::write( + &config_path, + r#"bind("normal", "", action.send_bytes_current("echo scripted-pty\r"))"#, + ) + .expect("write config"); + + let socket_arg = server.socket_path().to_string_lossy().into_owned(); + let socket_path = server.socket_path().to_path_buf(); + let config_arg = config_path.to_string_lossy().into_owned(); + let (_spawned, mut harness) = spawn_embers( + &[ + "attach", + "--socket", + &socket_arg, + "--config", + &config_arg, + "-t", + "main", + ], + socket_path, + ); + harness + .read_until_contains("[main]", STARTUP_TIMEOUT) + .expect("attach client renders"); + harness + .write_all("stty -echo\r") + .expect("disable shell echo in focused pane"); + harness + .wait_for_quiet(QUIET_TIMEOUT, IO_TIMEOUT) + .expect("focused shell settles"); + + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + let snapshot = session_snapshot_by_name(&mut connection, "main").await; + let buffer_id = pane_buffer_id(&snapshot, focused_pane_id(&snapshot)); + + harness.write_all("\x07").expect("trigger scripted binding"); + harness + .read_until_contains("scripted-pty", IO_TIMEOUT) + .expect("scripted output renders"); + connection + .wait_for_capture_contains(buffer_id, "scripted-pty", IO_TIMEOUT) + .await + .expect("scripted output reaches focused buffer"); + + harness.write_all("\x11").expect("quit attached client"); + harness.wait().expect("client exits"); + server.shutdown().await.expect("shutdown server"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn config_reload_updates_live_bindings_without_breaking_terminal_io() { + let _guard = acquire_test_lock().await.expect("acquire test lock"); + if !require_pty() { + return; + } + let server = TestServer::start().await.expect("start server"); + + run_cli(&server, ["new-session", "main"]); + run_cli( + &server, + [ + "new-window", + "-t", + "main", + "--title", + "shell", + "--", + "/bin/sh", + ], + ); + + let tempdir = tempfile::tempdir().expect("tempdir"); + let config_path = tempdir.path().join("config.rhai"); + fs::write( + &config_path, + r#"bind("normal", "", action.send_bytes_current("echo before-reload\r"))"#, + ) + .expect("write initial config"); + + let socket_arg = server.socket_path().to_string_lossy().into_owned(); + let socket_path = server.socket_path().to_path_buf(); + let config_arg = config_path.to_string_lossy().into_owned(); + let (_spawned, mut harness) = spawn_embers( + &[ + "attach", + "--socket", + &socket_arg, + "--config", + &config_arg, + "-t", + "main", + ], + socket_path, + ); + harness + .read_until_contains("[main]", STARTUP_TIMEOUT) + .expect("attach client renders"); + harness + .write_all("stty -echo\r") + .expect("disable shell echo in focused pane"); + harness + .wait_for_quiet(QUIET_TIMEOUT, IO_TIMEOUT) + .expect("focused shell settles"); + + harness.write_all("\x07").expect("trigger initial binding"); + harness + .read_until_contains("before-reload", IO_TIMEOUT) + .expect("initial binding renders"); + + fs::write( + &config_path, + r#"bind("normal", "", action.send_bytes_current("echo after-reload\r"))"#, + ) + .expect("write reloaded config"); + let previous_mtime = FileTime::from_last_modification_time( + &fs::metadata(&config_path).expect("read reloaded config metadata"), + ); + let next_mtime = FileTime::from_unix_time( + previous_mtime.unix_seconds() + 1, + previous_mtime.nanoseconds(), + ); + filetime::set_file_mtime(&config_path, next_mtime).expect("bump reloaded config mtime"); + let reload_deadline = tokio::time::Instant::now() + IO_TIMEOUT; + loop { + harness.write_all("\x07").expect("trigger reloaded binding"); + if harness + .read_until_contains("after-reload", Duration::from_millis(200)) + .is_ok() + { + break; + } + assert!( + tokio::time::Instant::now() < reload_deadline, + "timed out waiting for reloaded binding to activate" + ); + } + + let output = run_pane_command(&mut harness, "echo still-live", "still-live"); + assert!( + output.contains("still-live"), + "regular terminal input must still work after reload:\n{output}" + ); + + harness.write_all("\x11").expect("quit attached client"); + harness.wait().expect("client exits"); + server.shutdown().await.expect("shutdown server"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn live_pty_client_preserves_buffers_across_layout_and_attachment_changes() { + let _guard = acquire_test_lock().await.expect("acquire test lock"); + if !require_pty() { + return; + } + let server = TestServer::start().await.expect("start server"); + + run_cli(&server, ["new-session", "main"]); + run_cli( + &server, + [ + "new-window", + "-t", + "main", + "--title", + "shell", + "--", + "/bin/sh", + ], + ); + + let socket_arg = server.socket_path().to_string_lossy().into_owned(); + let socket_path = server.socket_path().to_path_buf(); + let (_spawned, mut harness) = spawn_embers( + &["attach", "--socket", &socket_arg, "-t", "main"], + socket_path, + ); + harness + .read_until_contains("[main]", STARTUP_TIMEOUT) + .expect("attach client renders"); + + let split = run_cli(&server, ["split-window", "--", "/bin/sh"]); + let moving_pane_id = stdout(&split) + .trim() + .parse::() + .expect("split-window returns new pane id"); + disable_echo_in_pane(&server, moving_pane_id).await; + + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + let snapshot = session_snapshot_by_name(&mut connection, "main").await; + let anchor_pane_id = snapshot + .nodes + .iter() + .filter(|node| node.buffer_view.is_some()) + .map(|node| node.id.0) + .find(|pane_id| *pane_id != moving_pane_id) + .expect("anchor pane exists"); + let moving_buffer_id = pane_buffer_id(&snapshot, moving_pane_id); + + run_cli( + &server, + [ + "send-keys", + "-t", + &moving_pane_id.to_string(), + "--enter", + "echo", + "split-live", + ], + ); + connection + .wait_for_capture_contains(moving_buffer_id, "split-live", IO_TIMEOUT) + .await + .expect("split pane keeps running"); + harness + .read_until_contains("split-live", IO_TIMEOUT) + .expect("split output renders in attached client"); + + let initial_order = split_child_order(&snapshot, anchor_pane_id, moving_pane_id) + .expect("split containing anchor and moving panes exists"); + let expected_order = if initial_order == [anchor_pane_id, moving_pane_id] { + run_cli( + &server, + [ + "node", + "move-before", + &moving_pane_id.to_string(), + &anchor_pane_id.to_string(), + ], + ); + [moving_pane_id, anchor_pane_id] + } else { + run_cli( + &server, + [ + "node", + "move-after", + &moving_pane_id.to_string(), + &anchor_pane_id.to_string(), + ], + ); + [anchor_pane_id, moving_pane_id] + }; + let _moved_snapshot = wait_for_split_child_order( + &mut connection, + "main", + anchor_pane_id, + moving_pane_id, + expected_order, + ) + .await; + + run_cli( + &server, + [ + "send-keys", + "-t", + &moving_pane_id.to_string(), + "--enter", + "echo", + "moved-live", + ], + ); + connection + .wait_for_capture_contains(moving_buffer_id, "moved-live", IO_TIMEOUT) + .await + .expect("moved pane keeps running"); + harness + .read_until_contains("moved-live", IO_TIMEOUT) + .expect("moved pane output still renders"); + + let response = connection + .request(&ClientMessage::Buffer(BufferRequest::Detach { + request_id: new_request_id(), + buffer_id: moving_buffer_id, + })) + .await + .expect("detach buffer succeeds"); + assert!( + matches!(response, ServerResponse::Ok(_)), + "expected ok response to buffer detach, got {response:?}" + ); + + send_buffer_input(&mut connection, moving_buffer_id, b"echo detached-live\r").await; + connection + .wait_for_capture_contains(moving_buffer_id, "detached-live", IO_TIMEOUT) + .await + .expect("detached buffer continues receiving output"); + + run_cli( + &server, + [ + "attach-buffer", + &moving_buffer_id.to_string(), + "-t", + &anchor_pane_id.to_string(), + ], + ); + let _reattached_snapshot = + wait_for_target_pane_buffer(&mut connection, "main", anchor_pane_id, moving_buffer_id) + .await; + + send_buffer_input(&mut connection, moving_buffer_id, b"echo reattach-live\r").await; + connection + .wait_for_capture_contains(moving_buffer_id, "reattach-live", IO_TIMEOUT) + .await + .expect("reattached buffer continues receiving output"); + harness + .read_until_contains("reattach-live", IO_TIMEOUT) + .expect("reattached buffer output renders in attached client"); + + harness.write_all("\x11").expect("quit attached client"); + harness.wait().expect("client exits"); + server.shutdown().await.expect("shutdown server"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn hidden_buffer_bells_surface_in_the_attached_client_and_reveal_buffered_output() { + let _guard = acquire_test_lock().await.expect("acquire test lock"); + if !require_pty() { + return; + } + let server = TestServer::start().await.expect("start server"); + + run_cli(&server, ["new-session", "main"]); + run_cli( + &server, + [ + "new-window", + "-t", + "main", + "--title", + "shell", + "--", + "/bin/sh", + ], + ); + run_cli( + &server, + ["new-window", "-t", "main", "--title", "bg", "--", "/bin/sh"], + ); + run_cli(&server, ["select-window", "-t", "main:0"]); + + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + let snapshot = session_snapshot_by_name(&mut connection, "main").await; + let hidden_pane_id = root_tab_child_id(&snapshot, "bg").0; + let hidden_buffer_id = pane_buffer_id(&snapshot, hidden_pane_id); + disable_echo_in_pane(&server, hidden_pane_id).await; + + let socket_arg = server.socket_path().to_string_lossy().into_owned(); + let socket_path = server.socket_path().to_path_buf(); + let (_spawned, mut harness) = spawn_embers( + &["attach", "--socket", &socket_arg, "-t", "main"], + socket_path, + ); + harness + .read_until_contains("[main]", STARTUP_TIMEOUT) + .expect("attach client renders"); + + send_buffer_input( + &mut connection, + hidden_buffer_id, + b"printf 'hidden-bell\\n\\a'; sleep 0.5\r", + ) + .await; + connection + .wait_for_capture_contains(hidden_buffer_id, "hidden-bell", IO_TIMEOUT) + .await + .expect("hidden buffer output accumulates"); + wait_for_buffer_activity(&mut connection, hidden_buffer_id, ActivityState::Bell).await; + harness + .read_until_contains("!bg", IO_TIMEOUT) + .expect("hidden bell updates tab marker in attached client"); + + run_cli(&server, ["select-window", "-t", "main:bg"]); + harness + .read_until_contains("hidden-bell", IO_TIMEOUT) + .expect("revealed hidden buffer shows accumulated output"); + + harness.write_all("\x11").expect("quit attached client"); + harness.wait().expect("client exits"); + server.shutdown().await.expect("shutdown server"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn fullscreen_terminal_transitions_render_in_the_live_client_pty() { + let _guard = acquire_test_lock().await.expect("acquire test lock"); + if !require_pty() { + return; + } + let server = TestServer::start().await.expect("start server"); + + run_cli(&server, ["new-session", "main"]); + run_cli( + &server, + [ + "new-window", + "-t", + "main", + "--title", + "shell", + "--", + "/bin/sh", + ], + ); + + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + let snapshot = session_snapshot_by_name(&mut connection, "main").await; + let buffer_id = pane_buffer_id(&snapshot, focused_pane_id(&snapshot)); + + let socket_arg = server.socket_path().to_string_lossy().into_owned(); + let socket_path = server.socket_path().to_path_buf(); + let (_spawned, mut harness) = spawn_embers( + &["attach", "--socket", &socket_arg, "-t", "main"], + socket_path, + ); + harness + .read_until_contains("[main]", STARTUP_TIMEOUT) + .expect("attach client renders"); + harness + .write_all("stty -echo\r") + .expect("disable shell echo in focused pane"); + harness + .wait_for_quiet(QUIET_TIMEOUT, IO_TIMEOUT) + .expect("focused shell settles"); + + harness + .write_all( + "printf '\\033[?1049h\\033[2J\\033[HPTY-FULLSCREEN'; sleep 1; printf '\\033[?1049lPTY-RESTORED\\n'\r", + ) + .expect("run fullscreen fixture"); + harness + .read_until_contains("PTY-FULLSCREEN", IO_TIMEOUT) + .expect("fullscreen output renders in live client"); + + let live = wait_for_visible_snapshot(&mut connection, buffer_id, |snapshot| { + snapshot.alternate_screen + && snapshot + .lines + .iter() + .any(|line| line.contains("PTY-FULLSCREEN")) + }) + .await; + assert!(live.alternate_screen); + + harness + .read_until_contains("PTY-RESTORED", IO_TIMEOUT) + .expect("primary screen restoration renders in live client"); + let restored = wait_for_visible_snapshot(&mut connection, buffer_id, |snapshot| { + !snapshot.alternate_screen + && snapshot + .lines + .iter() + .any(|line| line.contains("PTY-RESTORED")) + }) + .await; + assert!(!restored.alternate_screen); + + harness.write_all("\x11").expect("quit attached client"); + harness.wait().expect("client exits"); + server.shutdown().await.expect("shutdown server"); } diff --git a/crates/embers-cli/tests/panes.rs b/crates/embers-cli/tests/panes.rs index 8f935e0..24188e5 100644 --- a/crates/embers-cli/tests/panes.rs +++ b/crates/embers-cli/tests/panes.rs @@ -1,7 +1,7 @@ use std::time::Duration; -use embers_core::RequestId; -use embers_protocol::{BufferRequest, ClientMessage, ServerResponse}; +use embers_core::{ErrorCode, RequestId}; +use embers_protocol::{BufferRequest, ClientMessage, InputRequest, ServerResponse}; use embers_test_support::{TestConnection, TestServer, acquire_test_lock}; use tokio::time::sleep; @@ -246,3 +246,304 @@ async fn detached_buffers_can_be_listed_and_attached_via_cli() { server.shutdown().await.expect("shutdown server"); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn buffer_show_and_history_open_helper_buffers() { + let _guard = acquire_test_lock().await.expect("acquire test lock"); + let server = TestServer::start().await.expect("start server"); + + run_cli(&server, ["new-session", "alpha"]); + run_cli( + &server, + [ + "new-window", + "-t", + "alpha", + "--title", + "work", + "--", + "/bin/sh", + ], + ); + + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + let snapshot = session_snapshot_by_name(&mut connection, "alpha").await; + let leaf = snapshot + .session + .focused_leaf_id + .expect("focused pane exists"); + let session_id = snapshot.session.id; + let buffer_id = snapshot + .nodes + .iter() + .find(|node| node.id == leaf) + .and_then(|node| node.buffer_view.as_ref()) + .map(|view| view.buffer_id) + .expect("focused pane buffer exists"); + + run_cli( + &server, + [ + "send-keys", + "-t", + &leaf.to_string(), + "--enter", + "printf", + "history-helper\\n", + ], + ); + let deadline = tokio::time::Instant::now() + Duration::from_secs(2); + loop { + let captured = run_cli(&server, ["capture-pane", "-t", &leaf.to_string()]); + if stdout(&captured).contains("history-helper") { + break; + } + assert!( + tokio::time::Instant::now() < deadline, + "timed out waiting for pane output" + ); + sleep(Duration::from_millis(50)).await; + } + + let shown = run_cli(&server, ["buffer", "show", &buffer_id.to_string()]); + let shown_stdout = stdout(&shown); + assert!(shown_stdout.contains(&format!("id\t{buffer_id}"))); + assert!(shown_stdout.contains("kind\tpty")); + assert!(shown_stdout.contains(&format!("location\tsession:{session_id} node:{leaf}"))); + + let opened = run_cli( + &server, + [ + "buffer", + "history", + &buffer_id.to_string(), + "--scope", + "visible", + ], + ); + let opened_stdout = stdout(&opened).trim().to_owned(); + let helper_buffer_id = opened_stdout + .split('\t') + .next() + .expect("helper buffer id column") + .parse::() + .expect("helper buffer id parses"); + + let snapshot = session_snapshot_by_name(&mut connection, "alpha").await; + let helper = snapshot + .buffers + .iter() + .find(|buffer| buffer.id.0 == helper_buffer_id) + .expect("helper buffer exists in session"); + assert_eq!(helper.kind, embers_protocol::BufferRecordKind::Helper); + assert!(helper.read_only); + assert_eq!(helper.helper_source_buffer_id, Some(buffer_id)); + assert_eq!( + helper.helper_scope, + Some(embers_protocol::BufferHistoryScope::Visible) + ); + + let helper_capture = connection + .request(&ClientMessage::Buffer(BufferRequest::Capture { + request_id: RequestId(3), + buffer_id: helper.id, + })) + .await + .expect("capture helper succeeds"); + let helper_text = match helper_capture { + ServerResponse::Snapshot(response) => response.lines.join("\n"), + other => panic!("expected helper snapshot response, got {other:?}"), + }; + assert!(helper_text.contains("history-helper")); + + let send = connection + .request(&ClientMessage::Input(InputRequest::Send { + request_id: RequestId(4), + buffer_id: helper.id, + bytes: b"nope".to_vec(), + })) + .await + .expect("helper send request returns a response"); + assert!( + matches!( + send, + ServerResponse::Error(ref response) + if response.error.code == ErrorCode::Conflict + && response.error.message.contains("read-only") + ), + "helper buffers should reject input with a read-only conflict, got {send:?}" + ); + + server.shutdown().await.expect("shutdown server"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn node_commands_cover_zoom_swap_break_join_and_reorder() { + let _guard = acquire_test_lock().await.expect("acquire test lock"); + let server = TestServer::start().await.expect("start server"); + + run_cli(&server, ["new-session", "alpha"]); + run_cli( + &server, + [ + "new-window", + "-t", + "alpha", + "--title", + "work", + "--", + "/bin/sh", + ], + ); + let split = run_cli(&server, ["split-window", "--", "/bin/sh"]); + let second_pane_id = stdout(&split) + .trim() + .parse::() + .expect("split-window returns pane id"); + + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + let snapshot = session_snapshot_by_name(&mut connection, "alpha").await; + let first_pane_id = snapshot + .nodes + .iter() + .find(|node| node.buffer_view.as_ref().is_some() && node.id.0 != second_pane_id) + .map(|node| node.id.0) + .expect("first pane id exists"); + + run_cli(&server, ["node", "zoom", &first_pane_id.to_string()]); + let snapshot = session_snapshot_by_name(&mut connection, "alpha").await; + assert_eq!( + snapshot.session.zoomed_node_id, + Some(embers_core::NodeId(first_pane_id)) + ); + + run_cli(&server, ["node", "unzoom", "-t", "alpha"]); + let snapshot = session_snapshot_by_name(&mut connection, "alpha").await; + assert_eq!(snapshot.session.zoomed_node_id, None); + + let parent_split_id = snapshot + .nodes + .iter() + .find(|node| { + node.split.as_ref().is_some_and(|split| { + split + .child_ids + .contains(&embers_core::NodeId(first_pane_id)) + && split + .child_ids + .contains(&embers_core::NodeId(second_pane_id)) + }) + }) + .map(|node| node.id) + .expect("parent split exists"); + + run_cli( + &server, + [ + "node", + "swap", + &first_pane_id.to_string(), + &second_pane_id.to_string(), + ], + ); + let snapshot = session_snapshot_by_name(&mut connection, "alpha").await; + let split = snapshot + .nodes + .iter() + .find(|node| node.id == parent_split_id) + .and_then(|node| node.split.as_ref()) + .expect("split still exists"); + assert_eq!(split.child_ids[0], embers_core::NodeId(second_pane_id)); + + run_cli( + &server, + [ + "node", + "move-before", + &first_pane_id.to_string(), + &second_pane_id.to_string(), + ], + ); + let snapshot = session_snapshot_by_name(&mut connection, "alpha").await; + let split = snapshot + .nodes + .iter() + .find(|node| node.id == parent_split_id) + .and_then(|node| node.split.as_ref()) + .expect("split still exists after reorder"); + assert_eq!(split.child_ids[0], embers_core::NodeId(first_pane_id)); + + run_cli( + &server, + [ + "node", + "break", + &second_pane_id.to_string(), + "--to", + "floating", + ], + ); + let snapshot = session_snapshot_by_name(&mut connection, "alpha").await; + assert_eq!(snapshot.floating.len(), 1); + assert_eq!( + snapshot.floating[0].root_node_id, + embers_core::NodeId(second_pane_id) + ); + + let detached = connection + .request(&ClientMessage::Buffer(BufferRequest::Create { + request_id: RequestId(10), + title: Some("notes".to_owned()), + command: vec!["/bin/sh".to_owned()], + cwd: None, + env: Default::default(), + })) + .await + .expect("buffer create succeeds"); + let detached_buffer_id = match detached { + ServerResponse::Buffer(response) => response.buffer.id, + other => panic!("expected buffer response, got {other:?}"), + }; + + run_cli( + &server, + [ + "node", + "join-buffer", + &first_pane_id.to_string(), + &detached_buffer_id.to_string(), + "--as", + "tab-after", + ], + ); + let snapshot = session_snapshot_by_name(&mut connection, "alpha").await; + let (first_index, detached_index) = snapshot + .nodes + .iter() + .find_map(|node| { + node.tabs.as_ref().and_then(|tabs| { + let first_index = tabs.tabs.iter().position(|tab| tab.child_id.0 == first_pane_id)?; + let detached_index = tabs.tabs.iter().position(|tab| { + snapshot + .nodes + .iter() + .find(|candidate| candidate.id == tab.child_id) + .and_then(|candidate| candidate.buffer_view.as_ref()) + .is_some_and(|view| view.buffer_id == detached_buffer_id) + })?; + Some((first_index, detached_index)) + }) + }) + .expect("join-buffer attached the detached buffer into the same tabs container as the first pane"); + assert_eq!( + detached_index, + first_index + 1, + "join-buffer placed the detached buffer immediately after the first pane" + ); + + server.shutdown().await.expect("shutdown server"); +} diff --git a/crates/embers-cli/tests/support/mod.rs b/crates/embers-cli/tests/support/mod.rs index cfa7a31..5c66733 100644 --- a/crates/embers-cli/tests/support/mod.rs +++ b/crates/embers-cli/tests/support/mod.rs @@ -4,7 +4,22 @@ use std::ffi::OsStr; use std::process::Output; use embers_protocol::{ClientMessage, ServerResponse, SessionRequest, SessionSnapshot}; -use embers_test_support::{TestConnection, TestServer, cargo_bin}; +use embers_test_support::{TestConnection, TestServer, cargo_bin, is_pty_available}; + +/// Returns true if PTY tests can run on this system. +/// Call this at the start of tests that require PTY support. +#[must_use] +pub fn require_pty() -> bool { + if is_pty_available() { + true + } else { + eprintln!( + "WARNING: PTY devices not available on this system. \ + Tests requiring PTY will be skipped." + ); + false + } +} pub fn cli_command(server: &TestServer) -> assert_cmd::Command { let mut command = cargo_bin("embers"); diff --git a/crates/embers-client/Cargo.toml b/crates/embers-client/Cargo.toml index cf9032f..1a1f1be 100644 --- a/crates/embers-client/Cargo.toml +++ b/crates/embers-client/Cargo.toml @@ -6,6 +6,9 @@ license.workspace = true rust-version.workspace = true version.workspace = true +[lib] +doctest = false + [dependencies] async-trait.workspace = true base64.workspace = true diff --git a/crates/embers-client/src/client.rs b/crates/embers-client/src/client.rs index 92c6a5c..06128ca 100644 --- a/crates/embers-client/src/client.rs +++ b/crates/embers-client/src/client.rs @@ -107,9 +107,8 @@ where } pub async fn process_next_event(&mut self) -> Result { - let event = self.transport.next_event().await?; - self.state.apply_event(&event); - self.resync_for_event(&event).await?; + let event = self.next_event().await?; + self.handle_event(&event).await?; Ok(event) } @@ -121,6 +120,27 @@ where } } + pub async fn process_next_event_timeout( + &mut self, + timeout: std::time::Duration, + ) -> Result> { + let event = match tokio::time::timeout(timeout, self.next_event()).await { + Ok(result) => result?, + Err(_) => return Ok(None), + }; + self.handle_event(&event).await?; + Ok(Some(event)) + } + + pub async fn next_event(&mut self) -> Result { + self.transport.next_event().await + } + + pub async fn handle_event(&mut self, event: &ServerEvent) -> Result<()> { + self.state.apply_event(event); + self.resync_for_event(event).await + } + pub async fn resync_session(&mut self, session_id: SessionId) -> Result<()> { let response = self .transport @@ -161,6 +181,26 @@ where } } + pub async fn refresh_buffer_record(&mut self, buffer_id: BufferId) -> Result<()> { + let response = self + .transport + .request(ClientMessage::Buffer(BufferRequest::Get { + request_id: self.next_request_id(), + buffer_id, + })) + .await?; + + match expect_response(response)? { + ServerResponse::Buffer(response) => { + self.state.apply_buffer_record(response.buffer); + Ok(()) + } + other => Err(MuxError::protocol(format!( + "expected buffer response, got {other:?}" + ))), + } + } + pub async fn capture_buffer(&self, buffer_id: BufferId) -> Result { let response = self .transport @@ -278,10 +318,12 @@ where } Ok(()) } + ServerEvent::RenderInvalidated(event) => { + self.refresh_buffer_record(event.buffer_id).await + } ServerEvent::BufferCreated(_) | ServerEvent::BufferDetached(_) - | ServerEvent::FocusChanged(_) - | ServerEvent::RenderInvalidated(_) => Ok(()), + | ServerEvent::FocusChanged(_) => Ok(()), } } diff --git a/crates/embers-client/src/config/loader.rs b/crates/embers-client/src/config/loader.rs index 20f581f..99aee5e 100644 --- a/crates/embers-client/src/config/loader.rs +++ b/crates/embers-client/src/config/loader.rs @@ -31,6 +31,11 @@ bind("select", "k", action.select_move_up()); bind("select", "l", action.select_move_right()); bind("select", "y", action.yank_selection()); bind("select", "", action.cancel_selection()); + +bind("search", "", action.commit_search()); +bind("search", "", action.cancel_search()); +bind("search", "n", action.search_next()); +bind("search", "N", action.search_prev()); "#; #[derive(Clone, Debug, PartialEq, Eq)] @@ -59,10 +64,7 @@ pub struct ConfigManager { impl ConfigManager { pub fn load(discovery: ConfigDiscoveryOptions) -> Result { let active_source = load_config_source(&discovery)?; - let active_script = match active_source.origin { - ConfigOrigin::BuiltIn => ScriptEngine::load(&active_source)?, - _ => ScriptEngine::load_with_overlay(BUILTIN_CONFIG_SOURCE, &active_source)?, - }; + let active_script = load_script_engine(&active_source)?; Ok(Self { discovery, active_source, @@ -88,14 +90,33 @@ impl ConfigManager { pub fn reload(&mut self) -> Result<(), ConfigManagerError> { let candidate_source = load_config_source(&self.discovery)?; - let candidate_script = match candidate_source.origin { - ConfigOrigin::BuiltIn => ScriptEngine::load(&candidate_source)?, - _ => ScriptEngine::load_with_overlay(BUILTIN_CONFIG_SOURCE, &candidate_source)?, - }; + let candidate_script = load_script_engine(&candidate_source)?; self.active_source = candidate_source; self.active_script = candidate_script; Ok(()) } + + pub fn reload_if_changed(&mut self) -> Result { + let candidate_source = load_config_source(&self.discovery)?; + if candidate_source == self.active_source { + return Ok(false); + } + + let candidate_script = load_script_engine(&candidate_source)?; + self.active_source = candidate_source; + self.active_script = candidate_script; + Ok(true) + } +} + +fn load_script_engine(source: &LoadedConfigSource) -> Result { + match source.origin { + ConfigOrigin::BuiltIn => Ok(ScriptEngine::load(source)?), + _ => Ok(ScriptEngine::load_with_overlay( + BUILTIN_CONFIG_SOURCE, + source, + )?), + } } pub fn load_config_source(discovery: &ConfigDiscoveryOptions) -> ConfigResult { diff --git a/crates/embers-client/src/configured_client.rs b/crates/embers-client/src/configured_client.rs index 7a87aea..d3191c5 100644 --- a/crates/embers-client/src/configured_client.rs +++ b/crates/embers-client/src/configured_client.rs @@ -9,8 +9,8 @@ use embers_core::{ ActivityState, BufferId, FloatGeometry, MuxError, NodeId, Point, Result, SessionId, Size, }; use embers_protocol::{ - BufferRecord, BufferRequest, BufferResponse, ClientMessage, FloatingRequest, InputRequest, - NodeRequest, ServerEvent, ServerResponse, + BufferLocation, BufferLocationAttachment, BufferRecord, BufferRequest, BufferResponse, + ClientMessage, FloatingRequest, InputRequest, NodeRequest, ServerEvent, ServerResponse, }; use crate::RenderGrid; @@ -325,14 +325,52 @@ where } pub async fn process_next_event(&mut self) -> Result { - let event = self.client.process_next_event().await?; - if let ServerEvent::RenderInvalidated(event) = &event { + let event = self.next_event().await?; + self.handle_event(&event).await?; + Ok(event) + } + + pub async fn process_next_event_timeout( + &mut self, + timeout: std::time::Duration, + ) -> Result> { + let Some(event) = tokio::time::timeout(timeout, self.client.next_event()) + .await + .ok() + .transpose()? + else { + return Ok(None); + }; + self.handle_event(&event).await?; + Ok(Some(event)) + } + + pub async fn next_event(&mut self) -> Result { + self.client.next_event().await + } + + pub async fn handle_event(&mut self, event: &ServerEvent) -> Result<()> { + let previous_render_activity = match event { + ServerEvent::RenderInvalidated(render) => self + .client + .state() + .buffers + .get(&render.buffer_id) + .map(|buffer| buffer.activity), + _ => None, + }; + let detached_session_id = matches!(event, ServerEvent::BufferDetached(_)) + .then(|| self.event_session_id(event)) + .flatten(); + self.client.handle_event(event).await?; + if let ServerEvent::RenderInvalidated(event) = event { self.client.refresh_buffer_snapshot(event.buffer_id).await?; } + let session_id = detached_session_id.or_else(|| self.event_session_id(event)); - let session_id = self.event_session_id(&event); - let mut event_names = vec![event_name(&event).to_owned()]; - if let ServerEvent::RenderInvalidated(render) = &event + let mut event_names = vec![event_name(event).to_owned()]; + if let ServerEvent::RenderInvalidated(render) = event + && previous_render_activity != Some(ActivityState::Bell) && self .client .state() @@ -347,7 +385,7 @@ where let context = self.context_for( session_id, self.viewport, - Some(event_info(&event_name, &event)), + Some(event_info(&event_name, event, session_id)), ); match self .config @@ -362,7 +400,7 @@ where Err(error) => self.record_notification(error.to_string()), } } - Ok(event) + Ok(()) } pub async fn render_session( @@ -402,18 +440,39 @@ where self.config .reload() .map_err(|error| MuxError::invalid_input(error.to_string()))?; + self.finish_config_reload(¤t_mode); + Ok(()) + } + + pub fn reload_config_if_changed(&mut self) -> Result { + match self.config.reload_if_changed() { + Ok(false) => Ok(false), + Ok(true) => { + let current_mode = self.input_state.current_mode().to_owned(); + self.finish_config_reload(¤t_mode); + Ok(true) + } + Err(error) => { + let message = error.to_string(); + self.record_notification(message.clone()); + Err(MuxError::invalid_input(message)) + } + } + } + + fn finish_config_reload(&mut self, current_mode: &str) { if self .config .active_script() .loaded_config() .modes - .contains_key(¤t_mode) + .contains_key(current_mode) { self.input_state.clear_pending(); } else { self.input_state.set_mode(NORMAL_MODE); + self.search_prompt = None; } - Ok(()) } async fn execute_actions( @@ -424,6 +483,8 @@ where ) -> Result<()> { let mut pending = VecDeque::from(actions); let mut expansions = 0usize; + let mut current_session_id = session_id; + let current_viewport = viewport; while let Some(action) = pending.pop_front() { let result = match action { Action::Noop => Ok(()), @@ -431,11 +492,10 @@ where prepend_actions_with_limit(&mut pending, actions, &mut expansions) } Action::RunNamedAction { name } => { - match self - .config - .active_script() - .run_named_action(&name, self.context_for(session_id, viewport, None)) - { + match self.config.active_script().run_named_action( + &name, + self.context_for(current_session_id, current_viewport, None), + ) { Ok(actions) => { prepend_actions_with_limit(&mut pending, actions, &mut expansions) } @@ -443,12 +503,18 @@ where } } Action::EnterMode { mode } => { - let actions = self.transition_mode(mode, session_id, viewport).await?; + let actions = self + .transition_mode(mode, current_session_id, current_viewport) + .await?; prepend_actions_with_limit(&mut pending, actions, &mut expansions) } Action::LeaveMode => { let actions = self - .transition_mode(NORMAL_MODE.to_owned(), session_id, viewport) + .transition_mode( + NORMAL_MODE.to_owned(), + current_session_id, + current_viewport, + ) .await?; prepend_actions_with_limit(&mut pending, actions, &mut expansions) } @@ -459,7 +525,7 @@ where mode }; let actions = self - .transition_mode(next_mode, session_id, viewport) + .transition_mode(next_mode, current_session_id, current_viewport) .await?; prepend_actions_with_limit(&mut pending, actions, &mut expansions) } @@ -471,13 +537,113 @@ where self.record_notification(format_notification(level, &message)); Ok(()) } + Action::FocusBuffer { buffer_id } => { + let location = Self::buffer_location_from_response( + self.client + .request_message(ClientMessage::Buffer(BufferRequest::GetLocation { + request_id: self.client.next_request_id(), + buffer_id, + })) + .await?, + "buffer focus", + )?; + let (session_id, node_id) = + Self::attached_buffer_location(Some(buffer_id), location, "buffer focus")?; + self.focus_node_with_shortcut( + session_id, + node_id, + self.active_session_id == Some(session_id), + ) + .await?; + current_session_id = Some(session_id); + self.active_session_id = Some(session_id); + if let Some(viewport) = current_viewport { + self.set_active_view(session_id, viewport); + } + self.client.resync_all_sessions().await + } + Action::RevealBuffer { buffer_id } => { + let location = Self::buffer_location_from_response( + self.client + .request_message(ClientMessage::Buffer(BufferRequest::Reveal { + request_id: self.client.next_request_id(), + buffer_id, + client_id: None, + })) + .await?, + "buffer reveal", + )?; + let (session_id, _) = + Self::attached_buffer_location(Some(buffer_id), location, "buffer reveal")?; + current_session_id = Some(session_id); + self.active_session_id = Some(session_id); + if let Some(viewport) = current_viewport { + self.set_active_view(session_id, viewport); + } + self.client.resync_all_sessions().await + } + Action::OpenBufferHistory { + buffer_id, + scope, + placement, + } => { + let location = Self::buffer_location_from_response( + self.client + .request_message(ClientMessage::Buffer(BufferRequest::OpenHistory { + request_id: self.client.next_request_id(), + buffer_id, + scope, + placement, + client_id: None, + })) + .await?, + "buffer history", + )?; + let (session_id, _) = + Self::attached_buffer_location(None, location, "buffer history")?; + current_session_id = Some(session_id); + self.active_session_id = Some(session_id); + if let Some(viewport) = current_viewport { + self.set_active_view(session_id, viewport); + } + self.client.resync_all_sessions().await + } action => { - let Some((session_id, viewport)) = session_id.zip(viewport) else { - continue; - }; - let presentation = self.prepare_presentation(session_id, viewport).await?; - self.execute_action(session_id, viewport, &presentation, action) + match self + .execute_without_presentation(current_session_id, action) .await + { + Ok(None) => Ok(()), + Ok(Some(action)) => { + if matches!(action, Action::UnzoomNode { .. }) + && current_session_id.is_none() + { + return Err(MuxError::invalid_input(format!( + "cannot execute action {action:?} without current_session_id" + ))); + } + let mut missing = Vec::new(); + if current_session_id.is_none() { + missing.push("current_session_id"); + } + if current_viewport.is_none() { + missing.push("current_viewport"); + } + if !missing.is_empty() { + return Err(MuxError::invalid_input(format!( + "cannot execute action {action:?} without {}", + missing.join(" and ") + ))); + } + let session_id = current_session_id.expect("checked current session"); + let viewport = current_viewport.expect("checked current viewport"); + let presentation = + self.prepare_presentation(session_id, viewport).await?; + self.execute_action(session_id, viewport, &presentation, action) + .await + } + Err(error) => Err(error), + } } }; if let Err(error) = result { @@ -487,6 +653,189 @@ where Ok(()) } + async fn execute_without_presentation( + &mut self, + current_session_id: Option, + action: Action, + ) -> Result> { + match action { + Action::CloseFloating { + floating_id: Some(floating_id), + } => { + self.client + .request_message(ClientMessage::Floating(FloatingRequest::Close { + request_id: self.client.next_request_id(), + floating_id, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::CloseView { + node_id: Some(node_id), + } => { + self.client + .request_message(ClientMessage::Node(NodeRequest::Close { + request_id: self.client.next_request_id(), + node_id, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::KillBuffer { + buffer_id: Some(buffer_id), + } => { + self.client + .request_message(ClientMessage::Buffer(BufferRequest::Kill { + request_id: self.client.next_request_id(), + buffer_id, + force: false, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::DetachBuffer { + buffer_id: Some(buffer_id), + } => { + self.client + .request_message(ClientMessage::Buffer(BufferRequest::Detach { + request_id: self.client.next_request_id(), + buffer_id, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::MoveBufferToNode { buffer_id, node_id } => { + self.client + .request_message(ClientMessage::Node(NodeRequest::MoveBufferToNode { + request_id: self.client.next_request_id(), + buffer_id, + target_leaf_node_id: node_id, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::ZoomNode { + node_id: Some(node_id), + } => { + self.client + .request_message(ClientMessage::Node(NodeRequest::Zoom { + request_id: self.client.next_request_id(), + node_id, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::UnzoomNode { + session_id: target_session_id, + } => { + let Some(session_id) = target_session_id.or(current_session_id) else { + return Ok(Some(Action::UnzoomNode { + session_id: target_session_id, + })); + }; + self.client + .request_message(ClientMessage::Node(NodeRequest::Unzoom { + request_id: self.client.next_request_id(), + session_id, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::ToggleZoomNode { + node_id: Some(node_id), + } => { + self.client + .request_message(ClientMessage::Node(NodeRequest::ToggleZoom { + request_id: self.client.next_request_id(), + node_id, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::SwapSiblingNodes { + first_node_id: Some(first_node_id), + second_node_id, + } => { + self.client + .request_message(ClientMessage::Node(NodeRequest::SwapSiblings { + request_id: self.client.next_request_id(), + first_node_id, + second_node_id, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::BreakNode { + node_id: Some(node_id), + destination, + } => { + self.client + .request_message(ClientMessage::Node(NodeRequest::BreakNode { + request_id: self.client.next_request_id(), + node_id, + destination, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::JoinBufferAtNode { + node_id: Some(node_id), + buffer_id, + placement, + } => { + self.client + .request_message(ClientMessage::Node(NodeRequest::JoinBufferAtNode { + request_id: self.client.next_request_id(), + node_id, + buffer_id, + placement, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::MoveNodeBefore { + node_id: Some(node_id), + sibling_node_id, + } => { + self.client + .request_message(ClientMessage::Node(NodeRequest::MoveNodeBefore { + request_id: self.client.next_request_id(), + node_id, + sibling_node_id, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + Action::MoveNodeAfter { + node_id: Some(node_id), + sibling_node_id, + } => { + self.client + .request_message(ClientMessage::Node(NodeRequest::MoveNodeAfter { + request_id: self.client.next_request_id(), + node_id, + sibling_node_id, + })) + .await?; + self.client.resync_all_sessions().await?; + Ok(None) + } + other => Ok(Some(other)), + } + } + async fn transition_mode( &mut self, mode: String, @@ -582,6 +931,7 @@ where } Action::SearchNext => self.navigate_search(presentation, true).await, Action::SearchPrev => self.navigate_search(presentation, false).await, + Action::CommitSearch => self.commit_search_prompt(session_id, viewport).await, Action::CancelSearch => self.cancel_search_prompt(session_id, viewport).await, Action::EnterSelect { kind } => { self.enter_select_mode(session_id, viewport, presentation, kind) @@ -888,9 +1238,122 @@ where .await?; self.client.resync_all_sessions().await } - Action::FocusBuffer { buffer_id } | Action::RevealBuffer { buffer_id } => { - self.focus_buffer(session_id, buffer_id).await + Action::ZoomNode { node_id: None } => { + let node_id = presentation + .focused_leaf() + .map(|leaf| leaf.node_id) + .ok_or_else(|| MuxError::invalid_input("no focused node to zoom"))?; + self.client + .request_message(ClientMessage::Node(NodeRequest::Zoom { + request_id: self.client.next_request_id(), + node_id, + })) + .await?; + self.client.resync_all_sessions().await + } + Action::ToggleZoomNode { node_id } => { + let node_id = node_id + .or_else(|| presentation.focused_leaf().map(|leaf| leaf.node_id)) + .ok_or_else(|| MuxError::invalid_input("no focused node to toggle zoom"))?; + self.client + .request_message(ClientMessage::Node(NodeRequest::ToggleZoom { + request_id: self.client.next_request_id(), + node_id, + })) + .await?; + self.client.resync_all_sessions().await + } + Action::SwapSiblingNodes { + first_node_id, + second_node_id, + } => { + let first_node_id = first_node_id + .or_else(|| presentation.focused_leaf().map(|leaf| leaf.node_id)) + .ok_or_else(|| MuxError::invalid_input("no focused node to swap"))?; + self.client + .request_message(ClientMessage::Node(NodeRequest::SwapSiblings { + request_id: self.client.next_request_id(), + first_node_id, + second_node_id, + })) + .await?; + self.client.resync_all_sessions().await + } + Action::BreakNode { + node_id, + destination, + } => { + let node_id = node_id + .or_else(|| presentation.focused_leaf().map(|leaf| leaf.node_id)) + .ok_or_else(|| MuxError::invalid_input("no focused node to break"))?; + self.client + .request_message(ClientMessage::Node(NodeRequest::BreakNode { + request_id: self.client.next_request_id(), + node_id, + destination, + })) + .await?; + self.client.resync_all_sessions().await + } + Action::JoinBufferAtNode { + node_id, + buffer_id, + placement, + } => { + let node_id = node_id + .or_else(|| presentation.focused_leaf().map(|leaf| leaf.node_id)) + .ok_or_else(|| { + MuxError::invalid_input("no focused node to join buffer into") + })?; + self.client + .request_message(ClientMessage::Node(NodeRequest::JoinBufferAtNode { + request_id: self.client.next_request_id(), + node_id, + buffer_id, + placement, + })) + .await?; + self.client.resync_all_sessions().await + } + Action::MoveNodeBefore { + node_id, + sibling_node_id, + } => { + let node_id = node_id + .or_else(|| presentation.focused_leaf().map(|leaf| leaf.node_id)) + .ok_or_else(|| MuxError::invalid_input("no focused node to reorder"))?; + self.client + .request_message(ClientMessage::Node(NodeRequest::MoveNodeBefore { + request_id: self.client.next_request_id(), + node_id, + sibling_node_id, + })) + .await?; + self.client.resync_all_sessions().await + } + Action::MoveNodeAfter { + node_id, + sibling_node_id, + } => { + let node_id = node_id + .or_else(|| presentation.focused_leaf().map(|leaf| leaf.node_id)) + .ok_or_else(|| MuxError::invalid_input("no focused node to reorder"))?; + self.client + .request_message(ClientMessage::Node(NodeRequest::MoveNodeAfter { + request_id: self.client.next_request_id(), + node_id, + sibling_node_id, + })) + .await?; + self.client.resync_all_sessions().await } + Action::FocusBuffer { .. } + | Action::RevealBuffer { .. } + | Action::OpenBufferHistory { .. } + | Action::UnzoomNode { .. } + | Action::ZoomNode { node_id: Some(_) } => Err(MuxError::invalid_input( + "action should be handled before presentation is required", + )), other => Err(MuxError::invalid_input(format!( "action '{other:?}' is not supported by the live executor yet" ))), @@ -1179,6 +1642,53 @@ where .map(|node| node.session_id) } + fn buffer_location_from_response( + response: ServerResponse, + context: &str, + ) -> Result { + match response { + ServerResponse::BufferLocation(response) => Ok(response.location), + ServerResponse::BufferWithLocation(response) => { + let (_, _, location, _) = response.into_parts(); + Ok(location) + } + other => Err(MuxError::protocol(format!( + "expected {context} response, got {other:?}" + ))), + } + } + + fn attached_buffer_location( + expected_buffer_id: Option, + location: BufferLocation, + context: &str, + ) -> Result<(SessionId, NodeId)> { + if let Some(expected_buffer_id) = expected_buffer_id + && location.buffer_id != expected_buffer_id + { + return Err(MuxError::protocol(format!( + "{context} returned location for buffer {} while acting on buffer {expected_buffer_id}", + location.buffer_id + ))); + } + + match location.attachment { + BufferLocationAttachment::Session { + session_id, + node_id, + } + | BufferLocationAttachment::Floating { + session_id, + node_id, + .. + } => Ok((session_id, node_id)), + BufferLocationAttachment::Detached => Err(MuxError::conflict(format!( + "{context} returned a detached location for buffer {}", + expected_buffer_id.unwrap_or(location.buffer_id) + ))), + } + } + fn set_active_view(&mut self, session_id: SessionId, viewport: Size) { self.active_session_id = Some(session_id); self.viewport = Some(viewport); @@ -1194,58 +1704,6 @@ where .unwrap_or(FallbackPolicy::Ignore) } - async fn focus_buffer(&mut self, session_id: SessionId, buffer_id: BufferId) -> Result<()> { - let node_id = self - .client - .state() - .buffers - .get(&buffer_id) - .and_then(|buffer| buffer.attachment_node_id) - .ok_or_else(|| MuxError::invalid_input(format!("buffer {buffer_id} is detached")))?; - - let mut selections = Vec::new(); - let mut child_id = node_id; - let mut parent_id = self - .client - .state() - .nodes - .get(&node_id) - .and_then(|node| node.parent_id); - while let Some(current_parent) = parent_id { - if let Some(tabs) = self - .client - .state() - .nodes - .get(¤t_parent) - .and_then(|node| node.tabs.as_ref()) - && let Some(index) = tabs.tabs.iter().position(|tab| tab.child_id == child_id) - { - selections.push((current_parent, index)); - } - child_id = current_parent; - parent_id = self - .client - .state() - .nodes - .get(¤t_parent) - .and_then(|node| node.parent_id); - } - selections.reverse(); - - for (tabs_node_id, index) in selections { - self.client - .request_message(ClientMessage::Node(NodeRequest::SelectTab { - request_id: self.client.next_request_id(), - tabs_node_id, - index: u32::try_from(index).map_err(|_| { - MuxError::invalid_input("tab index exceeds protocol limits") - })?, - })) - .await?; - } - self.focus_node(session_id, node_id).await - } - fn focused_leaf<'a>(&self, presentation: &'a PresentationModel) -> Result<&'a LeafFrame> { presentation .focused_leaf() @@ -1277,6 +1735,9 @@ where presentation: &PresentationModel, actions: &[Action], ) -> bool { + // When the focused pane is acting like a live terminal surface, prefer + // forwarding local search/select/navigation bindings to the program + // instead of stealing keys that fullscreen apps expect to receive. let Some(leaf) = presentation.focused_leaf() else { return false; }; @@ -1454,8 +1915,14 @@ where } Ok(()) } - KeyEvent::Enter => self.commit_search_prompt(session_id, viewport).await, - KeyEvent::Escape => self.cancel_search_prompt(session_id, viewport).await, + KeyEvent::Enter => { + self.execute_actions(Some(session_id), Some(viewport), vec![Action::CommitSearch]) + .await + } + KeyEvent::Escape => { + self.execute_actions(Some(session_id), Some(viewport), vec![Action::CancelSearch]) + .await + } KeyEvent::Bytes(bytes) => { if let Some(prompt) = &mut self.search_prompt { prompt.query.push_str(&String::from_utf8_lossy(&bytes)); @@ -1780,18 +2247,31 @@ where } async fn focus_node(&mut self, session_id: SessionId, node_id: NodeId) -> Result<()> { - if self - .client - .state() - .sessions - .get(&session_id) - .and_then(|session| session.focused_leaf_id) - == Some(node_id) + self.focus_node_with_shortcut(session_id, node_id, true) + .await + } + + async fn focus_node_with_shortcut( + &mut self, + session_id: SessionId, + node_id: NodeId, + allow_same_session_shortcut: bool, + ) -> Result<()> { + if allow_same_session_shortcut + && self + .client + .state() + .sessions + .get(&session_id) + .and_then(|session| session.focused_leaf_id) + == Some(node_id) { return Ok(()); } - let previous_buffer = self.focused_buffer_for_session(session_id); + let previous_buffer = self + .active_session_id + .and_then(|active_session_id| self.focused_buffer_for_session(active_session_id)); let sent_focus_out = if let Some(buffer_id) = previous_buffer { self.maybe_send_focus_sequence(buffer_id, false).await? } else { @@ -1903,6 +2383,7 @@ fn action_is_local_terminal_action(action: &Action) -> bool { | Action::EnterSearchMode | Action::SearchNext | Action::SearchPrev + | Action::CommitSearch | Action::CancelSearch | Action::EnterSelect { .. } | Action::SelectMove { .. } @@ -2292,7 +2773,11 @@ fn event_name(event: &ServerEvent) -> &'static str { } } -fn event_info(name: &str, event: &ServerEvent) -> EventInfo { +fn event_info( + name: &str, + event: &ServerEvent, + fallback_session_id: Option, +) -> EventInfo { let mut info = base_event_info(name); match event { ServerEvent::SessionCreated(event) => info.session_id = Some(event.session.id), @@ -2304,7 +2789,10 @@ fn event_info(name: &str, event: &ServerEvent) -> EventInfo { } ServerEvent::BufferDetached(event) => info.buffer_id = Some(event.buffer_id), ServerEvent::NodeChanged(event) => info.session_id = Some(event.session_id), - ServerEvent::FloatingChanged(event) => info.session_id = Some(event.session_id), + ServerEvent::FloatingChanged(event) => { + info.session_id = Some(event.session_id); + info.floating_id = event.floating_id; + } ServerEvent::FocusChanged(event) => { info.session_id = Some(event.session_id); info.node_id = event.focused_leaf_id; @@ -2317,6 +2805,9 @@ fn event_info(name: &str, event: &ServerEvent) -> EventInfo { info.client_id = Some(event.client.id); } } + if info.session_id.is_none() { + info.session_id = fallback_session_id; + } info } @@ -2331,3 +2822,163 @@ fn base_event_info(name: &str) -> EventInfo { floating_id: None, } } + +#[cfg(test)] +mod tests { + use std::fs; + + use embers_core::{ActivityState, BufferId, FloatingId, NodeId, PtySize, RequestId, SessionId}; + use embers_protocol::{ + BufferLocation, BufferLocationAttachment, BufferRecord, BufferRecordKind, + BufferRecordState, BufferWithLocationResponse, FloatingChangedEvent, ServerEvent, + ServerResponse, + }; + use tempfile::tempdir; + + use super::{ConfiguredClient, SearchPrompt, event_info}; + use crate::client::MuxClient; + use crate::config::{ConfigDiscoveryOptions, ConfigManager}; + use crate::input::NORMAL_MODE; + use crate::testing::FakeTransport; + + #[test] + fn attached_buffer_location_accepts_session_and_floating_locations() { + assert_eq!( + ConfiguredClient::::attached_buffer_location( + Some(BufferId(7)), + BufferLocation::session(BufferId(7), SessionId(2), NodeId(5)), + "buffer focus", + ) + .expect("session attachment should validate"), + (SessionId(2), NodeId(5)) + ); + assert_eq!( + ConfiguredClient::::attached_buffer_location( + Some(BufferId(7)), + BufferLocation::floating(BufferId(7), SessionId(2), NodeId(5), FloatingId(9)), + "buffer reveal", + ) + .expect("floating attachment should validate"), + (SessionId(2), NodeId(5)) + ); + } + + #[test] + fn attached_buffer_location_accepts_history_helper_locations_without_source_id_match() { + assert_eq!( + ConfiguredClient::::attached_buffer_location( + None, + BufferLocation::session(BufferId(8), SessionId(2), NodeId(5)), + "buffer history", + ) + .expect("history helper attachment should validate"), + (SessionId(2), NodeId(5)) + ); + } + + #[test] + fn attached_buffer_location_rejects_mismatched_or_detached_locations() { + let mismatch = ConfiguredClient::::attached_buffer_location( + Some(BufferId(7)), + BufferLocation::session(BufferId(8), SessionId(2), NodeId(5)), + "buffer focus", + ) + .expect_err("mismatched buffer ids should fail"); + let detached = ConfiguredClient::::attached_buffer_location( + Some(BufferId(7)), + BufferLocation { + buffer_id: BufferId(7), + attachment: BufferLocationAttachment::Detached, + }, + "buffer reveal", + ) + .expect_err("detached locations should fail"); + + assert!( + mismatch + .to_string() + .contains("returned location for buffer 8") + ); + assert!(detached.to_string().contains("detached location")); + } + + #[test] + fn buffer_location_from_response_accepts_buffer_with_location() { + let location = BufferLocation::session(BufferId(8), SessionId(2), NodeId(5)); + let response = ServerResponse::BufferWithLocation( + BufferWithLocationResponse::new( + RequestId(1), + BufferRecord { + id: BufferId(8), + title: "helper".to_owned(), + command: Vec::new(), + cwd: None, + kind: BufferRecordKind::Helper, + state: BufferRecordState::Created, + pid: None, + attachment_node_id: Some(NodeId(5)), + read_only: true, + helper_source_buffer_id: Some(BufferId(7)), + helper_scope: Some(embers_protocol::BufferHistoryScope::Visible), + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + location, + false, + ) + .expect("buffer and location ids should match"), + ); + + assert_eq!( + ConfiguredClient::::buffer_location_from_response( + response, + "buffer history", + ) + .expect("buffer-with-location response should validate"), + location + ); + } + + #[test] + fn floating_changed_event_info_includes_floating_id() { + let info = event_info( + "floating_changed", + &ServerEvent::FloatingChanged(FloatingChangedEvent { + session_id: SessionId(2), + floating_id: Some(FloatingId(9)), + }), + None, + ); + + assert_eq!(info.session_id, Some(SessionId(2))); + assert_eq!(info.floating_id, Some(FloatingId(9))); + } + + #[test] + fn finish_config_reload_clears_search_prompt_when_mode_falls_back_to_normal() { + let tempdir = tempdir().expect("create tempdir"); + let config_path = tempdir.path().join("config.rhai"); + fs::write(&config_path, "define_mode(\"custom\");").expect("write custom config"); + let config = ConfigManager::load( + ConfigDiscoveryOptions::default().with_project_config_dir(tempdir.path()), + ) + .expect("load config"); + let client = MuxClient::new(FakeTransport::default()); + let mut configured = ConfiguredClient::new(client, config); + configured.input_state.set_mode("custom".to_owned()); + configured.search_prompt = Some(SearchPrompt { + node_id: NodeId(7), + query: "stale".to_owned(), + }); + + fs::write(&config_path, "").expect("rewrite config without custom mode"); + + configured.reload_config().expect("reload config"); + + assert_eq!(configured.input_state.current_mode(), NORMAL_MODE); + assert_eq!(configured.search_prompt, None); + } +} diff --git a/crates/embers-client/src/grid.rs b/crates/embers-client/src/grid.rs index 312076e..de8d913 100644 --- a/crates/embers-client/src/grid.rs +++ b/crates/embers-client/src/grid.rs @@ -30,6 +30,7 @@ pub struct CellStyle { pub underline: bool, pub dim: bool, pub reverse: bool, + pub blink: bool, } impl CellStyle { @@ -43,6 +44,16 @@ impl CellStyle { self } + pub const fn with_italic(mut self) -> Self { + self.italic = true; + self + } + + pub const fn with_blink(mut self) -> Self { + self.blink = true; + self + } + pub fn is_plain(self) -> bool { self == Self::default() } @@ -58,6 +69,7 @@ impl From<&crate::scripting::StyleSpec> for CellStyle { underline: value.underline, dim: value.dim, reverse: false, + blink: value.blink, } } } @@ -386,6 +398,9 @@ fn write_style_transition(output: &mut String, from: CellStyle, to: CellStyle) { if to.underline { output.push_str("\x1b[4m"); } + if to.blink { + output.push_str("\x1b[5m"); + } if to.reverse { output.push_str("\x1b[7m"); } diff --git a/crates/embers-client/src/input/keymap.rs b/crates/embers-client/src/input/keymap.rs index 6cf8f55..5a99b99 100644 --- a/crates/embers-client/src/input/keymap.rs +++ b/crates/embers-client/src/input/keymap.rs @@ -144,6 +144,52 @@ mod tests { ); } + #[test] + fn prefix_match_keeps_pending_sequence_until_it_resolves() { + let mut state = InputState::default(); + let bindings = bindings(&[("normal", "ab", "target")]); + let modes = builtin_modes(); + + assert_eq!( + resolve_key(&bindings, &modes, &mut state, KeyToken::Char('a')), + InputResolution::PrefixMatch + ); + assert_eq!(state.pending_sequence(), &[KeyToken::Char('a')]); + + assert_eq!( + resolve_key(&bindings, &modes, &mut state, KeyToken::Char('b')), + InputResolution::ExactMatch(super::BindingMatch { + mode: "normal".to_owned(), + sequence: vec![KeyToken::Char('a'), KeyToken::Char('b')], + target: "target".to_owned(), + }) + ); + assert!(state.pending_sequence().is_empty()); + } + + #[test] + fn unmatched_sequence_clears_pending_state_after_prefix_miss() { + let mut state = InputState::default(); + let bindings = bindings(&[("normal", "ab", "target")]); + let modes = builtin_modes(); + + assert_eq!( + resolve_key(&bindings, &modes, &mut state, KeyToken::Char('a')), + InputResolution::PrefixMatch + ); + assert_eq!(state.pending_sequence(), &[KeyToken::Char('a')]); + + assert_eq!( + resolve_key(&bindings, &modes, &mut state, KeyToken::Char('x')), + InputResolution::Unmatched { + mode: "normal".to_owned(), + sequence: vec![KeyToken::Char('a'), KeyToken::Char('x')], + fallback_policy: FallbackPolicy::Passthrough, + } + ); + assert!(state.pending_sequence().is_empty()); + } + fn bindings(entries: &[(&str, &str, &str)]) -> BTreeMap>> { let mut bindings = BTreeMap::>>::new(); for (mode, sequence, target) in entries { diff --git a/crates/embers-client/src/presentation.rs b/crates/embers-client/src/presentation.rs index b36a720..01dcb85 100644 --- a/crates/embers-client/src/presentation.rs +++ b/crates/embers-client/src/presentation.rs @@ -4,7 +4,7 @@ use embers_core::{ ActivityState, BufferId, FloatGeometry, FloatingId, MuxError, NodeId, Point, Rect, Result, SessionId, Size, SplitDirection, }; -use embers_protocol::{NodeRecordKind, SessionRecord}; +use embers_protocol::{FloatingRecord, NodeRecordKind, SessionRecord}; use crate::state::ClientState; @@ -92,38 +92,28 @@ impl PresentationModel { activity_by_node: BTreeMap::new(), buffer_count_by_node: BTreeMap::new(), }; - projector.project_node(session.root_node_id, root_bounds, None, true, Vec::new())?; - - let overlay_bounds = root_bounds; - for floating_id in &session.floating_ids { - let Some(window) = state.floating.get(floating_id) else { - continue; - }; - if !window.visible { - continue; + if let Some(zoomed_node_id) = session + .zoomed_node_id + .filter(|node_id| node_belongs_to_session(state, session, *node_id)) + { + if let Some(window) = floating_ancestor_for_node(state, session, zoomed_node_id) { + project_floating_window(&mut projector, window, root_bounds, zoomed_node_id)?; + } else { + let is_root = zoomed_node_id == session.root_node_id; + projector.project_node(zoomed_node_id, root_bounds, None, is_root, Vec::new())?; } + } else { + projector.project_node(session.root_node_id, root_bounds, None, true, Vec::new())?; - let rect = clip_rect(geometry_rect(window.geometry), overlay_bounds); - if rect.size.width == 0 || rect.size.height == 0 { - continue; + for floating_id in &session.floating_ids { + let Some(window) = state.floating.get(floating_id) else { + continue; + }; + if !node_belongs_to_session(state, session, window.root_node_id) { + continue; + } + project_floating_window(&mut projector, window, root_bounds, window.root_node_id)?; } - - let content_rect = inset_border(rect); - projector.projection.floating.push(FloatingFrame { - floating_id: window.id, - rect, - content_rect, - title: window.title.clone(), - focused: window.focused, - }); - - projector.project_node( - window.root_node_id, - content_rect, - Some(window.id), - false, - Vec::new(), - )?; } let root_tabs = projection.tab_bars.iter().find(|bar| bar.is_root).cloned(); @@ -197,6 +187,53 @@ impl PresentationModel { } } +fn node_belongs_to_session(state: &ClientState, session: &SessionRecord, node_id: NodeId) -> bool { + let mut current = Some(node_id); + while let Some(candidate) = current { + let Some(node) = state.nodes.get(&candidate) else { + return false; + }; + if node.session_id != session.id { + return false; + } + if candidate == session.root_node_id { + return true; + } + if floating_window_for_root(state, session, candidate).is_some() { + return true; + } + current = node.parent_id; + } + false +} + +fn floating_window_for_root<'a>( + state: &'a ClientState, + session: &SessionRecord, + root_node_id: NodeId, +) -> Option<&'a FloatingRecord> { + session + .floating_ids + .iter() + .filter_map(|floating_id| state.floating.get(floating_id)) + .find(|window| window.root_node_id == root_node_id) +} + +fn floating_ancestor_for_node<'a>( + state: &'a ClientState, + session: &SessionRecord, + node_id: NodeId, +) -> Option<&'a FloatingRecord> { + let mut current = Some(node_id); + while let Some(candidate) = current { + if let Some(window) = floating_window_for_root(state, session, candidate) { + return Some(window); + } + current = state.nodes.get(&candidate).and_then(|node| node.parent_id); + } + None +} + #[derive(Default)] struct Projection { tab_bars: Vec, @@ -628,6 +665,550 @@ fn inset_top(rect: Rect, amount: u16) -> Rect { } } +#[cfg(test)] +mod zoom_tests { + use super::PresentationModel; + use crate::state::ClientState; + use embers_core::{ + ActivityState, BufferId, FloatGeometry, FloatingId, NodeId, PtySize, SessionId, Size, + }; + use embers_protocol::{ + BufferRecord, BufferRecordKind, BufferRecordState, BufferViewRecord, FloatingRecord, + NodeRecord, NodeRecordKind, SessionRecord, SplitRecord, TabRecord, TabsRecord, + }; + + #[test] + fn zoomed_node_projects_only_the_zoomed_subtree() { + let mut state = ClientState::default(); + state.sessions.insert( + SessionId(1), + SessionRecord { + id: SessionId(1), + name: "main".to_owned(), + root_node_id: NodeId(1), + floating_ids: Vec::new(), + focused_leaf_id: Some(NodeId(3)), + focused_floating_id: None, + zoomed_node_id: Some(NodeId(3)), + }, + ); + state.nodes.insert( + NodeId(1), + NodeRecord { + id: NodeId(1), + session_id: SessionId(1), + parent_id: None, + kind: NodeRecordKind::Split, + buffer_view: None, + split: Some(SplitRecord { + direction: embers_core::SplitDirection::Vertical, + child_ids: vec![NodeId(2), NodeId(3)], + sizes: vec![1, 1], + }), + tabs: None, + }, + ); + for (node_id, buffer_id, focused) in [ + (NodeId(2), BufferId(10), false), + (NodeId(3), BufferId(11), true), + ] { + state.nodes.insert( + node_id, + NodeRecord { + id: node_id, + session_id: SessionId(1), + parent_id: Some(NodeId(1)), + kind: NodeRecordKind::BufferView, + buffer_view: Some(BufferViewRecord { + buffer_id, + focused, + zoomed: node_id == NodeId(3), + follow_output: true, + last_render_size: PtySize::new(80, 24), + }), + split: None, + tabs: None, + }, + ); + state.buffers.insert( + buffer_id, + BufferRecord { + id: buffer_id, + title: format!("buffer-{buffer_id}"), + command: vec!["sh".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: Some(1), + attachment_node_id: Some(node_id), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + ); + } + + let presentation = PresentationModel::project( + &state, + SessionId(1), + Size { + width: 80, + height: 24, + }, + ) + .expect("project zoomed session"); + assert_eq!(presentation.leaves.len(), 1); + assert_eq!(presentation.leaves[0].node_id, NodeId(3)); + } + + #[test] + fn missing_zoomed_node_falls_back_to_normal_projection() { + let mut state = ClientState::default(); + state.sessions.insert( + SessionId(1), + SessionRecord { + id: SessionId(1), + name: "main".to_owned(), + root_node_id: NodeId(1), + floating_ids: Vec::new(), + focused_leaf_id: Some(NodeId(3)), + focused_floating_id: None, + zoomed_node_id: Some(NodeId(99)), + }, + ); + state.nodes.insert( + NodeId(1), + NodeRecord { + id: NodeId(1), + session_id: SessionId(1), + parent_id: None, + kind: NodeRecordKind::Split, + buffer_view: None, + split: Some(SplitRecord { + direction: embers_core::SplitDirection::Vertical, + child_ids: vec![NodeId(2), NodeId(3)], + sizes: vec![1, 1], + }), + tabs: None, + }, + ); + for (node_id, buffer_id, focused) in [ + (NodeId(2), BufferId(10), false), + (NodeId(3), BufferId(11), true), + ] { + state.nodes.insert( + node_id, + NodeRecord { + id: node_id, + session_id: SessionId(1), + parent_id: Some(NodeId(1)), + kind: NodeRecordKind::BufferView, + buffer_view: Some(BufferViewRecord { + buffer_id, + focused, + zoomed: false, + follow_output: true, + last_render_size: PtySize::new(80, 24), + }), + split: None, + tabs: None, + }, + ); + state.buffers.insert( + buffer_id, + BufferRecord { + id: buffer_id, + title: format!("buffer-{buffer_id}"), + command: vec!["sh".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: Some(1), + attachment_node_id: Some(node_id), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + ); + } + + let presentation = PresentationModel::project( + &state, + SessionId(1), + Size { + width: 80, + height: 24, + }, + ) + .expect("project with missing zoom node"); + + assert_eq!(presentation.leaves.len(), 2); + assert!(presentation.root_tabs.is_none()); + assert!(presentation.floating.is_empty()); + } + + #[test] + fn hidden_zoomed_floating_is_not_projected() { + let mut state = ClientState::default(); + state.sessions.insert( + SessionId(1), + SessionRecord { + id: SessionId(1), + name: "main".to_owned(), + root_node_id: NodeId(1), + floating_ids: vec![FloatingId(30)], + focused_leaf_id: Some(NodeId(3)), + focused_floating_id: Some(FloatingId(30)), + zoomed_node_id: Some(NodeId(3)), + }, + ); + for (node_id, buffer_id, focused, zoomed) in [ + (NodeId(1), BufferId(10), false, false), + (NodeId(3), BufferId(11), true, true), + ] { + state.nodes.insert( + node_id, + NodeRecord { + id: node_id, + session_id: SessionId(1), + parent_id: None, + kind: NodeRecordKind::BufferView, + buffer_view: Some(BufferViewRecord { + buffer_id, + focused, + zoomed, + follow_output: true, + last_render_size: PtySize::new(80, 24), + }), + split: None, + tabs: None, + }, + ); + state.buffers.insert( + buffer_id, + BufferRecord { + id: buffer_id, + title: format!("buffer-{buffer_id}"), + command: vec!["sh".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: Some(1), + attachment_node_id: Some(node_id), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + ); + } + state.floating.insert( + FloatingId(30), + FloatingRecord { + id: FloatingId(30), + session_id: SessionId(1), + root_node_id: NodeId(3), + title: Some("hidden".to_owned()), + geometry: FloatGeometry::new(4, 4, 20, 10), + focused: true, + visible: false, + close_on_empty: true, + }, + ); + + let presentation = PresentationModel::project( + &state, + SessionId(1), + Size { + width: 80, + height: 24, + }, + ) + .expect("project hidden zoomed floating"); + assert!(presentation.floating.is_empty()); + assert!(presentation.leaves.is_empty()); + assert!(presentation.root_tabs.is_none()); + assert_eq!(presentation.focused_floating_id(), None); + } + + #[test] + fn zoomed_descendant_of_floating_root_keeps_floating_projection() { + let mut state = ClientState::default(); + state.sessions.insert( + SessionId(1), + SessionRecord { + id: SessionId(1), + name: "main".to_owned(), + root_node_id: NodeId(1), + floating_ids: vec![FloatingId(30)], + focused_leaf_id: Some(NodeId(12)), + focused_floating_id: Some(FloatingId(30)), + zoomed_node_id: Some(NodeId(12)), + }, + ); + state.nodes.insert( + NodeId(1), + NodeRecord { + id: NodeId(1), + session_id: SessionId(1), + parent_id: None, + kind: NodeRecordKind::BufferView, + buffer_view: Some(BufferViewRecord { + buffer_id: BufferId(10), + focused: false, + zoomed: false, + follow_output: true, + last_render_size: PtySize::new(80, 24), + }), + split: None, + tabs: None, + }, + ); + state.nodes.insert( + NodeId(10), + NodeRecord { + id: NodeId(10), + session_id: SessionId(1), + parent_id: None, + kind: NodeRecordKind::Split, + buffer_view: None, + split: Some(SplitRecord { + direction: embers_core::SplitDirection::Vertical, + child_ids: vec![NodeId(11), NodeId(12)], + sizes: vec![1, 1], + }), + tabs: None, + }, + ); + for (node_id, buffer_id, focused, zoomed) in [ + (NodeId(11), BufferId(11), false, false), + (NodeId(12), BufferId(12), true, true), + ] { + state.nodes.insert( + node_id, + NodeRecord { + id: node_id, + session_id: SessionId(1), + parent_id: Some(NodeId(10)), + kind: NodeRecordKind::BufferView, + buffer_view: Some(BufferViewRecord { + buffer_id, + focused, + zoomed, + follow_output: true, + last_render_size: PtySize::new(80, 24), + }), + split: None, + tabs: None, + }, + ); + state.buffers.insert( + buffer_id, + BufferRecord { + id: buffer_id, + title: format!("buffer-{buffer_id}"), + command: vec!["sh".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: Some(1), + attachment_node_id: Some(node_id), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + ); + } + state.buffers.insert( + BufferId(10), + BufferRecord { + id: BufferId(10), + title: "buffer-10".to_owned(), + command: vec!["sh".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: Some(1), + attachment_node_id: Some(NodeId(1)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + ); + state.floating.insert( + FloatingId(30), + FloatingRecord { + id: FloatingId(30), + session_id: SessionId(1), + root_node_id: NodeId(10), + title: Some("popup".to_owned()), + geometry: FloatGeometry::new(4, 4, 20, 10), + focused: true, + visible: true, + close_on_empty: true, + }, + ); + + let presentation = PresentationModel::project( + &state, + SessionId(1), + Size { + width: 80, + height: 24, + }, + ) + .expect("project zoomed floating descendant"); + + assert_eq!(presentation.floating.len(), 1); + assert_eq!(presentation.floating[0].floating_id, FloatingId(30)); + assert_eq!(presentation.leaves.len(), 1); + assert_eq!(presentation.leaves[0].node_id, NodeId(12)); + assert_eq!(presentation.leaves[0].floating_id, Some(FloatingId(30))); + assert_eq!(presentation.focused_floating_id(), Some(FloatingId(30))); + } + + #[test] + fn zoomed_root_tabs_preserve_root_tabs_frame() { + let mut state = ClientState::default(); + state.sessions.insert( + SessionId(1), + SessionRecord { + id: SessionId(1), + name: "main".to_owned(), + root_node_id: NodeId(1), + floating_ids: Vec::new(), + focused_leaf_id: Some(NodeId(2)), + focused_floating_id: None, + zoomed_node_id: Some(NodeId(1)), + }, + ); + state.nodes.insert( + NodeId(1), + NodeRecord { + id: NodeId(1), + session_id: SessionId(1), + parent_id: None, + kind: NodeRecordKind::Tabs, + buffer_view: None, + split: None, + tabs: Some(TabsRecord { + active: 0, + tabs: vec![TabRecord { + title: "main".to_owned(), + child_id: NodeId(2), + }], + }), + }, + ); + state.nodes.insert( + NodeId(2), + NodeRecord { + id: NodeId(2), + session_id: SessionId(1), + parent_id: Some(NodeId(1)), + kind: NodeRecordKind::BufferView, + buffer_view: Some(BufferViewRecord { + buffer_id: BufferId(10), + focused: true, + zoomed: false, + follow_output: true, + last_render_size: PtySize::new(80, 24), + }), + split: None, + tabs: None, + }, + ); + state.buffers.insert( + BufferId(10), + BufferRecord { + id: BufferId(10), + title: "buffer-10".to_owned(), + command: vec!["sh".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: Some(1), + attachment_node_id: Some(NodeId(2)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + ); + + let presentation = PresentationModel::project( + &state, + SessionId(1), + Size { + width: 80, + height: 24, + }, + ) + .expect("project zoomed root tabs"); + + let root_tabs = presentation + .root_tabs + .as_ref() + .expect("root tabs frame exists"); + assert_eq!(root_tabs.node_id, NodeId(1)); + assert!(root_tabs.is_root); + } +} + +fn project_floating_window( + projector: &mut Projector<'_>, + window: &FloatingRecord, + bounds: Rect, + node_id: NodeId, +) -> Result<()> { + if !window.visible { + return Ok(()); + } + + let rect = clip_rect(geometry_rect(window.geometry), bounds); + if rect.size.width == 0 || rect.size.height == 0 { + return Ok(()); + } + + let content_rect = inset_border(rect); + projector.projection.floating.push(FloatingFrame { + floating_id: window.id, + rect, + content_rect, + title: window.title.clone(), + focused: window.focused, + }); + projector.project_node(node_id, content_rect, Some(window.id), false, Vec::new()) +} + fn inset_border(rect: Rect) -> Rect { Rect { origin: Point { diff --git a/crates/embers-client/src/renderer.rs b/crates/embers-client/src/renderer.rs index 4219577..c6ea613 100644 --- a/crates/embers-client/src/renderer.rs +++ b/crates/embers-client/src/renderer.rs @@ -185,9 +185,18 @@ impl Renderer { } let view_state = state.view_state(leaf.node_id); - let lines = view_state - .map(|view| view.visible_lines.as_slice()) - .filter(|lines| !lines.is_empty()) + let rendered_lines = view_state + .map(|view| { + if view.visible_lines.is_empty() { + state + .snapshots + .get(&leaf.buffer_id) + .map(|snapshot| snapshot.lines.as_slice()) + .unwrap_or(&[]) + } else { + view.visible_lines.as_slice() + } + }) .or_else(|| { state .snapshots @@ -195,10 +204,34 @@ impl Renderer { .map(|snapshot| snapshot.lines.as_slice()) }); - if let Some(lines) = lines { + let content_rows = usize::from(height.saturating_sub(1)); + let display_offset = view_state + .filter(|view| view.follow_output) + .map(|_| { + rendered_lines.map_or(0, |lines| { + let significant_len = lines + .iter() + .rposition(|line| !line.is_empty()) + .map(|index| index + 1) + .unwrap_or(0); + significant_len.saturating_sub(content_rows) + }) + }) + .unwrap_or(0); + let displayed_top_line = view_state + .map(|view| { + view.scroll_top_line + .saturating_add(u64::try_from(display_offset).unwrap_or(u64::MAX)) + }) + .unwrap_or(0); + let displayed_view_lines = + rendered_lines.map(|lines| &lines[display_offset.min(lines.len())..]); + + if let Some(lines) = rendered_lines { for (row, line) in lines .iter() - .take(usize::from(height.saturating_sub(1))) + .skip(display_offset) + .take(content_rows) .enumerate() { let Some(row) = u16::try_from(row).ok() else { @@ -217,8 +250,8 @@ impl Renderer { x, y + 1, width, - view_state.scroll_top_line, - &view_state.visible_lines, + displayed_top_line, + displayed_view_lines.unwrap_or(rendered_lines.unwrap_or(&[])), search_state, ); } @@ -228,8 +261,8 @@ impl Renderer { x, y + 1, width, - view_state.scroll_top_line, - &view_state.visible_lines, + displayed_top_line, + displayed_view_lines.unwrap_or(rendered_lines.unwrap_or(&[])), selection_state, ); } @@ -664,6 +697,7 @@ fn scroll_indicator_style() -> CellStyle { fn search_style() -> CellStyle { CellStyle { underline: true, + italic: true, ..CellStyle::default() } } @@ -672,6 +706,7 @@ fn active_search_style() -> CellStyle { CellStyle { underline: true, reverse: true, + italic: true, ..CellStyle::default() } } diff --git a/crates/embers-client/src/scripting/documentation.rs b/crates/embers-client/src/scripting/documentation.rs index 5abf4bb..1e968c1 100644 --- a/crates/embers-client/src/scripting/documentation.rs +++ b/crates/embers-client/src/scripting/documentation.rs @@ -287,7 +287,7 @@ fn write_page_set( .ok_or_else(|| format!("missing rendered page for {}", page.title))?; fs::write( output_dir.join(page.file), - trim_trailing_whitespace(&content), + trim_trailing_whitespace(&rewrite_generated_markdown(&content)), )?; } Ok(()) @@ -305,6 +305,73 @@ fn trim_trailing_whitespace(content: &str) -> String { trimmed } +fn rewrite_generated_markdown(content: &str) -> String { + rewrite_signature_literals(content) +} + +#[cfg(test)] +fn rewrite_generated_defs(path: &Path) -> Result<(), Box> { + let content = fs::read_to_string(path)?; + fs::write(path, rewrite_signature_literals(&content))?; + Ok(()) +} + +// Rhai emits these signatures with generic `String`/`string` parameters, so docs generation +// rewrites the exact `break_current_node`, `join_buffer_here`, `open_buffer_history`, `notify`, +// and `split_with` patterns into literal unions after the fact. Both the markdown forms without +// trailing semicolons and the defs forms with trailing semicolons are matched here, so upstream +// Rhai signature changes will break these replacements and should be updated together. +const SIGNATURE_REWRITES: &[(&str, &str)] = &[ + ( + "fn break_current_node(_: ActionApi, destination: String) -> Action", + "fn break_current_node(_: ActionApi, destination: \"tab\" | \"floating\") -> Action", + ), + ( + "fn join_buffer_here(_: ActionApi, buffer_id: int, placement: String) -> Action", + "fn join_buffer_here(_: ActionApi, buffer_id: int, placement: \"tab-after\" | \"tab-before\" | \"left\" | \"right\" | \"up\" | \"down\") -> Action", + ), + ( + "fn open_buffer_history(_: ActionApi, buffer_id: int, scope: String, placement: String) -> Action", + "fn open_buffer_history(_: ActionApi, buffer_id: int, scope: \"visible\" | \"full\", placement: \"floating\" | \"tab\") -> Action", + ), + ( + "fn notify(_: ActionApi, level: String, message: String) -> Action", + "fn notify(_: ActionApi, level: \"info\" | \"warn\" | \"error\", message: String) -> Action", + ), + ( + "fn split_with(_: ActionApi, direction: String, tree: TreeSpec) -> Action", + "fn split_with(_: ActionApi, direction: \"h\" | \"horizontal\" | \"v\" | \"vertical\", tree: TreeSpec) -> Action", + ), + ( + "fn break_current_node(_: ActionApi, destination: string) -> Action;", + "fn break_current_node(_: ActionApi, destination: \"tab\" | \"floating\") -> Action;", + ), + ( + "fn join_buffer_here(_: ActionApi, buffer_id: int, placement: string) -> Action;", + "fn join_buffer_here(_: ActionApi, buffer_id: int, placement: \"tab-after\" | \"tab-before\" | \"left\" | \"right\" | \"up\" | \"down\") -> Action;", + ), + ( + "fn open_buffer_history(_: ActionApi, buffer_id: int, scope: string, placement: string) -> Action;", + "fn open_buffer_history(_: ActionApi, buffer_id: int, scope: \"visible\" | \"full\", placement: \"floating\" | \"tab\") -> Action;", + ), + ( + "fn notify(_: ActionApi, level: string, message: string) -> Action;", + "fn notify(_: ActionApi, level: \"info\" | \"warn\" | \"error\", message: string) -> Action;", + ), + ( + "fn split_with(_: ActionApi, direction: string, tree: TreeSpec) -> Action;", + "fn split_with(_: ActionApi, direction: \"h\" | \"horizontal\" | \"v\" | \"vertical\", tree: TreeSpec) -> Action;", + ), +]; + +fn rewrite_signature_literals(content: &str) -> String { + let mut rewritten = content.to_owned(); + for (source, target) in SIGNATURE_REWRITES { + rewritten = rewritten.replace(source, target); + } + rewritten +} + fn filter_item_for_page(item: &Item, page: &PageSpec<'_>) -> Option { if let Some(receiver) = page.receiver { let Item::Function { @@ -578,9 +645,69 @@ pub fn build_mdbook(output_dir: &Path) -> Result<(), Box> { return Err(format!("mdbook build failed for {}", output_dir.display()).into()); } + postprocess_mdbook_output(&build_dir)?; + + Ok(()) +} + +fn postprocess_mdbook_output(build_dir: &Path) -> Result<(), Box> { + for entry in fs::read_dir(build_dir)? { + let entry = entry?; + let path = entry.path(); + if !path.is_file() { + continue; + } + let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; + if file_name.starts_with("searcher-") && file_name.ends_with(".js") { + let content = fs::read_to_string(&path)?; + fs::write(&path, rewrite_mdbook_searcher_js(&content))?; + } else if file_name.starts_with("book-") && file_name.ends_with(".js") { + let content = fs::read_to_string(&path)?; + fs::write(&path, rewrite_mdbook_book_js(&content))?; + } else if file_name.starts_with("searchindex-") && file_name.ends_with(".js") { + let content = fs::read_to_string(&path)?; + fs::write(&path, rewrite_mdbook_searchindex_js(&content))?; + } + } + Ok(()) } +fn rewrite_mdbook_searcher_js(content: &str) -> String { + content + .replace( + "window.search = window.search || {};", + "const EMBERS_DOC_SEARCH =\n window.__EMBERS_CONFIG_API_SEARCH__ || (window.__EMBERS_CONFIG_API_SEARCH__ = {});", + ) + .replace("initSearchInteractions(window.search);", "initSearchInteractions(EMBERS_DOC_SEARCH);") + .replace("script.onload = () => init(window.search);", "script.onload = () => init(EMBERS_DOC_SEARCH);") + .replace("})(window.search);", "})(EMBERS_DOC_SEARCH);") +} + +fn rewrite_mdbook_book_js(content: &str) -> String { + content.replace( + "if (window.search && window.search.hasFocus()) {", + "if (window.__EMBERS_CONFIG_API_SEARCH__\n && window.__EMBERS_CONFIG_API_SEARCH__.hasFocus()) {", + ) +} + +fn rewrite_mdbook_searchindex_js(content: &str) -> String { + let prefix = "window.search = Object.assign(window.search, JSON.parse('"; + let suffix = "'));"; + let Some(rest) = content.strip_prefix(prefix) else { + return content.to_owned(); + }; + let Some(json) = rest.strip_suffix(suffix) else { + return content.to_owned(); + }; + + format!( + "(function(){{const target=(window.__EMBERS_CONFIG_API_SEARCH__&&typeof window.__EMBERS_CONFIG_API_SEARCH__===\"object\")?window.__EMBERS_CONFIG_API_SEARCH__:(window.__EMBERS_CONFIG_API_SEARCH__={{}});let parsed={{}};try{{parsed=JSON.parse('{json}');}}catch(error){{console.error(\"Failed to parse Embers config API search index:\", error);}}Object.assign(target, parsed);}})();" + ) +} + fn example_page() -> &'static str { r##"# Example @@ -624,3 +751,85 @@ mouse.set_click_focus(true); ``` "## } + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::{rewrite_generated_defs, rewrite_generated_markdown, rewrite_signature_literals}; + + #[test] + fn rewrite_signature_literals_rewrites_known_markdown_and_defs_signatures() { + let input = concat!( + "fn break_current_node(_: ActionApi, destination: String) -> Action\n", + "fn join_buffer_here(_: ActionApi, buffer_id: int, placement: String) -> Action\n", + "fn open_buffer_history(_: ActionApi, buffer_id: int, scope: String, placement: String) -> Action\n", + "fn notify(_: ActionApi, level: String, message: String) -> Action\n", + "fn split_with(_: ActionApi, direction: String, tree: TreeSpec) -> Action\n", + "unchanged\n", + "fn break_current_node(_: ActionApi, destination: string) -> Action;\n", + "fn join_buffer_here(_: ActionApi, buffer_id: int, placement: string) -> Action;\n", + "fn open_buffer_history(_: ActionApi, buffer_id: int, scope: string, placement: string) -> Action;\n", + "fn notify(_: ActionApi, level: string, message: string) -> Action;\n", + "fn split_with(_: ActionApi, direction: string, tree: TreeSpec) -> Action;\n", + ); + let expected = concat!( + "fn break_current_node(_: ActionApi, destination: \"tab\" | \"floating\") -> Action\n", + "fn join_buffer_here(_: ActionApi, buffer_id: int, placement: \"tab-after\" | \"tab-before\" | \"left\" | \"right\" | \"up\" | \"down\") -> Action\n", + "fn open_buffer_history(_: ActionApi, buffer_id: int, scope: \"visible\" | \"full\", placement: \"floating\" | \"tab\") -> Action\n", + "fn notify(_: ActionApi, level: \"info\" | \"warn\" | \"error\", message: String) -> Action\n", + "fn split_with(_: ActionApi, direction: \"h\" | \"horizontal\" | \"v\" | \"vertical\", tree: TreeSpec) -> Action\n", + "unchanged\n", + "fn break_current_node(_: ActionApi, destination: \"tab\" | \"floating\") -> Action;\n", + "fn join_buffer_here(_: ActionApi, buffer_id: int, placement: \"tab-after\" | \"tab-before\" | \"left\" | \"right\" | \"up\" | \"down\") -> Action;\n", + "fn open_buffer_history(_: ActionApi, buffer_id: int, scope: \"visible\" | \"full\", placement: \"floating\" | \"tab\") -> Action;\n", + "fn notify(_: ActionApi, level: \"info\" | \"warn\" | \"error\", message: string) -> Action;\n", + "fn split_with(_: ActionApi, direction: \"h\" | \"horizontal\" | \"v\" | \"vertical\", tree: TreeSpec) -> Action;\n", + ); + + assert_eq!(rewrite_signature_literals(input), expected); + assert_eq!( + rewrite_signature_literals("already-correct"), + "already-correct" + ); + } + + #[test] + fn rewrite_generated_markdown_rewrites_signature_literals_and_is_idempotent() { + let input = "fn break_current_node(_: ActionApi, destination: String) -> Action"; + let expected = + "fn break_current_node(_: ActionApi, destination: \"tab\" | \"floating\") -> Action"; + + assert_eq!(rewrite_generated_markdown(input), expected); + assert_eq!(rewrite_generated_markdown(expected), expected); + } + + #[test] + fn rewrite_generated_defs_updates_file_contents() { + let tempdir = tempdir().expect("create tempdir"); + let path = tempdir.path().join("runtime.rhai"); + let input = concat!( + "fn join_buffer_here(_: ActionApi, buffer_id: int, placement: string) -> Action;\n", + "fn open_buffer_history(_: ActionApi, buffer_id: int, scope: string, placement: string) -> Action;\n", + ); + let expected = concat!( + "fn join_buffer_here(_: ActionApi, buffer_id: int, placement: \"tab-after\" | \"tab-before\" | \"left\" | \"right\" | \"up\" | \"down\") -> Action;\n", + "fn open_buffer_history(_: ActionApi, buffer_id: int, scope: \"visible\" | \"full\", placement: \"floating\" | \"tab\") -> Action;\n", + ); + + fs::write(&path, input).expect("write defs fixture"); + rewrite_generated_defs(&path).expect("rewrite defs"); + assert_eq!( + fs::read_to_string(&path).expect("read rewritten defs"), + expected + ); + + rewrite_generated_defs(&path).expect("rewrite defs again"); + assert_eq!( + fs::read_to_string(&path).expect("read rewritten defs"), + expected + ); + } +} diff --git a/crates/embers-client/src/scripting/engine.rs b/crates/embers-client/src/scripting/engine.rs index edea286..cd8d594 100644 --- a/crates/embers-client/src/scripting/engine.rs +++ b/crates/embers-client/src/scripting/engine.rs @@ -415,14 +415,12 @@ fn validate_action_refs( ) -> Result<(), ScriptError> { for action in actions { match action { - Action::RunNamedAction { name } => { - if !named_actions.contains_key(name) { - return Err(ScriptError::validation( - source, - position, - format!("binding references unknown action '{name}'"), - )); - } + Action::RunNamedAction { name } if !named_actions.contains_key(name) => { + return Err(ScriptError::validation( + source, + position, + format!("binding references unknown action '{name}'"), + )); } Action::Chain(actions) => { validate_action_refs(source, position, named_actions, actions)?; diff --git a/crates/embers-client/src/scripting/model.rs b/crates/embers-client/src/scripting/model.rs index e201d69..631d632 100644 --- a/crates/embers-client/src/scripting/model.rs +++ b/crates/embers-client/src/scripting/model.rs @@ -1,6 +1,9 @@ use std::collections::BTreeMap; -use embers_core::{BufferId, FloatingId, NodeId, SplitDirection}; +use embers_core::{BufferId, FloatingId, NodeId, SessionId, SplitDirection}; +use embers_protocol::{ + BufferHistoryPlacement, BufferHistoryScope, NodeBreakDestination, NodeJoinPlacement, +}; use crate::input::KeySequence; use crate::presentation::NavigationDirection; @@ -37,6 +40,11 @@ pub enum Action { RevealBuffer { buffer_id: BufferId, }, + OpenBufferHistory { + buffer_id: BufferId, + scope: BufferHistoryScope, + placement: BufferHistoryPlacement, + }, SplitCurrent { direction: SplitDirection, new_child: TreeSpec, @@ -81,6 +89,36 @@ pub enum Action { focus: bool, close_on_empty: bool, }, + ZoomNode { + node_id: Option, + }, + UnzoomNode { + session_id: Option, + }, + ToggleZoomNode { + node_id: Option, + }, + SwapSiblingNodes { + first_node_id: Option, + second_node_id: NodeId, + }, + BreakNode { + node_id: Option, + destination: NodeBreakDestination, + }, + JoinBufferAtNode { + node_id: Option, + buffer_id: BufferId, + placement: NodeJoinPlacement, + }, + MoveNodeBefore { + node_id: Option, + sibling_node_id: NodeId, + }, + MoveNodeAfter { + node_id: Option, + sibling_node_id: NodeId, + }, SendKeys { buffer_id: Option, keys: KeySequence, @@ -99,6 +137,7 @@ pub enum Action { EnterSearchMode, SearchNext, SearchPrev, + CommitSearch, CancelSearch, EnterSelect { kind: SelectionKind, diff --git a/crates/embers-client/src/scripting/runtime.rs b/crates/embers-client/src/scripting/runtime.rs index 955a463..745664b 100644 --- a/crates/embers-client/src/scripting/runtime.rs +++ b/crates/embers-client/src/scripting/runtime.rs @@ -4,6 +4,9 @@ use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use embers_core::{BufferId, FloatingId, NodeId, Rect, SplitDirection}; +use embers_protocol::{ + BufferHistoryPlacement, BufferHistoryScope, NodeBreakDestination, NodeJoinPlacement, +}; use rhai::plugin::*; use rhai::{ Array, Dynamic, Engine, EvalAltResult, ImmutableString, Map, NativeCallContext, Position, Scope, @@ -916,9 +919,10 @@ mod documented_mux_api { mod documented_action_api { use super::{ Action, ActionApi, Array, ImmutableString, Map, NativeCallContext, NavigationDirection, - TreeSpec, parse_action_array, parse_buffer_id, parse_bytes, parse_floating_id, - parse_floating_options, parse_floating_spec, parse_index, parse_key_sequence, - parse_node_id, parse_notify_level, parse_split_direction, runtime_error_at, + TreeSpec, parse_action_array, parse_buffer_history_placement, parse_buffer_history_scope, + parse_buffer_id, parse_bytes, parse_floating_id, parse_floating_options, + parse_floating_spec, parse_index, parse_key_sequence, parse_node_break_destination, + parse_node_id, parse_node_join_placement, parse_notify_level, parse_split_direction, with_call_position, }; @@ -1295,6 +1299,176 @@ mod documented_action_api { }) } + /// Open the history of a buffer in a new view. + /// `scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. + /// Example: `action.open_buffer_history(12, "visible", "floating")`. + #[rhai_fn(return_raw, name = "open_buffer_history")] + pub fn open_buffer_history( + ctx: NativeCallContext, + _: &mut ActionApi, + buffer_id: i64, + scope: &str, + placement: &str, + ) -> RhaiResultOf { + let position = ctx.call_position(); + with_call_position(ctx, || { + Ok(Action::OpenBufferHistory { + buffer_id: parse_buffer_id(buffer_id)?, + scope: parse_buffer_history_scope(scope, position)?, + placement: parse_buffer_history_placement(placement, position)?, + }) + }) + } + + /// Zoom the session's currently focused node. + /// There is intentionally no separate `zoom_node(node_id)` helper in this API surface. + #[rhai_fn(name = "zoom_current_node")] + pub fn zoom_current_node(_: &mut ActionApi) -> Action { + Action::ZoomNode { node_id: None } + } + + /// Clear the current session's active zoom state. + /// This removes the current session zoom rather than unwinding a stack of prior zooms. + #[rhai_fn(name = "unzoom_current_session")] + pub fn unzoom_current_session(_: &mut ActionApi) -> Action { + Action::UnzoomNode { session_id: None } + } + + /// Toggle zoom for the specified node id. + /// There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the + /// focused node and `toggle_zoom_node` when you already know the target id. + #[rhai_fn(return_raw, name = "toggle_zoom_node")] + pub fn toggle_zoom_node( + ctx: NativeCallContext, + _: &mut ActionApi, + node_id: i64, + ) -> RhaiResultOf { + with_call_position(ctx, || { + Ok(Action::ToggleZoomNode { + node_id: Some(parse_node_id(node_id)?), + }) + }) + } + + /// Swap the current node with a sibling. + #[rhai_fn(return_raw, name = "swap_current_node")] + pub fn swap_current_node( + ctx: NativeCallContext, + _: &mut ActionApi, + sibling_node_id: i64, + ) -> RhaiResultOf { + with_call_position(ctx, || { + Ok(Action::SwapSiblingNodes { + first_node_id: None, + second_node_id: parse_node_id(sibling_node_id)?, + }) + }) + } + + /// Break the current node into a new tab or floating window. + /// `destination` accepts `tab` or `floating`. + /// Example: `action.break_current_node("floating")`. + #[rhai_fn(return_raw, name = "break_current_node")] + pub fn break_current_node( + ctx: NativeCallContext, + _: &mut ActionApi, + destination: &str, + ) -> RhaiResultOf { + let position = ctx.call_position(); + with_call_position(ctx, || { + Ok(Action::BreakNode { + node_id: None, + destination: parse_node_break_destination(destination, position)?, + }) + }) + } + + /// Attach a buffer at the current node. + /// `placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. + /// Example: `action.join_buffer_here(12, "tab-after")`. + #[rhai_fn(return_raw, name = "join_buffer_here")] + pub fn join_buffer_here( + ctx: NativeCallContext, + _: &mut ActionApi, + buffer_id: i64, + placement: &str, + ) -> RhaiResultOf { + let position = ctx.call_position(); + with_call_position(ctx, || { + Ok(Action::JoinBufferAtNode { + node_id: None, + buffer_id: parse_buffer_id(buffer_id)?, + placement: parse_node_join_placement(placement, position)?, + }) + }) + } + + /// Move the current node before a sibling. + /// Use this when the current node is the one being repositioned. + /// Example: `action.move_current_node_before(42)`. + #[rhai_fn(return_raw, name = "move_current_node_before")] + pub fn move_current_node_before( + ctx: NativeCallContext, + _: &mut ActionApi, + sibling_node_id: i64, + ) -> RhaiResultOf { + with_call_position(ctx, || { + Ok(Action::MoveNodeBefore { + node_id: None, + sibling_node_id: parse_node_id(sibling_node_id)?, + }) + }) + } + + /// Move the current node after a sibling. + #[rhai_fn(return_raw, name = "move_current_node_after")] + pub fn move_current_node_after( + ctx: NativeCallContext, + _: &mut ActionApi, + sibling_node_id: i64, + ) -> RhaiResultOf { + with_call_position(ctx, || { + Ok(Action::MoveNodeAfter { + node_id: None, + sibling_node_id: parse_node_id(sibling_node_id)?, + }) + }) + } + + /// Move a node before a sibling. + #[rhai_fn(return_raw, name = "move_node_before")] + pub fn move_node_before( + ctx: NativeCallContext, + _: &mut ActionApi, + node_id: i64, + sibling_node_id: i64, + ) -> RhaiResultOf { + with_call_position(ctx, || { + Ok(Action::MoveNodeBefore { + node_id: Some(parse_node_id(node_id)?), + sibling_node_id: parse_node_id(sibling_node_id)?, + }) + }) + } + + /// Move a node after a sibling. + /// Use this when you need to move a specific node id instead of the current node. + /// Example: `action.move_node_after(10, 42)`. + #[rhai_fn(return_raw, name = "move_node_after")] + pub fn move_node_after( + ctx: NativeCallContext, + _: &mut ActionApi, + node_id: i64, + sibling_node_id: i64, + ) -> RhaiResultOf { + with_call_position(ctx, || { + Ok(Action::MoveNodeAfter { + node_id: Some(parse_node_id(node_id)?), + sibling_node_id: parse_node_id(sibling_node_id)?, + }) + }) + } + /// Move a buffer into a specific node. #[rhai_fn(return_raw, name = "move_buffer_to_node")] pub fn move_buffer_to_node( @@ -1496,6 +1670,13 @@ mod documented_action_api { Action::CancelSearch } + /// Finalize the active search, keep the current match and cursor position, and leave search + /// mode with the committed result in place. + #[rhai_fn(name = "commit_search")] + pub fn commit_search(_: &mut ActionApi) -> Action { + Action::CommitSearch + } + /// Jump to the next search match. #[rhai_fn(name = "search_next")] pub fn search_next(_: &mut ActionApi) -> Action { @@ -1822,6 +2003,9 @@ mod documented_ui_api { /// /// `segment(_: UiApi, text: String) -> BarSegment` produces plain text with default /// [`StyleSpec`] values and no click target. + /// + /// See the overloaded `segment(_: UiApi, text: String, options: Map) -> BarSegment` doc for + /// the full `options: Map` styling keys. #[rhai_fn(name = "segment")] pub fn segment(_: &mut UiApi, text: &str) -> BarSegment { BarSegment { @@ -1833,10 +2017,32 @@ mod documented_ui_api { /// Create a [`BarSegment`] from a [`UiApi`] receiver, text, and an `options: Map`. /// + /// See the main `segment(_: UiApi, text: String) -> BarSegment` doc for the shared behavior. + /// /// `segment(_: UiApi, text: String, options: Map) -> BarSegment` supports `fg`, `bg`, - /// `bold`, `italic`, `underline`, `dim`, and `target` keys to override styling and attach an - /// optional interaction target. `dim` is a boolean that renders the text with reduced - /// intensity for a muted appearance. + /// `bold`, `italic`, `underline`, `dim`, `blink`, and `target` keys to override styling and + /// attach an optional interaction target. + /// + /// `fg` is the foreground color and accepts a standard CSS color name, a hex code such as + /// `#ff0000`, or an RGB/RGBA string such as `rgb(255,0,0)` or `rgba(255,0,0,0.5)`. + /// `bg` is the background color and accepts the same color formats. + /// `bold`, `italic`, `underline`, `dim`, and `blink` are boolean `true`/`false` flags. + /// `dim` renders the text with reduced intensity for a muted appearance. + /// `blink` enables blinking text for that segment, but many modern terminal emulators ignore + /// or disable blink by default, so blinking text may not appear consistently. + /// `target` is an optional interaction target, usually either a string identifier such as + /// `"myTarget"` or a structured object such as `#{ type: "callback", id: "save" }`, + /// depending on the consumer API. + /// + /// Examples: + /// - `#{ fg: "#ff0000", bg: "rgba(0,0,0,0.5)", bold: true }` + /// - `#{ target: "myTarget" }` + /// - `#{ target: #{ type: "callback", id: "save" } }` + /// + /// Accessibility note: blinking text can be distracting and may trigger seizures for some + /// users. Use `blink` sparingly, prefer non-animated emphasis such as `dim` or color/weight + /// changes, follow WCAG guidance to avoid flashing content, and respect reduced-motion + /// preferences before enabling `blink`. #[rhai_fn(return_raw, name = "segment")] pub fn segment_with_options( ctx: NativeCallContext, @@ -2038,6 +2244,7 @@ fn parse_segment_options(mut options: Map) -> ScriptResult<(StyleSpec, Option ScriptResult { } } +fn parse_buffer_history_scope(value: &str, position: Position) -> RhaiResultOf { + match value { + "visible" => Ok(BufferHistoryScope::Visible), + "full" => Ok(BufferHistoryScope::Full), + _ => Err(runtime_error_at( + format!("invalid scope: {value}"), + position, + )), + } +} + +fn parse_buffer_history_placement( + value: &str, + position: Position, +) -> RhaiResultOf { + match value { + "floating" => Ok(BufferHistoryPlacement::Floating), + "tab" => Ok(BufferHistoryPlacement::Tab), + _ => Err(runtime_error_at( + format!("invalid placement: {value}"), + position, + )), + } +} + +fn parse_node_break_destination( + value: &str, + position: Position, +) -> RhaiResultOf { + match value { + "tab" => Ok(NodeBreakDestination::Tab), + "floating" => Ok(NodeBreakDestination::Floating), + _ => Err(runtime_error_at( + format!("invalid destination: {value}"), + position, + )), + } +} + +fn parse_node_join_placement(value: &str, position: Position) -> RhaiResultOf { + match value { + "tab-after" => Ok(NodeJoinPlacement::TabAfter), + "tab-before" => Ok(NodeJoinPlacement::TabBefore), + "left" => Ok(NodeJoinPlacement::Left), + "right" => Ok(NodeJoinPlacement::Right), + "up" => Ok(NodeJoinPlacement::Up), + "down" => Ok(NodeJoinPlacement::Down), + _ => Err(runtime_error_at( + format!("invalid placement: {value}"), + position, + )), + } +} + fn dynamic_option_custom(value: Option) -> Dynamic { value.map(Dynamic::from).unwrap_or(Dynamic::UNIT) } @@ -2435,9 +2696,23 @@ fn runtime_error_at(message: impl Into, position: Position) -> Box Result> { + let mut engine = Engine::new(); + register_documented_registration_runtime_api(&mut engine); + let mut scope = registration_scope(); + engine.eval_with_scope(&mut scope, script) + } #[test] fn parse_levels_accepts_draft_names() { @@ -2452,6 +2727,145 @@ mod tests { assert!(parse_split_direction("vertical").is_ok()); } + #[test] + fn parse_history_and_node_enums_accept_known_literals() { + let position = Position::NONE; + for (literal, scope) in [ + ("visible", BufferHistoryScope::Visible), + ("full", BufferHistoryScope::Full), + ] { + assert!( + parse_buffer_history_scope(literal, position).is_ok(), + "scope literal {literal:?} should parse" + ); + assert_eq!( + eval_action(&format!( + "action.open_buffer_history(12, {literal:?}, \"floating\")" + )) + .expect("history builder should parse"), + Action::OpenBufferHistory { + buffer_id: BufferId(12), + scope, + placement: BufferHistoryPlacement::Floating, + } + ); + } + for (literal, placement) in [ + ("floating", BufferHistoryPlacement::Floating), + ("tab", BufferHistoryPlacement::Tab), + ] { + assert!( + parse_buffer_history_placement(literal, position).is_ok(), + "placement literal {literal:?} should parse" + ); + assert_eq!( + eval_action(&format!( + "action.open_buffer_history(12, \"full\", {literal:?})" + )) + .expect("history placement builder should parse"), + Action::OpenBufferHistory { + buffer_id: BufferId(12), + scope: BufferHistoryScope::Full, + placement, + } + ); + } + for (literal, destination) in [ + ("tab", NodeBreakDestination::Tab), + ("floating", NodeBreakDestination::Floating), + ] { + assert!( + parse_node_break_destination(literal, position).is_ok(), + "break destination literal {literal:?} should parse" + ); + assert_eq!( + eval_action(&format!("action.break_current_node({literal:?})")) + .expect("break builder should parse"), + Action::BreakNode { + node_id: None, + destination, + } + ); + } + for (literal, placement) in [ + ("tab-after", NodeJoinPlacement::TabAfter), + ("tab-before", NodeJoinPlacement::TabBefore), + ("left", NodeJoinPlacement::Left), + ("right", NodeJoinPlacement::Right), + ("up", NodeJoinPlacement::Up), + ("down", NodeJoinPlacement::Down), + ] { + assert!( + parse_node_join_placement(literal, position).is_ok(), + "join placement literal {literal:?} should parse" + ); + assert_eq!( + eval_action(&format!("action.join_buffer_here(12, {literal:?})")) + .expect("join builder should parse"), + Action::JoinBufferAtNode { + node_id: None, + buffer_id: BufferId(12), + placement, + } + ); + } + + for literal in ["", "invalid"] { + assert!( + parse_buffer_history_scope(literal, position) + .expect_err("invalid scope should fail") + .to_string() + .contains(&format!("invalid scope: {literal}")) + ); + assert!( + parse_buffer_history_placement(literal, position) + .expect_err("invalid placement should fail") + .to_string() + .contains(&format!("invalid placement: {literal}")) + ); + assert!( + eval_action(&format!( + "action.open_buffer_history(12, {literal:?}, \"floating\")" + )) + .expect_err("history scope builder should fail") + .to_string() + .contains(&format!("invalid scope: {literal}")) + ); + assert!( + eval_action(&format!( + "action.open_buffer_history(12, \"full\", {literal:?})" + )) + .expect_err("history placement builder should fail") + .to_string() + .contains(&format!("invalid placement: {literal}")) + ); + assert!( + parse_node_break_destination(literal, position) + .expect_err("invalid destination should fail") + .to_string() + .contains(&format!("invalid destination: {literal}")) + ); + assert!( + eval_action(&format!("action.break_current_node({literal:?})")) + .expect_err("break builder should fail") + .to_string() + .contains(&format!("invalid destination: {literal}")) + ); + assert!( + parse_node_join_placement(literal, position) + .expect_err("invalid join placement should fail") + .to_string() + .contains(&format!("invalid placement: {literal}")) + ); + assert!( + eval_action(&format!("action.join_buffer_here(12, {literal:?})")) + .expect_err("join builder should fail") + .to_string() + .contains(&format!("invalid placement: {literal}")) + ); + } + } + #[test] fn parse_buffer_spawn_rejects_unknown_options() { let command = vec![Dynamic::from("/bin/sh")]; @@ -2479,4 +2893,20 @@ mod tests { "Runtime error: unknown bar target option(s): bogus" ); } + + #[test] + fn parse_segment_options_preserves_blink_flag() { + let mut options = Map::new(); + options.insert("blink".into(), Dynamic::TRUE); + + let (style, target) = parse_segment_options(options).expect("segment options parse"); + assert_eq!( + style, + StyleSpec { + blink: true, + ..StyleSpec::default() + } + ); + assert_eq!(target, None); + } } diff --git a/crates/embers-client/src/scripting/types.rs b/crates/embers-client/src/scripting/types.rs index 97ac1f8..2ba2a05 100644 --- a/crates/embers-client/src/scripting/types.rs +++ b/crates/embers-client/src/scripting/types.rs @@ -67,6 +67,7 @@ pub struct StyleSpec { pub italic: bool, pub underline: bool, pub dim: bool, + pub blink: bool, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/embers-client/src/state.rs b/crates/embers-client/src/state.rs index 62a27af..7ec1334 100644 --- a/crates/embers-client/src/state.rs +++ b/crates/embers-client/src/state.rs @@ -164,6 +164,10 @@ impl ClientState { } } + pub fn apply_buffer_record(&mut self, buffer: BufferRecord) { + self.buffers.insert(buffer.id, buffer); + } + pub fn apply_buffer_snapshot(&mut self, snapshot: VisibleSnapshotResponse) { if let Some(buffer) = self.buffers.get_mut(&snapshot.buffer_id) { buffer.last_snapshot_seq = snapshot.sequence; diff --git a/crates/embers-client/tests/configured_client.rs b/crates/embers-client/tests/configured_client.rs index c37dfa0..afc8387 100644 --- a/crates/embers-client/tests/configured_client.rs +++ b/crates/embers-client/tests/configured_client.rs @@ -8,15 +8,19 @@ use embers_client::{ }; use embers_core::{ActivityState, BufferId, NodeId, PtySize, RequestId, SessionId, Size}; use embers_protocol::{ - BufferCreatedEvent, BufferRecord, BufferRecordState, BufferViewRecord, ClientChangedEvent, - ClientMessage, ClientRecord, ClientRequest, ClientResponse, FocusChangedEvent, InputRequest, - NodeRecord, NodeRecordKind, NodeRequest, OkResponse, RenderInvalidatedEvent, - ScrollbackSliceResponse, ServerEvent, ServerResponse, SessionRecord, SessionRequest, - SessionSnapshot, SessionSnapshotResponse, SnapshotResponse, VisibleSnapshotResponse, + BufferCreatedEvent, BufferRecord, BufferRecordKind, BufferRecordState, BufferResponse, + BufferViewRecord, BuffersResponse, ClientChangedEvent, ClientMessage, ClientRecord, + ClientRequest, ClientResponse, FocusChangedEvent, InputRequest, NodeChangedEvent, NodeRecord, + NodeRecordKind, NodeRequest, OkResponse, RenderInvalidatedEvent, ScrollbackSliceResponse, + ServerEvent, ServerResponse, SessionCreatedEvent, SessionRecord, SessionRequest, + SessionSnapshot, SessionSnapshotResponse, SessionsResponse, SnapshotResponse, + VisibleSnapshotResponse, }; use tempfile::tempdir; -use crate::support::{FOCUSED_LEAF_ID, LEFT_LEAF_ID, SESSION_ID, demo_state, root_focus_state}; +use crate::support::{ + FOCUSED_BUFFER_ID, FOCUSED_LEAF_ID, LEFT_LEAF_ID, SESSION_ID, demo_state, root_focus_state, +}; const SECOND_SESSION_ID: SessionId = SessionId(2); const SECOND_ROOT_ID: NodeId = NodeId(200); @@ -84,6 +88,17 @@ fn visible_snapshot_from_state( snapshot } +fn buffer_response_from_state( + state: &embers_client::ClientState, + buffer_id: BufferId, + request_id: RequestId, +) -> BufferResponse { + BufferResponse { + request_id, + buffer: state.buffers.get(&buffer_id).unwrap().clone(), + } +} + fn scrollback_slice_response( buffer_id: BufferId, request_id: RequestId, @@ -116,6 +131,41 @@ fn snapshot_response( } } +fn push_send_input_refresh_responses( + transport: &FakeTransport, + state: &embers_client::ClientState, + buffer_id: BufferId, +) { + transport.push_response(ServerResponse::Ok(OkResponse { + request_id: RequestId(1), + })); + transport.push_response(ServerResponse::VisibleSnapshot( + visible_snapshot_from_state(state, buffer_id, RequestId(2)), + )); + transport.push_response(ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(3), + snapshot: session_snapshot_from_state(state, SESSION_ID), + })); +} + +fn expected_send_input_refresh_requests(buffer_id: BufferId, bytes: &[u8]) -> Vec { + vec![ + ClientMessage::Input(InputRequest::Send { + request_id: RequestId(1), + buffer_id, + bytes: bytes.to_vec(), + }), + ClientMessage::Buffer(embers_protocol::BufferRequest::CaptureVisible { + request_id: RequestId(2), + buffer_id, + }), + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(3), + session_id: SESSION_ID, + }), + ] +} + fn second_session_state() -> embers_client::ClientState { let mut state = demo_state(); state.sessions.insert( @@ -127,6 +177,7 @@ fn second_session_state() -> embers_client::ClientState { floating_ids: Vec::new(), focused_leaf_id: Some(SECOND_ROOT_ID), focused_floating_id: None, + zoomed_node_id: None, }, ); state.nodes.insert( @@ -154,9 +205,13 @@ fn second_session_state() -> embers_client::ClientState { title: "other pane".to_owned(), command: vec!["/bin/sh".to_owned()], cwd: None, + kind: BufferRecordKind::Pty, state: BufferRecordState::Running, pid: None, attachment_node_id: Some(SECOND_ROOT_ID), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, pty_size: PtySize::new(80, 20), activity: ActivityState::Idle, last_snapshot_seq: 1, @@ -247,6 +302,161 @@ async fn configured_keybinding_executes_live_focus_action() { transport.assert_exhausted().unwrap(); } +#[tokio::test] +async fn unmapped_keys_forward_to_the_focused_buffer_in_normal_mode() { + let transport = FakeTransport::default(); + let state = demo_state(); + push_send_input_refresh_responses(&transport, &state, FOCUSED_BUFFER_ID); + + let client = MuxClient::new(transport.clone()); + let (config, _tempdir) = manager_from_source(""); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = state; + + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Char('x'), + ) + .await + .unwrap(); + + assert_eq!( + transport.requests(), + expected_send_input_refresh_requests(FOCUSED_BUFFER_ID, b"x") + ); +} + +#[tokio::test] +async fn leader_prefix_waits_without_forwarding_input() { + let client = MuxClient::new(FakeTransport::default()); + let (config, _tempdir) = manager_from_source( + r#" + fn open_workspace_split(ctx) { action.notify("info", "workspace-split") } + define_action("workspace-split", open_workspace_split); + set_leader(""); + bind("normal", "ws", "workspace-split"); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = demo_state(); + + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Ctrl('a'), + ) + .await + .unwrap(); + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Char('w'), + ) + .await + .unwrap(); + + assert!(configured.client().transport().requests().is_empty()); + assert!(configured.notifications().is_empty()); + + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Char('s'), + ) + .await + .unwrap(); + + assert!(configured.client().transport().requests().is_empty()); + assert_eq!(configured.notifications(), ["workspace-split"]); +} + +#[tokio::test] +async fn reload_clears_pending_prefix_before_next_unmapped_key() { + let transport = FakeTransport::default(); + let state = demo_state(); + push_send_input_refresh_responses(&transport, &state, FOCUSED_BUFFER_ID); + + let tempdir = tempdir().unwrap(); + let config_path = tempdir.path().join("config.rhai"); + fs::write( + &config_path, + r#" + fn open_workspace_split(ctx) { action.notify("info", "workspace-split") } + define_action("workspace-split", open_workspace_split); + set_leader(""); + bind("normal", "ws", "workspace-split"); + "#, + ) + .unwrap(); + let config = ConfigManager::load( + ConfigDiscoveryOptions::default().with_project_config_dir(tempdir.path()), + ) + .unwrap(); + let client = MuxClient::new(transport.clone()); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = state; + + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Ctrl('a'), + ) + .await + .unwrap(); + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Char('w'), + ) + .await + .unwrap(); + assert!(transport.requests().is_empty()); + + configured.reload_config().unwrap(); + + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Char('x'), + ) + .await + .unwrap(); + + assert_eq!( + transport.requests(), + expected_send_input_refresh_requests(FOCUSED_BUFFER_ID, b"x") + ); +} + #[tokio::test] async fn configured_render_uses_scripted_tab_bars() { let client = MuxClient::new(FakeTransport::default()); @@ -722,6 +932,47 @@ async fn select_mode_yanks_selection_to_osc52() { ); } +#[tokio::test] +async fn copy_mode_blocks_unmapped_passthrough() { + let transport = FakeTransport::default(); + let client = MuxClient::new(transport.clone()); + let (config, _tempdir) = manager_from_source( + r#" + fn enter_copy(ctx) { action.enter_mode("copy") } + define_action("enter-copy", enter_copy); + unbind("normal", "v"); + bind("normal", "v", "enter-copy"); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = demo_state(); + + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Char('v'), + ) + .await + .unwrap(); + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Char('x'), + ) + .await + .unwrap(); + + assert!(transport.requests().is_empty()); +} + #[tokio::test] async fn wheel_mouse_events_scroll_locally_or_forward_to_program() { let mut initial_state = demo_state(); @@ -945,10 +1196,15 @@ async fn render_invalidated_events_use_their_buffer_session_context() { transport.push_event(ServerEvent::RenderInvalidated(RenderInvalidatedEvent { buffer_id: SECOND_BUFFER_ID, })); + transport.push_response(ServerResponse::Buffer(buffer_response_from_state( + &state, + SECOND_BUFFER_ID, + RequestId(1), + ))); transport.push_response(ServerResponse::VisibleSnapshot( - visible_snapshot_from_state(&state, SECOND_BUFFER_ID, RequestId(1)), + visible_snapshot_from_state(&state, SECOND_BUFFER_ID, RequestId(2)), )); - let client = MuxClient::new(transport); + let client = MuxClient::new(transport.clone()); let (config, _tempdir) = manager_from_source( r#" fn on_render(ctx) { action.notify("info", ctx.current_session().name()) } @@ -972,58 +1228,253 @@ async fn render_invalidated_events_use_their_buffer_session_context() { assert!(matches!(event, ServerEvent::RenderInvalidated(_))); assert_eq!(configured.notifications(), ["other"]); + assert_eq!( + transport.requests(), + vec![ + ClientMessage::Buffer(embers_protocol::BufferRequest::Get { + request_id: RequestId(1), + buffer_id: SECOND_BUFFER_ID, + }), + ClientMessage::Buffer(embers_protocol::BufferRequest::CaptureVisible { + request_id: RequestId(2), + buffer_id: SECOND_BUFFER_ID, + }), + ] + ); } #[tokio::test] -async fn detached_buffer_events_do_not_fall_back_to_the_active_session() { +async fn render_invalidated_events_refresh_buffer_activity_before_bell_hooks() { + let mut state = second_session_state(); + state.buffers.get_mut(&SECOND_BUFFER_ID).unwrap().activity = ActivityState::Bell; + let transport = FakeTransport::default(); - transport.push_event(ServerEvent::BufferCreated(BufferCreatedEvent { - buffer: BufferRecord { - id: BufferId(71), - title: "detached".to_owned(), - command: vec!["/bin/sh".to_owned()], - cwd: None, - state: BufferRecordState::Running, - pid: None, - attachment_node_id: None, - pty_size: PtySize::new(80, 20), - activity: ActivityState::Idle, - last_snapshot_seq: 0, - exit_code: None, - env: BTreeMap::new(), - }, + transport.push_event(ServerEvent::RenderInvalidated(RenderInvalidatedEvent { + buffer_id: SECOND_BUFFER_ID, })); - let client = MuxClient::new(transport); + transport.push_response(ServerResponse::Buffer(buffer_response_from_state( + &state, + SECOND_BUFFER_ID, + RequestId(1), + ))); + transport.push_response(ServerResponse::VisibleSnapshot( + visible_snapshot_from_state(&state, SECOND_BUFFER_ID, RequestId(2)), + )); + let client = MuxClient::new(transport.clone()); let (config, _tempdir) = manager_from_source( r#" - fn on_buffer(ctx) { - if ctx.current_session() == () { - action.notify("info", "none") - } else { - action.notify("info", ctx.current_session().name()) - } - } - on("buffer_created", on_buffer); + fn on_bell(ctx) { action.notify("info", ctx.current_session().name()) } + on("buffer_bell", on_bell); "#, ); let mut configured = ConfiguredClient::new(client, config); - *configured.client_mut().state_mut() = demo_state(); - configured - .render_session( - SESSION_ID, - Size { - width: 80, - height: 20, - }, - ) - .await - .unwrap(); + *configured.client_mut().state_mut() = second_session_state(); let event = configured.process_next_event().await.unwrap(); - assert!(matches!(event, ServerEvent::BufferCreated(_))); - assert_eq!(configured.notifications(), ["none"]); -} + assert!(matches!(event, ServerEvent::RenderInvalidated(_))); + assert_eq!(configured.notifications(), ["other"]); + assert_eq!( + transport.requests(), + vec![ + ClientMessage::Buffer(embers_protocol::BufferRequest::Get { + request_id: RequestId(1), + buffer_id: SECOND_BUFFER_ID, + }), + ClientMessage::Buffer(embers_protocol::BufferRequest::CaptureVisible { + request_id: RequestId(2), + buffer_id: SECOND_BUFFER_ID, + }), + ] + ); +} + +#[tokio::test] +async fn render_session_refreshes_invalidated_snapshot_before_rendering_title_and_content() { + let transport = FakeTransport::default(); + let mut stale_state = demo_state(); + stale_state.apply_event(&ServerEvent::RenderInvalidated(RenderInvalidatedEvent { + buffer_id: FOCUSED_BUFFER_ID, + })); + + let mut refreshed_state = demo_state(); + let snapshot = refreshed_state + .snapshots + .get_mut(&FOCUSED_BUFFER_ID) + .unwrap(); + snapshot.lines = vec!["fresh render line".to_owned()]; + snapshot.title = Some("fresh-title".to_owned()); + + transport.push_response(ServerResponse::VisibleSnapshot( + visible_snapshot_from_state(&refreshed_state, FOCUSED_BUFFER_ID, RequestId(1)), + )); + + let client = MuxClient::new(transport.clone()); + let (config, _tempdir) = manager_from_source(""); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = stale_state; + + let grid = configured + .render_session( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + ) + .await + .unwrap(); + let rendered = grid.render(); + let presentation = PresentationModel::project( + configured.client().state(), + SESSION_ID, + Size { + width: 80, + height: 20, + }, + ) + .expect("projection succeeds"); + + assert!(rendered.contains("fresh render line")); + assert!(!rendered.contains("logs visible")); + assert_eq!( + configured + .client() + .state() + .buffers + .get(&FOCUSED_BUFFER_ID) + .expect("focused buffer") + .title, + "fresh-title" + ); + assert_eq!( + presentation.focused_leaf().expect("focused leaf").title, + "fresh-title" + ); + assert!(configured.client().state().invalidated_buffers.is_empty()); + assert_eq!( + transport.requests(), + vec![ClientMessage::Buffer( + embers_protocol::BufferRequest::CaptureVisible { + request_id: RequestId(1), + buffer_id: FOCUSED_BUFFER_ID, + } + )] + ); +} + +#[tokio::test] +async fn render_session_replaces_stale_scrolled_cache_when_snapshot_switches_to_alternate_screen() { + let transport = FakeTransport::default(); + let mut stale_state = demo_state(); + let view = stale_state + .view_state_mut(FOCUSED_LEAF_ID) + .expect("focused view state"); + view.follow_output = false; + view.scroll_top_line = 12; + view.total_line_count = 60; + view.visible_lines = vec!["stale scrolled line".to_owned()]; + stale_state.apply_event(&ServerEvent::RenderInvalidated(RenderInvalidatedEvent { + buffer_id: FOCUSED_BUFFER_ID, + })); + + let mut refreshed_state = demo_state(); + let snapshot = refreshed_state + .snapshots + .get_mut(&FOCUSED_BUFFER_ID) + .unwrap(); + snapshot.lines = vec!["alternate screen live".to_owned()]; + snapshot.alternate_screen = true; + snapshot.viewport_top_line = 0; + snapshot.total_lines = 24; + + transport.push_response(ServerResponse::VisibleSnapshot( + visible_snapshot_from_state(&refreshed_state, FOCUSED_BUFFER_ID, RequestId(1)), + )); + + let client = MuxClient::new(transport); + let (config, _tempdir) = manager_from_source(""); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = stale_state; + + let grid = configured + .render_session( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + ) + .await + .unwrap(); + let rendered = grid.render(); + + assert!(rendered.contains("alternate screen live")); + assert!(!rendered.contains("stale scrolled line")); + assert!(!rendered.contains("13/60")); + let view = configured + .client() + .state() + .view_state(FOCUSED_LEAF_ID) + .expect("focused view state"); + assert!(view.alternate_screen); + assert_eq!(view.visible_lines, vec!["alternate screen live".to_owned()]); +} + +#[tokio::test] +async fn detached_buffer_events_do_not_fall_back_to_the_active_session() { + let transport = FakeTransport::default(); + transport.push_event(ServerEvent::BufferCreated(BufferCreatedEvent { + buffer: BufferRecord { + id: BufferId(71), + title: "detached".to_owned(), + command: vec!["/bin/sh".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: None, + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 20), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: BTreeMap::new(), + }, + })); + let client = MuxClient::new(transport); + let (config, _tempdir) = manager_from_source( + r#" + fn on_buffer(ctx) { + if ctx.current_session() == () { + action.notify("info", "none") + } else { + action.notify("info", ctx.current_session().name()) + } + } + on("buffer_created", on_buffer); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = demo_state(); + configured + .render_session( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + ) + .await + .unwrap(); + + let event = configured.process_next_event().await.unwrap(); + + assert!(matches!(event, ServerEvent::BufferCreated(_))); + assert_eq!(configured.notifications(), ["none"]); +} #[tokio::test] async fn disabling_wheel_scroll_in_config_suppresses_local_mouse_scrolling() { @@ -1042,66 +1493,749 @@ async fn disabling_wheel_scroll_in_config_suppresses_local_mouse_scrolling() { let (config, _tempdir) = manager_from_source("mouse.set_wheel_scroll(false);"); let mut configured = ConfiguredClient::new(client, config); *configured.client_mut().state_mut() = state; - + + configured + .handle_mouse( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + MouseEvent { + row: (focused.rect.origin.y + 1) as u16, + column: focused.rect.origin.x as u16, + modifiers: MouseModifiers::default(), + kind: MouseEventKind::WheelUp, + }, + ) + .await + .unwrap(); + + assert!(configured.client().transport().requests().is_empty()); +} + +#[tokio::test] +async fn event_hook_executes_real_actions() { + let transport = ScriptedTransport::default(); + let focused_state = root_focus_state(); + transport.push_event(ServerEvent::FocusChanged(FocusChangedEvent { + session_id: SESSION_ID, + focused_leaf_id: Some(FOCUSED_LEAF_ID), + focused_floating_id: None, + })); + transport.push_exchange( + ClientMessage::Node(NodeRequest::Focus { + request_id: RequestId(1), + session_id: SESSION_ID, + node_id: LEFT_LEAF_ID, + }), + ServerResponse::Ok(OkResponse { + request_id: RequestId(1), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(2), + session_id: SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(2), + snapshot: session_snapshot_from_state(&focused_state, SESSION_ID), + }), + ); + + let mut client = MuxClient::new(transport.clone()); + *client.state_mut() = demo_state(); + let (config, _tempdir) = manager_from_source( + r#" + fn on_focus(ctx) { action.focus_left() } + on("focus_changed", on_focus); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + configured + .render_session( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + ) + .await + .unwrap(); + + configured.process_next_event().await.unwrap(); + + assert_eq!( + configured + .client() + .state() + .sessions + .get(&SESSION_ID) + .and_then(|session| session.focused_leaf_id), + Some(LEFT_LEAF_ID) + ); + assert!(configured.notifications().is_empty()); + transport.assert_exhausted().unwrap(); +} + +#[tokio::test] +async fn viewportless_event_hooks_still_run_explicit_node_actions() { + let transport = ScriptedTransport::default(); + let state = second_session_state(); + let second_session = state + .sessions + .get(&SECOND_SESSION_ID) + .expect("second session") + .clone(); + transport.push_event(ServerEvent::SessionCreated(SessionCreatedEvent { + session: second_session.clone(), + })); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(1), + session_id: SECOND_SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(1), + snapshot: session_snapshot_from_state(&state, SECOND_SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Node(NodeRequest::MoveNodeAfter { + request_id: RequestId(2), + node_id: FOCUSED_LEAF_ID, + sibling_node_id: LEFT_LEAF_ID, + }), + ServerResponse::Ok(OkResponse { + request_id: RequestId(2), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::List { + request_id: RequestId(3), + }), + ServerResponse::Sessions(SessionsResponse { + request_id: RequestId(3), + sessions: vec![ + state + .sessions + .get(&SESSION_ID) + .expect("primary session") + .clone(), + second_session, + ], + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(4), + session_id: SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(4), + snapshot: session_snapshot_from_state(&state, SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(5), + session_id: SECOND_SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(5), + snapshot: session_snapshot_from_state(&state, SECOND_SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Buffer(embers_protocol::BufferRequest::List { + request_id: RequestId(6), + session_id: None, + attached_only: false, + detached_only: true, + }), + ServerResponse::Buffers(BuffersResponse { + request_id: RequestId(6), + buffers: Vec::new(), + }), + ); + + let mut client = MuxClient::new(transport.clone()); + *client.state_mut() = demo_state(); + let (config, _tempdir) = manager_from_source( + r#" + fn on_session(ctx) { action.move_node_after(32, 21) } + on("session_created", on_session); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + + let event = configured.process_next_event().await.unwrap(); + + assert!(matches!(event, ServerEvent::SessionCreated(_))); + assert!(configured.notifications().is_empty()); + transport.assert_exhausted().unwrap(); +} + +#[tokio::test] +async fn viewportless_focus_buffer_actions_resolve_session_from_the_buffer() { + let transport = ScriptedTransport::default(); + let mut state = second_session_state(); + let mut focused_state = state.clone(); + focused_state + .sessions + .get_mut(&SESSION_ID) + .expect("primary session") + .focused_leaf_id = Some(LEFT_LEAF_ID); + focused_state + .nodes + .get_mut(&LEFT_LEAF_ID) + .and_then(|node| node.buffer_view.as_mut()) + .expect("left leaf") + .focused = true; + focused_state + .nodes + .get_mut(&FOCUSED_LEAF_ID) + .and_then(|node| node.buffer_view.as_mut()) + .expect("previous focused leaf") + .focused = false; + state + .buffers + .get_mut(&BufferId(2)) + .expect("buffer record") + .attachment_node_id = Some(FOCUSED_LEAF_ID); + + transport.push_event(ServerEvent::ClientChanged(ClientChangedEvent { + client: ClientRecord { + id: 77, + current_session_id: None, + subscribed_all_sessions: true, + subscribed_session_ids: vec![], + }, + previous_session_id: Some(SECOND_SESSION_ID), + })); + transport.push_exchange( + ClientMessage::Client(ClientRequest::Get { + request_id: RequestId(1), + client_id: None, + }), + ServerResponse::Client(ClientResponse { + request_id: RequestId(1), + client: ClientRecord { + id: 77, + current_session_id: None, + subscribed_all_sessions: true, + subscribed_session_ids: vec![], + }, + }), + ); + transport.push_exchange( + ClientMessage::Buffer(embers_protocol::BufferRequest::GetLocation { + request_id: RequestId(2), + buffer_id: BufferId(2), + }), + ServerResponse::BufferLocation(embers_protocol::BufferLocationResponse { + request_id: RequestId(2), + location: embers_protocol::BufferLocation::session( + BufferId(2), + SESSION_ID, + LEFT_LEAF_ID, + ), + }), + ); + transport.push_exchange( + ClientMessage::Node(NodeRequest::Focus { + request_id: RequestId(3), + session_id: SESSION_ID, + node_id: LEFT_LEAF_ID, + }), + ServerResponse::Ok(OkResponse { + request_id: RequestId(3), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(4), + session_id: SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(4), + snapshot: session_snapshot_from_state(&focused_state, SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::List { + request_id: RequestId(5), + }), + ServerResponse::Sessions(SessionsResponse { + request_id: RequestId(5), + sessions: vec![ + focused_state + .sessions + .get(&SESSION_ID) + .expect("primary session") + .clone(), + state + .sessions + .get(&SECOND_SESSION_ID) + .expect("second session") + .clone(), + ], + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(6), + session_id: SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(6), + snapshot: session_snapshot_from_state(&focused_state, SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(7), + session_id: SECOND_SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(7), + snapshot: session_snapshot_from_state(&state, SECOND_SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Buffer(embers_protocol::BufferRequest::List { + request_id: RequestId(8), + session_id: None, + attached_only: false, + detached_only: true, + }), + ServerResponse::Buffers(BuffersResponse { + request_id: RequestId(8), + buffers: Vec::new(), + }), + ); + + let mut client = MuxClient::new(transport.clone()); + *client.state_mut() = state; + let (config, _tempdir) = manager_from_source( + r#" + fn on_client_changed(ctx) { action.focus_buffer(2) } + on("client_changed", on_client_changed); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + + let event = configured.process_next_event().await.unwrap(); + + assert!(matches!(event, ServerEvent::ClientChanged(_))); + transport.assert_exhausted().expect("all requests consumed"); +} + +#[tokio::test] +async fn focus_buffer_actions_force_cross_session_focus_requests() { + let transport = ScriptedTransport::default(); + let state = second_session_state(); + transport.push_exchange( + ClientMessage::Buffer(embers_protocol::BufferRequest::GetLocation { + request_id: RequestId(1), + buffer_id: SECOND_BUFFER_ID, + }), + ServerResponse::BufferLocation(embers_protocol::BufferLocationResponse { + request_id: RequestId(1), + location: embers_protocol::BufferLocation::session( + SECOND_BUFFER_ID, + SECOND_SESSION_ID, + SECOND_ROOT_ID, + ), + }), + ); + transport.push_exchange( + ClientMessage::Node(NodeRequest::Focus { + request_id: RequestId(2), + session_id: SECOND_SESSION_ID, + node_id: SECOND_ROOT_ID, + }), + ServerResponse::Ok(OkResponse { + request_id: RequestId(2), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(3), + session_id: SECOND_SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(3), + snapshot: session_snapshot_from_state(&state, SECOND_SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::List { + request_id: RequestId(4), + }), + ServerResponse::Sessions(SessionsResponse { + request_id: RequestId(4), + sessions: vec![ + state + .sessions + .get(&SESSION_ID) + .expect("primary session") + .clone(), + state + .sessions + .get(&SECOND_SESSION_ID) + .expect("second session") + .clone(), + ], + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(5), + session_id: SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(5), + snapshot: session_snapshot_from_state(&state, SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(6), + session_id: SECOND_SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(6), + snapshot: session_snapshot_from_state(&state, SECOND_SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Buffer(embers_protocol::BufferRequest::List { + request_id: RequestId(7), + session_id: None, + attached_only: false, + detached_only: true, + }), + ServerResponse::Buffers(BuffersResponse { + request_id: RequestId(7), + buffers: Vec::new(), + }), + ); + + let client = MuxClient::new(transport.clone()); + let (config, _tempdir) = manager_from_source( + r#" + fn focus_other(ctx) { action.focus_buffer(70) } + define_action("focus-other", focus_other); + bind("normal", "", "focus-other"); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = state; + + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Ctrl('o'), + ) + .await + .unwrap(); + + assert!(configured.notifications().is_empty()); + transport.assert_exhausted().expect("all requests consumed"); +} + +#[tokio::test] +async fn cross_session_focus_actions_focus_out_the_active_session_buffer() { + let transport = FakeTransport::default(); + let mut state = second_session_state(); + state + .snapshots + .get_mut(&FOCUSED_BUFFER_ID) + .expect("primary snapshot") + .focus_reporting = true; + state + .snapshots + .get_mut(&SECOND_BUFFER_ID) + .expect("secondary snapshot") + .focus_reporting = true; + + transport.push_response(ServerResponse::BufferLocation( + embers_protocol::BufferLocationResponse { + request_id: RequestId(1), + location: embers_protocol::BufferLocation::session( + SECOND_BUFFER_ID, + SECOND_SESSION_ID, + SECOND_ROOT_ID, + ), + }, + )); + transport.push_response(ServerResponse::Ok(OkResponse { + request_id: RequestId(2), + })); + transport.push_response(ServerResponse::Ok(OkResponse { + request_id: RequestId(3), + })); + transport.push_response(ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(4), + snapshot: session_snapshot_from_state(&state, SECOND_SESSION_ID), + })); + transport.push_response(ServerResponse::Ok(OkResponse { + request_id: RequestId(5), + })); + transport.push_response(ServerResponse::VisibleSnapshot( + visible_snapshot_from_state(&state, SECOND_BUFFER_ID, RequestId(6)), + )); + transport.push_response(ServerResponse::VisibleSnapshot( + visible_snapshot_from_state(&state, FOCUSED_BUFFER_ID, RequestId(7)), + )); + transport.push_response(ServerResponse::Sessions(SessionsResponse { + request_id: RequestId(8), + sessions: vec![ + state + .sessions + .get(&SESSION_ID) + .expect("primary session") + .clone(), + state + .sessions + .get(&SECOND_SESSION_ID) + .expect("second session") + .clone(), + ], + })); + transport.push_response(ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(9), + snapshot: session_snapshot_from_state(&state, SESSION_ID), + })); + transport.push_response(ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(10), + snapshot: session_snapshot_from_state(&state, SECOND_SESSION_ID), + })); + transport.push_response(ServerResponse::Buffers(BuffersResponse { + request_id: RequestId(11), + buffers: Vec::new(), + })); + + let client = MuxClient::new(transport.clone()); + let (config, _tempdir) = manager_from_source( + r#" + fn focus_other(ctx) { action.focus_buffer(70) } + define_action("focus-other", focus_other); + bind("normal", "", "focus-other"); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = state; configured - .handle_mouse( + .render_session( SESSION_ID, Size { width: 80, height: 20, }, - MouseEvent { - row: (focused.rect.origin.y + 1) as u16, - column: focused.rect.origin.x as u16, - modifiers: MouseModifiers::default(), - kind: MouseEventKind::WheelUp, + ) + .await + .expect("render current session"); + + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, }, + KeyEvent::Ctrl('o'), ) .await .unwrap(); - assert!(configured.client().transport().requests().is_empty()); + assert_eq!( + transport.requests(), + vec![ + ClientMessage::Buffer(embers_protocol::BufferRequest::GetLocation { + request_id: RequestId(1), + buffer_id: SECOND_BUFFER_ID, + }), + ClientMessage::Input(InputRequest::Send { + request_id: RequestId(2), + buffer_id: FOCUSED_BUFFER_ID, + bytes: b"\x1b[O".to_vec(), + }), + ClientMessage::Node(NodeRequest::Focus { + request_id: RequestId(3), + session_id: SECOND_SESSION_ID, + node_id: SECOND_ROOT_ID, + }), + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(4), + session_id: SECOND_SESSION_ID, + }), + ClientMessage::Input(InputRequest::Send { + request_id: RequestId(5), + buffer_id: SECOND_BUFFER_ID, + bytes: b"\x1b[I".to_vec(), + }), + ClientMessage::Buffer(embers_protocol::BufferRequest::CaptureVisible { + request_id: RequestId(6), + buffer_id: SECOND_BUFFER_ID, + }), + ClientMessage::Buffer(embers_protocol::BufferRequest::CaptureVisible { + request_id: RequestId(7), + buffer_id: FOCUSED_BUFFER_ID, + }), + ClientMessage::Session(SessionRequest::List { + request_id: RequestId(8), + }), + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(9), + session_id: SESSION_ID, + }), + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(10), + session_id: SECOND_SESSION_ID, + }), + ClientMessage::Buffer(embers_protocol::BufferRequest::List { + request_id: RequestId(11), + session_id: None, + attached_only: false, + detached_only: true, + }), + ] + ); + assert!(configured.notifications().is_empty()); } #[tokio::test] -async fn event_hook_executes_real_actions() { +async fn node_changed_focus_buffer_actions_use_active_session_for_shortcuts() { let transport = ScriptedTransport::default(); - let focused_state = root_focus_state(); - transport.push_event(ServerEvent::FocusChanged(FocusChangedEvent { - session_id: SESSION_ID, - focused_leaf_id: Some(FOCUSED_LEAF_ID), - focused_floating_id: None, + let state = second_session_state(); + transport.push_event(ServerEvent::NodeChanged(NodeChangedEvent { + session_id: SECOND_SESSION_ID, })); transport.push_exchange( - ClientMessage::Node(NodeRequest::Focus { + ClientMessage::Session(SessionRequest::Get { request_id: RequestId(1), - session_id: SESSION_ID, - node_id: LEFT_LEAF_ID, + session_id: SECOND_SESSION_ID, }), - ServerResponse::Ok(OkResponse { + ServerResponse::SessionSnapshot(SessionSnapshotResponse { request_id: RequestId(1), + snapshot: session_snapshot_from_state(&state, SECOND_SESSION_ID), }), ); transport.push_exchange( - ClientMessage::Session(SessionRequest::Get { + ClientMessage::Buffer(embers_protocol::BufferRequest::List { + request_id: RequestId(2), + session_id: None, + attached_only: false, + detached_only: true, + }), + ServerResponse::Buffers(BuffersResponse { request_id: RequestId(2), + buffers: Vec::new(), + }), + ); + transport.push_exchange( + ClientMessage::Buffer(embers_protocol::BufferRequest::GetLocation { + request_id: RequestId(3), + buffer_id: SECOND_BUFFER_ID, + }), + ServerResponse::BufferLocation(embers_protocol::BufferLocationResponse { + request_id: RequestId(3), + location: embers_protocol::BufferLocation::session( + SECOND_BUFFER_ID, + SECOND_SESSION_ID, + SECOND_ROOT_ID, + ), + }), + ); + transport.push_exchange( + ClientMessage::Node(NodeRequest::Focus { + request_id: RequestId(4), + session_id: SECOND_SESSION_ID, + node_id: SECOND_ROOT_ID, + }), + ServerResponse::Ok(OkResponse { + request_id: RequestId(4), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(5), + session_id: SECOND_SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(5), + snapshot: session_snapshot_from_state(&state, SECOND_SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::List { + request_id: RequestId(6), + }), + ServerResponse::Sessions(SessionsResponse { + request_id: RequestId(6), + sessions: vec![ + state + .sessions + .get(&SESSION_ID) + .expect("primary session") + .clone(), + state + .sessions + .get(&SECOND_SESSION_ID) + .expect("second session") + .clone(), + ], + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(7), session_id: SESSION_ID, }), ServerResponse::SessionSnapshot(SessionSnapshotResponse { - request_id: RequestId(2), - snapshot: session_snapshot_from_state(&focused_state, SESSION_ID), + request_id: RequestId(7), + snapshot: session_snapshot_from_state(&state, SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(8), + session_id: SECOND_SESSION_ID, + }), + ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(8), + snapshot: session_snapshot_from_state(&state, SECOND_SESSION_ID), + }), + ); + transport.push_exchange( + ClientMessage::Buffer(embers_protocol::BufferRequest::List { + request_id: RequestId(9), + session_id: None, + attached_only: false, + detached_only: true, + }), + ServerResponse::Buffers(BuffersResponse { + request_id: RequestId(9), + buffers: Vec::new(), }), ); - let mut client = MuxClient::new(transport.clone()); - *client.state_mut() = demo_state(); + let client = MuxClient::new(transport.clone()); let (config, _tempdir) = manager_from_source( r#" - fn on_focus(ctx) { action.focus_left() } - on("focus_changed", on_focus); + fn on_node_changed(ctx) { action.focus_buffer(70) } + on("node_changed", on_node_changed); "#, ); let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = state; configured .render_session( SESSION_ID, @@ -1111,21 +2245,84 @@ async fn event_hook_executes_real_actions() { }, ) .await + .expect("render primary session"); + + let event = configured.process_next_event().await.unwrap(); + + assert!(matches!(event, ServerEvent::NodeChanged(_))); + assert!(configured.notifications().is_empty()); + transport.assert_exhausted().expect("all requests consumed"); +} + +#[tokio::test] +async fn scripted_send_keys_current_forwards_to_the_focused_buffer() { + let transport = FakeTransport::default(); + let state = demo_state(); + push_send_input_refresh_responses(&transport, &state, FOCUSED_BUFFER_ID); + + let client = MuxClient::new(transport.clone()); + let (config, _tempdir) = manager_from_source( + r#" + fn send_current(ctx) { action.send_keys_current("abc") } + define_action("send-current", send_current); + bind("normal", "", "send-current"); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = state; + + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Ctrl('g'), + ) + .await .unwrap(); - configured.process_next_event().await.unwrap(); + assert_eq!( + transport.requests(), + expected_send_input_refresh_requests(FOCUSED_BUFFER_ID, b"abc") + ); +} + +#[tokio::test] +async fn scripted_send_bytes_can_target_a_specific_buffer() { + let transport = FakeTransport::default(); + let state = demo_state(); + let target_buffer_id = BufferId(5); + push_send_input_refresh_responses(&transport, &state, target_buffer_id); + + let client = MuxClient::new(transport.clone()); + let (config, _tempdir) = manager_from_source( + r#" + fn send_popup(ctx) { action.send_bytes(5, "popup") } + define_action("send-popup", send_popup); + bind("normal", "", "send-popup"); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = state; + + configured + .handle_key( + SESSION_ID, + Size { + width: 80, + height: 20, + }, + KeyEvent::Ctrl('p'), + ) + .await + .unwrap(); assert_eq!( - configured - .client() - .state() - .sessions - .get(&SESSION_ID) - .and_then(|session| session.focused_leaf_id), - Some(LEFT_LEAF_ID) + transport.requests(), + expected_send_input_refresh_requests(target_buffer_id, b"popup") ); - assert!(configured.notifications().is_empty()); - transport.assert_exhausted().unwrap(); } #[tokio::test] @@ -1417,3 +2614,101 @@ async fn detached_client_changed_hooks_have_no_current_session() { assert_eq!(configured.notifications(), ["detached"]); transport.assert_exhausted().expect("all requests consumed"); } + +#[tokio::test] +async fn detached_client_changed_actions_require_current_session_context() { + let transport = ScriptedTransport::default(); + transport.push_event(ServerEvent::ClientChanged(ClientChangedEvent { + client: ClientRecord { + id: 77, + current_session_id: None, + subscribed_all_sessions: true, + subscribed_session_ids: vec![], + }, + previous_session_id: Some(SESSION_ID), + })); + transport.push_exchange( + ClientMessage::Client(ClientRequest::Get { + request_id: RequestId(1), + client_id: None, + }), + ServerResponse::Client(ClientResponse { + request_id: RequestId(1), + client: ClientRecord { + id: 77, + current_session_id: None, + subscribed_all_sessions: true, + subscribed_session_ids: vec![], + }, + }), + ); + let client = MuxClient::new(transport.clone()); + let (config, _tempdir) = manager_from_source( + r#" + fn on_client_changed(ctx) { action.focus_left() } + on("client_changed", on_client_changed); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = demo_state(); + + let error = configured + .process_next_event() + .await + .expect_err("missing context should fail the event hook"); + + assert!(error + .to_string() + .contains("cannot execute action FocusDirection { direction: Left } without current_session_id and current_viewport")); + assert!(configured.notifications().is_empty()); + transport.assert_exhausted().expect("all requests consumed"); +} + +#[tokio::test] +async fn detached_client_changed_unzoom_actions_require_current_session_context() { + let transport = ScriptedTransport::default(); + transport.push_event(ServerEvent::ClientChanged(ClientChangedEvent { + client: ClientRecord { + id: 77, + current_session_id: None, + subscribed_all_sessions: true, + subscribed_session_ids: vec![], + }, + previous_session_id: Some(SESSION_ID), + })); + transport.push_exchange( + ClientMessage::Client(ClientRequest::Get { + request_id: RequestId(1), + client_id: None, + }), + ServerResponse::Client(ClientResponse { + request_id: RequestId(1), + client: ClientRecord { + id: 77, + current_session_id: None, + subscribed_all_sessions: true, + subscribed_session_ids: vec![], + }, + }), + ); + let client = MuxClient::new(transport.clone()); + let (config, _tempdir) = manager_from_source( + r#" + fn on_client_changed(ctx) { action.unzoom_current_session() } + on("client_changed", on_client_changed); + "#, + ); + let mut configured = ConfiguredClient::new(client, config); + *configured.client_mut().state_mut() = demo_state(); + + let error = configured + .process_next_event() + .await + .expect_err("missing session should fail the unzoom event hook"); + + assert!(error.to_string().contains( + "cannot execute action UnzoomNode { session_id: None } without current_session_id" + )); + assert!(configured.notifications().is_empty()); + transport.assert_exhausted().expect("all requests consumed"); +} diff --git a/crates/embers-client/tests/e2e.rs b/crates/embers-client/tests/e2e.rs index bbd2506..a6a1e2a 100644 --- a/crates/embers-client/tests/e2e.rs +++ b/crates/embers-client/tests/e2e.rs @@ -1,15 +1,15 @@ use std::process::Output; -use std::time::Duration; use embers_client::{MuxClient, PresentationModel, Renderer}; use embers_core::{ ActivityState, BufferId, FloatGeometry, NodeId, SessionId, Size, SplitDirection, new_request_id, }; use embers_protocol::{ - BufferRequest, BufferResponse, BuffersResponse, ClientMessage, FloatingRequest, NodeRequest, - ServerResponse, SessionRequest, SessionSnapshot, + BufferRecord, BufferRequest, BufferResponse, BuffersResponse, ClientMessage, FloatingRequest, + NodeRequest, ServerResponse, SessionRequest, SessionSnapshot, VisibleSnapshotResponse, }; use embers_test_support::{TestConnection, TestServer, cargo_bin}; +use tokio::time::{Duration, Instant}; fn run_cli(server: &TestServer, args: &[&str]) -> Output { let output = cargo_bin("embers") @@ -48,12 +48,20 @@ async fn create_session(connection: &mut TestConnection, name: &str) -> SessionS async fn create_buffer( connection: &mut TestConnection, title: &str, +) -> embers_protocol::BufferRecord { + create_buffer_with_command(connection, title, vec!["/bin/sh".to_owned()]).await +} + +async fn create_buffer_with_command( + connection: &mut TestConnection, + title: &str, + command: Vec, ) -> embers_protocol::BufferRecord { let response = connection .request(&ClientMessage::Buffer(BufferRequest::Create { request_id: new_request_id(), title: Some(title.to_owned()), - command: vec!["/bin/sh".to_owned()], + command, cwd: None, env: Default::default(), })) @@ -115,14 +123,14 @@ fn session_id_by_name(client: &MuxClient, name: .id } -async fn refresh_all_snapshots(client: &mut MuxClient) { +async fn refresh_all_snapshots( + client: &mut MuxClient, +) -> embers_core::Result<()> { let buffer_ids = client.state().buffers.keys().copied().collect::>(); for buffer_id in buffer_ids { - client - .refresh_buffer_snapshot(buffer_id) - .await - .unwrap_or_else(|error| panic!("refreshing snapshot for {buffer_id} failed: {error}")); + let _ = client.refresh_buffer_snapshot(buffer_id).await; } + Ok(()) } async fn render_session( @@ -130,7 +138,9 @@ async fn render_session( session_name: &str, ) -> String { client.resync_all_sessions().await.expect("resync succeeds"); - refresh_all_snapshots(client).await; + refresh_all_snapshots(client) + .await + .expect("refresh snapshots succeeds"); let session_id = session_id_by_name(client, session_name); let model = PresentationModel::project( client.state(), @@ -144,6 +154,241 @@ async fn render_session( Renderer.render(client.state(), &model).render() } +async fn poll_for_tab_activity( + client: &mut MuxClient, + session_name: &str, + tabs_node_id: NodeId, + target_title: &str, + expected_activity: ActivityState, + timeout: Duration, + poll_interval: Duration, +) -> embers_core::Result { + let deadline = Instant::now() + timeout; + + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Ok(false); + } + match tokio::time::timeout(remaining, client.resync_all_sessions()).await { + Ok(Ok(())) => {} + Ok(Err(error)) => return Err(error), + Err(_) => return Ok(false), + } + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Ok(false); + } + match tokio::time::timeout(remaining, refresh_all_snapshots(client)).await { + Ok(Ok(())) => {} + Ok(Err(error)) => return Err(error), + Err(_) => return Ok(false), + } + let maybe_session_id = client + .state() + .sessions + .values() + .find(|session| session.name == session_name) + .map(|session| session.id); + if let Some(session_id) = maybe_session_id + && let Ok(model) = PresentationModel::project( + client.state(), + session_id, + Size { + width: 80, + height: 24, + }, + ) + && let Some(tabs) = model + .tab_bars + .iter() + .find(|tabs| tabs.node_id == tabs_node_id) + && tabs + .tabs + .iter() + .any(|tab| tab.title == target_title && tab.activity == expected_activity) + { + return Ok(true); + } + if Instant::now() >= deadline { + return Ok(false); + } + tokio::time::sleep(poll_interval).await; + } +} + +async fn wait_for_visible_snapshot( + connection: &mut TestConnection, + buffer_id: BufferId, + timeout: Duration, + mut predicate: F, +) -> VisibleSnapshotResponse +where + F: FnMut(&VisibleSnapshotResponse) -> bool, +{ + let deadline = Instant::now() + timeout; + let mut last_snapshot = "".to_owned(); + + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + let snapshot = + tokio::time::timeout(remaining, connection.capture_visible_buffer(buffer_id)) + .await + .unwrap_or_else(|_| { + panic!("timed out waiting for visible snapshot; last snapshot: {last_snapshot}") + }) + .expect("visible capture succeeds"); + if predicate(&snapshot) { + return snapshot; + } + last_snapshot = format!("{snapshot:?}"); + + if Instant::now() >= deadline { + panic!("timed out waiting for visible snapshot; last snapshot: {last_snapshot}"); + } + + tokio::time::sleep(Duration::from_millis(50)).await; + } +} + +async fn buffer_record(connection: &mut TestConnection, buffer_id: BufferId) -> BufferRecord { + match connection + .request(&ClientMessage::Buffer(BufferRequest::Get { + request_id: new_request_id(), + buffer_id, + })) + .await + .expect("get buffer succeeds") + { + ServerResponse::Buffer(BufferResponse { buffer, .. }) => buffer, + other => panic!("expected buffer response, got {other:?}"), + } +} + +async fn wait_for_buffer_activity( + connection: &mut TestConnection, + buffer_id: BufferId, + expected: ActivityState, + timeout: Duration, +) -> BufferRecord { + let deadline = Instant::now() + timeout; + let mut last_activity = "".to_owned(); + + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + let buffer = tokio::time::timeout(remaining, buffer_record(connection, buffer_id)) + .await + .unwrap_or_else(|_| { + panic!( + "timed out waiting for buffer {buffer_id} activity {expected:?}; last activity: {last_activity}" + ) + }); + if buffer.activity == expected { + return buffer; + } + last_activity = format!("{:?}", buffer.activity); + + if Instant::now() >= deadline { + panic!( + "timed out waiting for buffer {buffer_id} activity {expected:?}; last activity: {last_activity}" + ); + } + + tokio::time::sleep(Duration::from_millis(50)).await; + } +} + +struct HiddenTabFixture { + nested_tabs_id: NodeId, + hidden_buffer: BufferRecord, +} + +async fn create_hidden_tab_fixture(connection: &mut TestConnection) -> HiddenTabFixture { + let hidden_buffer = create_buffer(connection, "hidden").await; + create_hidden_tab_fixture_with_buffer(connection, hidden_buffer).await +} + +async fn create_hidden_tab_fixture_with_buffer( + connection: &mut TestConnection, + hidden_buffer: BufferRecord, +) -> HiddenTabFixture { + let session = create_session(connection, "alpha").await; + let buffer_a = create_buffer(connection, "main").await; + let session = match connection + .request(&ClientMessage::Session(SessionRequest::AddRootTab { + request_id: new_request_id(), + session_id: session.session.id, + title: "main".to_owned(), + buffer_id: Some(buffer_a.id), + child_node_id: None, + })) + .await + .expect("add root tab succeeds") + { + ServerResponse::SessionSnapshot(response) => response.snapshot, + other => panic!("expected session snapshot response, got {other:?}"), + }; + let main_leaf = session.session.focused_leaf_id.expect("main leaf exists"); + + let session = match connection + .request(&ClientMessage::Node(NodeRequest::WrapInTabs { + request_id: new_request_id(), + node_id: main_leaf, + title: "main".to_owned(), + })) + .await + .expect("wrap main leaf in tabs succeeds") + { + ServerResponse::SessionSnapshot(response) => response.snapshot, + other => panic!("expected session snapshot response, got {other:?}"), + }; + let nested_tabs_id = node(&session, main_leaf) + .parent_id + .expect("wrapped main leaf has tabs parent"); + + let _ = connection + .request(&ClientMessage::Node(NodeRequest::AddTab { + request_id: new_request_id(), + tabs_node_id: nested_tabs_id, + title: "bg".to_owned(), + buffer_id: Some(hidden_buffer.id), + child_node_id: None, + index: 1, + })) + .await + .expect("add hidden tab succeeds"); + let _ = connection + .request(&ClientMessage::Node(NodeRequest::SelectTab { + request_id: new_request_id(), + tabs_node_id: nested_tabs_id, + index: 0, + })) + .await + .expect("select visible tab succeeds"); + + HiddenTabFixture { + nested_tabs_id, + hidden_buffer, + } +} + +fn fullscreen_fixture_command( + live_title: &str, + restored_title: &str, + sleep_secs: &str, +) -> Vec { + vec![ + "/bin/sh".to_owned(), + "-lc".to_owned(), + format!( + "printf 'main-before\\n'; \ + printf '\\033]0;{live_title}\\007\\033[?1049h\\033[2J\\033[Hfullscreen-live\\033[3;10Hcursor-target'; \ + sleep {sleep_secs}; \ + printf '\\033]0;{restored_title}\\007\\033[?1049lrestored-after\\n'" + ), + ] +} + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn basic_cli_workflow_renders_split_output() { let server = TestServer::start().await.expect("server starts"); @@ -463,6 +708,52 @@ async fn move_and_detach_workflows_preserve_running_buffers() { })) .await .expect("detach succeeds"); + assert!( + buffer_record(&mut connection, buffer_a.id) + .await + .attachment_node_id + .is_none() + ); + + let _ = connection + .request(&ClientMessage::Input(embers_protocol::InputRequest::Send { + request_id: new_request_id(), + buffer_id: buffer_a.id, + bytes: b"printf detached-output\\n\r".to_vec(), + })) + .await + .expect("send detached output succeeds"); + connection + .wait_for_capture_contains(buffer_a.id, "detached-output", Duration::from_secs(3)) + .await + .expect("detached buffer captures output"); + wait_for_buffer_activity( + &mut connection, + buffer_a.id, + ActivityState::Activity, + Duration::from_secs(3), + ) + .await; + + let _ = connection + .request(&ClientMessage::Input(embers_protocol::InputRequest::Send { + request_id: new_request_id(), + buffer_id: buffer_a.id, + bytes: b"printf 'detached-bell'; printf '\\a'; sleep 0.5\r".to_vec(), + })) + .await + .expect("send detached bell succeeds"); + connection + .wait_for_capture_contains(buffer_a.id, "detached-bell", Duration::from_secs(3)) + .await + .expect("detached buffer captures bell marker"); + wait_for_buffer_activity( + &mut connection, + buffer_a.id, + ActivityState::Bell, + Duration::from_secs(3), + ) + .await; let popup = match connection .request(&ClientMessage::Floating(FloatingRequest::Create { @@ -481,6 +772,10 @@ async fn move_and_detach_workflows_preserve_running_buffers() { ServerResponse::Floating(response) => response.floating, other => panic!("expected floating response, got {other:?}"), }; + assert_eq!( + buffer_record(&mut connection, buffer_a.id).await.activity, + ActivityState::Idle + ); let _ = connection .request(&ClientMessage::Input(embers_protocol::InputRequest::Send { request_id: new_request_id(), @@ -527,6 +822,13 @@ async fn move_and_detach_workflows_preserve_running_buffers() { })) .await .expect("reattach detached buffer succeeds"); + assert_eq!( + buffer_record(&mut connection, buffer_a.id) + .await + .attachment_node_id, + Some(target_leaf), + "buffer should be reattached to the requested target leaf" + ); let _ = connection .request(&ClientMessage::Input(embers_protocol::InputRequest::Send { request_id: new_request_id(), @@ -566,119 +868,364 @@ async fn hidden_activity_is_visible_and_reconnect_rehydrates_state() { .await .expect("protocol connection"); - let session = create_session(&mut connection, "alpha").await; - let buffer_a = create_buffer(&mut connection, "main").await; - let session = match connection - .request(&ClientMessage::Session(SessionRequest::AddRootTab { + let fixture = create_hidden_tab_fixture(&mut connection).await; + + let _ = connection + .request(&ClientMessage::Input(embers_protocol::InputRequest::Send { request_id: new_request_id(), - session_id: session.session.id, - title: "main".to_owned(), - buffer_id: Some(buffer_a.id), - child_node_id: None, + buffer_id: fixture.hidden_buffer.id, + bytes: b"printf hidden-activity\\n\r".to_vec(), })) .await - .expect("add root tab succeeds") - { - ServerResponse::SessionSnapshot(response) => response.snapshot, - other => panic!("expected session snapshot response, got {other:?}"), - }; - let main_leaf = session.session.focused_leaf_id.expect("main leaf exists"); + .expect("send to hidden buffer succeeds"); + connection + .wait_for_capture_contains( + fixture.hidden_buffer.id, + "hidden-activity", + Duration::from_secs(3), + ) + .await + .expect("hidden buffer captures output"); + wait_for_buffer_activity( + &mut connection, + fixture.hidden_buffer.id, + ActivityState::Activity, + Duration::from_secs(3), + ) + .await; - let session = match connection - .request(&ClientMessage::Node(NodeRequest::WrapInTabs { - request_id: new_request_id(), - node_id: main_leaf, - title: "main".to_owned(), - })) + let mut first_client = MuxClient::connect(server.socket_path()) .await - .expect("wrap main leaf in tabs succeeds") - { - ServerResponse::SessionSnapshot(response) => response.snapshot, - other => panic!("expected session snapshot response, got {other:?}"), - }; - let nested_tabs_id = node(&session, main_leaf) - .parent_id - .expect("wrapped main leaf has tabs parent"); + .expect("first client connects"); + let saw_hidden_activity = poll_for_tab_activity( + &mut first_client, + "alpha", + fixture.nested_tabs_id, + "bg", + ActivityState::Activity, + Duration::from_secs(2), + Duration::from_millis(50), + ) + .await + .expect("poll hidden activity succeeds"); + assert!( + saw_hidden_activity, + "hidden tab activity should propagate before reconnect" + ); + + drop(first_client); - let buffer_b = create_buffer(&mut connection, "hidden").await; let _ = connection - .request(&ClientMessage::Node(NodeRequest::AddTab { + .request(&ClientMessage::Node(NodeRequest::SelectTab { request_id: new_request_id(), - tabs_node_id: nested_tabs_id, - title: "bg".to_owned(), - buffer_id: Some(buffer_b.id), - child_node_id: None, + tabs_node_id: fixture.nested_tabs_id, index: 1, })) .await - .expect("add hidden tab succeeds"); - let _ = connection - .request(&ClientMessage::Node(NodeRequest::SelectTab { - request_id: new_request_id(), - tabs_node_id: nested_tabs_id, - index: 0, - })) + .expect("select hidden tab succeeds"); + wait_for_buffer_activity( + &mut connection, + fixture.hidden_buffer.id, + ActivityState::Idle, + Duration::from_secs(3), + ) + .await; + + let mut second_client = MuxClient::connect(server.socket_path()) .await - .expect("select visible tab succeeds"); + .expect("second client connects"); + let render = render_session(&mut second_client, "alpha").await; + assert!(render.contains("hidden-activity")); + + server.shutdown().await.expect("server shuts down"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn hidden_bell_is_visible_to_clients_until_revealed() { + let server = TestServer::start().await.expect("server starts"); + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("protocol connection"); + + let fixture = create_hidden_tab_fixture(&mut connection).await; let _ = connection .request(&ClientMessage::Input(embers_protocol::InputRequest::Send { request_id: new_request_id(), - buffer_id: buffer_b.id, - bytes: b"printf hidden-activity\\n\r".to_vec(), + buffer_id: fixture.hidden_buffer.id, + bytes: b"printf 'hidden-bell'; printf '\\a'; sleep 0.5\r".to_vec(), })) .await - .expect("send to hidden buffer succeeds"); + .expect("send hidden bell succeeds"); connection - .wait_for_capture_contains(buffer_b.id, "hidden-activity", Duration::from_secs(3)) + .wait_for_capture_contains( + fixture.hidden_buffer.id, + "hidden-bell", + Duration::from_secs(3), + ) .await - .expect("hidden buffer captures output"); + .expect("hidden bell marker appears"); + wait_for_buffer_activity( + &mut connection, + fixture.hidden_buffer.id, + ActivityState::Bell, + Duration::from_secs(3), + ) + .await; - let mut first_client = MuxClient::connect(server.socket_path()) + let mut client = MuxClient::connect(server.socket_path()) .await - .expect("first client connects"); - first_client + .expect("client connects"); + client .resync_all_sessions() .await - .expect("first client resyncs"); - refresh_all_snapshots(&mut first_client).await; - let session_id = session_id_by_name(&first_client, "alpha"); - let model = PresentationModel::project( - first_client.state(), - session_id, - Size { - width: 80, - height: 24, - }, + .expect("client resyncs sessions"); + refresh_all_snapshots(&mut client) + .await + .expect("refresh snapshots succeeds"); + let saw_hidden_bell = poll_for_tab_activity( + &mut client, + "alpha", + fixture.nested_tabs_id, + "bg", + ActivityState::Bell, + Duration::from_secs(2), + Duration::from_millis(50), ) - .expect("projection succeeds"); - let tabs = model - .tab_bars - .iter() - .find(|tabs| tabs.node_id == nested_tabs_id) - .expect("nested tabs frame exists"); + .await + .expect("poll hidden bell succeeds"); assert!( - tabs.tabs - .iter() - .any(|tab| tab.title == "bg" && tab.activity != ActivityState::Idle) + saw_hidden_bell, + "timed out waiting for bell activity after reconnect" ); - drop(first_client); - let _ = connection .request(&ClientMessage::Node(NodeRequest::SelectTab { request_id: new_request_id(), - tabs_node_id: nested_tabs_id, + tabs_node_id: fixture.nested_tabs_id, index: 1, })) .await .expect("select hidden tab succeeds"); + wait_for_buffer_activity( + &mut connection, + fixture.hidden_buffer.id, + ActivityState::Idle, + Duration::from_secs(3), + ) + .await; + let cleared_hidden_bell = poll_for_tab_activity( + &mut client, + "alpha", + fixture.nested_tabs_id, + "bg", + ActivityState::Idle, + Duration::from_secs(2), + Duration::from_millis(50), + ) + .await + .expect("poll cleared hidden bell succeeds"); + assert!( + cleared_hidden_bell, + "hidden tab kept Bell activity after being revealed" + ); - let mut second_client = MuxClient::connect(server.socket_path()) + server.shutdown().await.expect("server shuts down"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn fullscreen_fixture_enters_alternate_screen_and_restores_primary_screen() { + let server = TestServer::start().await.expect("server starts"); + let mut connection = TestConnection::connect(server.socket_path()) .await - .expect("second client connects"); - let render = render_session(&mut second_client, "alpha").await; - assert!(render.contains("hidden-activity")); + .expect("protocol connection"); + + let session = create_session(&mut connection, "alpha").await; + let buffer = create_buffer_with_command( + &mut connection, + "fullscreen", + fullscreen_fixture_command("fullscreen-live-title", "primary-restored-title", "3.0"), + ) + .await; + let _ = connection + .request(&ClientMessage::Session(SessionRequest::AddRootTab { + request_id: new_request_id(), + session_id: session.session.id, + title: "fullscreen".to_owned(), + buffer_id: Some(buffer.id), + child_node_id: None, + })) + .await + .expect("add fullscreen tab succeeds"); + + let live = wait_for_visible_snapshot( + &mut connection, + buffer.id, + Duration::from_secs(3), + |snapshot| { + let text = snapshot.lines.join("\n"); + snapshot.alternate_screen + && snapshot.title.as_deref() == Some("fullscreen-live-title") + && text.contains("fullscreen-live") + && text.contains("cursor-target") + }, + ) + .await; + let live_text = live.lines.join("\n"); + assert!(!live_text.contains("main-before")); + + let mut client = MuxClient::connect(server.socket_path()) + .await + .expect("client connects"); + let render = render_session(&mut client, "alpha").await; + assert!(render.contains("fullscreen-live")); + assert!(render.contains("cursor-target")); + assert!(!render.contains("main-before")); + + let restored = wait_for_visible_snapshot( + &mut connection, + buffer.id, + Duration::from_secs(4), + |snapshot| { + let text = snapshot.lines.join("\n"); + !snapshot.alternate_screen + && snapshot.title.as_deref() == Some("primary-restored-title") + && text.contains("main-before") + && text.contains("restored-after") + }, + ) + .await; + let restored_text = restored.lines.join("\n"); + assert!(!restored_text.contains("fullscreen-live")); + + server.shutdown().await.expect("server shuts down"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn hidden_fullscreen_buffer_reveals_live_alternate_screen_coherently() { + let server = TestServer::start().await.expect("server starts"); + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("protocol connection"); + + let hidden_buffer = create_buffer_with_command( + &mut connection, + "fullscreen-hidden", + fullscreen_fixture_command( + "fullscreen-hidden-live", + "fullscreen-hidden-restored", + "4.0", + ), + ) + .await; + let fixture = create_hidden_tab_fixture_with_buffer(&mut connection, hidden_buffer).await; + + wait_for_visible_snapshot( + &mut connection, + fixture.hidden_buffer.id, + Duration::from_secs(3), + |snapshot| { + snapshot.alternate_screen + && snapshot.title.as_deref() == Some("fullscreen-hidden-live") + && snapshot.lines.join("\n").contains("fullscreen-live") + }, + ) + .await; + + let _ = connection + .request(&ClientMessage::Node(NodeRequest::SelectTab { + request_id: new_request_id(), + tabs_node_id: fixture.nested_tabs_id, + index: 1, + })) + .await + .expect("select fullscreen tab succeeds"); + + let mut client = MuxClient::connect(server.socket_path()) + .await + .expect("client connects"); + let live_render = render_session(&mut client, "alpha").await; + assert!(live_render.contains("fullscreen-live")); + assert!(live_render.contains("cursor-target")); + assert!(!live_render.contains("main-before")); + + let restored = wait_for_visible_snapshot( + &mut connection, + fixture.hidden_buffer.id, + Duration::from_secs(6), + |snapshot| { + let text = snapshot.lines.join("\n"); + !snapshot.alternate_screen + && snapshot.title.as_deref() == Some("fullscreen-hidden-restored") + && text.contains("main-before") + && text.contains("restored-after") + }, + ) + .await; + assert!(!restored.lines.join("\n").contains("fullscreen-live")); + + let restored_render = render_session(&mut client, "alpha").await; + assert!(restored_render.contains("main-before")); + assert!(restored_render.contains("restored-after")); + assert!(!restored_render.contains("fullscreen-live")); + + server.shutdown().await.expect("server shuts down"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn rapid_terminal_output_renders_latest_visible_snapshot() { + let server = TestServer::start().await.expect("server starts"); + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("protocol connection"); + + let session = create_session(&mut connection, "alpha").await; + let buffer = create_buffer_with_command( + &mut connection, + "burst", + vec![ + "/bin/sh".to_owned(), + "-lc".to_owned(), + "i=1; while [ $i -le 80 ]; do printf 'burst-%02d\\n' \"$i\"; i=$((i+1)); done" + .to_owned(), + ], + ) + .await; + let _ = connection + .request(&ClientMessage::Session(SessionRequest::AddRootTab { + request_id: new_request_id(), + session_id: session.session.id, + title: "burst".to_owned(), + buffer_id: Some(buffer.id), + child_node_id: None, + })) + .await + .expect("add burst tab succeeds"); + + connection + .wait_for_capture_contains(buffer.id, "burst-80", Duration::from_secs(3)) + .await + .expect("rapid output finishes"); + let visible_snapshot = wait_for_visible_snapshot( + &mut connection, + buffer.id, + Duration::from_secs(3), + |snapshot| snapshot.total_lines >= 80 && snapshot.lines.join("\n").contains("burst-80"), + ) + .await; + + let mut client = MuxClient::connect(server.socket_path()) + .await + .expect("client connects"); + let render = render_session(&mut client, "alpha").await; + let latest_rendered_line = visible_snapshot + .lines + .iter() + .rev() + .find(|line| line.starts_with("burst-")) + .expect("latest rendered burst line"); + assert!(render.contains(latest_rendered_line)); + assert!(!render.contains("burst-01")); server.shutdown().await.expect("server shuts down"); } diff --git a/crates/embers-client/tests/presentation.rs b/crates/embers-client/tests/presentation.rs index 29e0077..bb1758e 100644 --- a/crates/embers-client/tests/presentation.rs +++ b/crates/embers-client/tests/presentation.rs @@ -1,11 +1,16 @@ use embers_client::PresentationModel; -use embers_core::{FloatGeometry, Size, SplitDirection}; +use embers_core::{ + ActivityState, BufferId, FloatGeometry, NodeId, PtySize, SessionId, Size, SplitDirection, +}; +use embers_protocol::{ + BufferRecord, BufferRecordKind, BufferRecordState, BufferViewRecord, NodeRecord, NodeRecordKind, +}; use crate::support::{ - FLOATING_BOTTOM_LEAF_ID, FLOATING_ID, FLOATING_TOP_LEAF_ID, FOCUSED_LEAF_ID, LEFT_LEAF_ID, - NESTED_TABS_ID, ROOT_BUFFER_LEAF_ID, ROOT_ONLY_SPLIT_ID, ROOT_SPLIT_LEFT_LEAF_ID, - ROOT_SPLIT_RIGHT_LEAF_ID, ROOT_TABS_ID, SESSION_ID, demo_state, root_buffer_state, - root_split_state, + FLOATING_BOTTOM_LEAF_ID, FLOATING_ID, FLOATING_SPLIT_ID, FLOATING_TOP_LEAF_ID, FOCUSED_LEAF_ID, + LEFT_LEAF_ID, NESTED_TABS_ID, ROOT_BUFFER_LEAF_ID, ROOT_ONLY_SPLIT_ID, ROOT_SPLIT_LEFT_LEAF_ID, + ROOT_SPLIT_RIGHT_LEAF_ID, ROOT_TABS_ID, SESSION_ID, demo_state, floating_focused_state, + root_buffer_state, root_split_state, }; #[test] @@ -125,6 +130,195 @@ fn floating_windows_can_start_on_the_top_row() { assert_eq!(floating.rect.size.height, 7); } +#[test] +fn zoomed_floating_windows_keep_floating_context() { + let mut state = floating_focused_state(); + state + .sessions + .get_mut(&SESSION_ID) + .expect("session exists") + .zoomed_node_id = Some(FLOATING_SPLIT_ID); + + let presentation = PresentationModel::project( + &state, + SESSION_ID, + Size { + width: 40, + height: 14, + }, + ) + .expect("projection succeeds"); + + let floating = presentation + .floating + .iter() + .find(|window| window.floating_id == FLOATING_ID) + .expect("zoomed floating frame exists"); + let floating_leaves = presentation + .leaves + .iter() + .filter(|leaf| leaf.floating_id == Some(FLOATING_ID)) + .collect::>(); + let floating_dividers = presentation + .dividers + .iter() + .filter(|divider| divider.floating_id == Some(FLOATING_ID)) + .collect::>(); + let floating_leaf_ids = floating_leaves + .iter() + .map(|leaf| leaf.node_id) + .collect::>(); + assert_eq!(floating.rect.size.width, 20); + assert_eq!(floating.rect.size.height, 7); + assert_eq!(floating_leaf_ids.len(), 2); + assert!(floating_leaf_ids.contains(&FLOATING_TOP_LEAF_ID)); + assert!(floating_leaf_ids.contains(&FLOATING_BOTTOM_LEAF_ID)); + assert_eq!(presentation.leaves.len(), 2); + assert_eq!(floating_dividers.len(), 1); + assert_eq!(presentation.dividers.len(), 1); + assert_eq!(floating_dividers[0].direction, SplitDirection::Horizontal); +} + +#[test] +fn zoomed_floating_descendants_keep_floating_context() { + let mut state = floating_focused_state(); + state + .sessions + .get_mut(&SESSION_ID) + .expect("session exists") + .zoomed_node_id = Some(FLOATING_TOP_LEAF_ID); + + let presentation = PresentationModel::project( + &state, + SESSION_ID, + Size { + width: 40, + height: 14, + }, + ) + .expect("projection succeeds"); + + assert_eq!(presentation.focused_floating_id(), Some(FLOATING_ID)); + assert_eq!(presentation.floating.len(), 1); + assert_eq!(presentation.floating[0].floating_id, FLOATING_ID); + assert_eq!(presentation.leaves.len(), 1); + assert_eq!(presentation.leaves[0].node_id, FLOATING_TOP_LEAF_ID); + assert_eq!(presentation.leaves[0].floating_id, Some(FLOATING_ID)); +} + +#[test] +fn stale_zoomed_floating_nodes_outside_session_are_ignored() { + let mut state = demo_state(); + let session = state.sessions.get_mut(&SESSION_ID).expect("session exists"); + session.zoomed_node_id = Some(FLOATING_TOP_LEAF_ID); + session.floating_ids.clear(); + + let presentation = PresentationModel::project( + &state, + SESSION_ID, + Size { + width: 40, + height: 14, + }, + ) + .expect("projection succeeds"); + + assert_eq!( + presentation.root_tabs.as_ref().map(|tabs| tabs.node_id), + Some(ROOT_TABS_ID) + ); + assert_eq!( + presentation.focused_leaf().map(|leaf| leaf.node_id), + Some(FOCUSED_LEAF_ID) + ); + assert!(presentation.floating.is_empty()); + assert!( + presentation + .leaves + .iter() + .all(|leaf| leaf.floating_id.is_none()) + ); +} + +#[test] +fn foreign_session_zoom_targets_are_ignored() { + const FOREIGN_SESSION_ID: SessionId = SessionId(99); + const FOREIGN_NODE_ID: NodeId = NodeId(999); + const FOREIGN_BUFFER_ID: BufferId = BufferId(999); + + let mut state = demo_state(); + state + .sessions + .get_mut(&SESSION_ID) + .expect("session exists") + .zoomed_node_id = Some(FOREIGN_NODE_ID); + state.nodes.insert( + FOREIGN_NODE_ID, + NodeRecord { + id: FOREIGN_NODE_ID, + session_id: FOREIGN_SESSION_ID, + // Intentional: point the foreign node at the local root tabs so session checks win. + parent_id: Some(ROOT_TABS_ID), + kind: NodeRecordKind::BufferView, + buffer_view: Some(BufferViewRecord { + buffer_id: FOREIGN_BUFFER_ID, + focused: false, + zoomed: false, + follow_output: true, + last_render_size: PtySize::new(80, 24), + }), + split: None, + tabs: None, + }, + ); + state.buffers.insert( + FOREIGN_BUFFER_ID, + BufferRecord { + id: FOREIGN_BUFFER_ID, + title: "foreign".to_owned(), + command: vec!["/bin/sh".to_owned()], + cwd: Some("/tmp".to_owned()), + kind: BufferRecordKind::Pty, + pid: None, + env: Default::default(), + state: BufferRecordState::Running, + attachment_node_id: Some(FOREIGN_NODE_ID), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 1, + exit_code: None, + }, + ); + + let presentation = PresentationModel::project( + &state, + SESSION_ID, + Size { + width: 40, + height: 14, + }, + ) + .expect("projection succeeds"); + + assert_eq!( + presentation.root_tabs.as_ref().map(|tabs| tabs.node_id), + Some(ROOT_TABS_ID) + ); + assert_eq!( + presentation.focused_leaf().map(|leaf| leaf.node_id), + Some(FOCUSED_LEAF_ID) + ); + assert!( + presentation + .leaves + .iter() + .all(|leaf| leaf.node_id != FOREIGN_NODE_ID) + ); +} + #[test] fn projects_root_buffer_without_tabs_frame() { let state = root_buffer_state(); diff --git a/crates/embers-client/tests/reducer.rs b/crates/embers-client/tests/reducer.rs index 96afccf..5ae7209 100644 --- a/crates/embers-client/tests/reducer.rs +++ b/crates/embers-client/tests/reducer.rs @@ -6,10 +6,10 @@ use embers_core::{ ActivityState, BufferId, FloatGeometry, NodeId, PtySize, RequestId, SessionId, SplitDirection, }; use embers_protocol::{ - BufferDetachedEvent, BufferRecord, BufferRecordState, BufferViewRecord, BuffersResponse, - ClientChangedEvent, ClientMessage, ClientRecord, ClientRequest, ClientResponse, - FloatingChangedEvent, FloatingRecord, FocusChangedEvent, NodeChangedEvent, NodeRecord, - NodeRecordKind, RenderInvalidatedEvent, ServerEvent, ServerResponse, SessionRecord, + BufferDetachedEvent, BufferRecord, BufferRecordKind, BufferRecordState, BufferViewRecord, + BuffersResponse, ClientChangedEvent, ClientMessage, ClientRecord, ClientRequest, + ClientResponse, FloatingChangedEvent, FloatingRecord, FocusChangedEvent, NodeChangedEvent, + NodeRecord, NodeRecordKind, RenderInvalidatedEvent, ServerEvent, ServerResponse, SessionRecord, SessionRequest, SessionSnapshot, SessionSnapshotResponse, SplitRecord, TabRecord, TabsRecord, VisibleSnapshotResponse, }; @@ -20,10 +20,14 @@ fn buffer(id: u64, attachment_node_id: Option, title: &str) -> BufferRecord title: title.to_owned(), command: vec!["/bin/sh".to_owned()], cwd: Some("/tmp".to_owned()), + kind: BufferRecordKind::Pty, pid: None, env: Default::default(), state: BufferRecordState::Running, attachment_node_id: attachment_node_id.map(NodeId), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, pty_size: PtySize::new(80, 24), activity: ActivityState::Idle, last_snapshot_seq: 0, @@ -64,6 +68,7 @@ fn session_snapshot(root_active: u32, nested_active: u32) -> SessionSnapshot { floating_ids: vec![embers_core::FloatingId(90)], focused_leaf_id: Some(NodeId(11)), focused_floating_id: None, + zoomed_node_id: None, }, nodes: vec![ NodeRecord { @@ -543,6 +548,7 @@ async fn reconnect_resync_rebuilds_sessions_and_detached_buffers() { floating_ids: vec![], focused_leaf_id: Some(NodeId(11)), focused_floating_id: None, + zoomed_node_id: None, }], }), ); @@ -579,6 +585,7 @@ async fn reconnect_resync_rebuilds_sessions_and_detached_buffers() { floating_ids: vec![], focused_leaf_id: None, focused_floating_id: None, + zoomed_node_id: None, }, ); client diff --git a/crates/embers-client/tests/renderer.rs b/crates/embers-client/tests/renderer.rs index b25fd3d..b3a8c4e 100644 --- a/crates/embers-client/tests/renderer.rs +++ b/crates/embers-client/tests/renderer.rs @@ -135,6 +135,42 @@ fn renderer_shows_scroll_indicator_and_search_highlights() { assert!(ansi.iter().any(|line| line.contains("\x1b[4m\x1b[7m"))); } +#[test] +fn renderer_uses_snapshot_lines_for_overlays_when_visible_lines_are_empty() { + let mut state = demo_state(); + let view = state.view_state_mut(FOCUSED_LEAF_ID).unwrap(); + view.follow_output = false; + view.scroll_top_line = 0; + view.visible_lines.clear(); + view.search_state = Some(SearchState { + query: "left".to_owned(), + matches: vec![SearchMatch { + line: 0, + start_column: 0, + end_column: 4, + }], + active_match_index: Some(0), + }); + + let presentation = PresentationModel::project( + &state, + SESSION_ID, + Size { + width: 40, + height: 14, + }, + ) + .unwrap(); + let renderer = Renderer; + let grid = renderer.render(&state, &presentation); + let ansi = grid.ansi_lines(); + + assert!( + ansi.iter().any(|line| line.contains("\x1b[4m\x1b[7m")), + "{ansi:?}" + ); +} + #[test] fn renderer_draws_selection_overlay_and_hides_program_cursor_when_selecting() { let mut state = demo_state(); diff --git a/crates/embers-client/tests/script_actions.rs b/crates/embers-client/tests/script_actions.rs index 065271b..b8cf6de 100644 --- a/crates/embers-client/tests/script_actions.rs +++ b/crates/embers-client/tests/script_actions.rs @@ -7,6 +7,9 @@ use embers_client::{ config::{ConfigOrigin, LoadedConfigSource}, }; use embers_core::{BufferId, FloatingId, NodeId, Size, SplitDirection}; +use embers_protocol::{ + BufferHistoryPlacement, BufferHistoryScope, NodeBreakDestination, NodeJoinPlacement, +}; use crate::support::{SESSION_ID, demo_state}; @@ -46,6 +49,17 @@ fn action_helpers_roundtrip_to_typed_actions() { fn select_move_action(ctx) { action.select_move_left() } fn yank_action(ctx) { action.yank_selection() } fn notify_user_action(ctx) { action.notify("info", "hello") } + fn open_history_action(ctx) { + action.open_buffer_history(4, "visible", "floating") + } + fn zoom_action(ctx) { action.zoom_current_node() } + fn unzoom_action(ctx) { action.unzoom_current_session() } + fn toggle_zoom_action(ctx) { action.toggle_zoom_node(7) } + fn swap_nodes_action(ctx) { action.swap_current_node(8) } + fn break_node_action(ctx) { action.break_current_node("tab") } + fn join_buffer_action(ctx) { action.join_buffer_here(9, "tab-after") } + fn move_before_action(ctx) { action.move_current_node_before(10) } + fn move_after_action(ctx) { action.move_node_after(11, 12) } define_action("enter-copy", enter_copy_action); define_action("focus-left", focus_left_action); @@ -67,6 +81,15 @@ fn action_helpers_roundtrip_to_typed_actions() { define_action("select-move", select_move_action); define_action("yank", yank_action); define_action("notify-user", notify_user_action); + define_action("open-history", open_history_action); + define_action("zoom", zoom_action); + define_action("unzoom", unzoom_action); + define_action("toggle-zoom", toggle_zoom_action); + define_action("swap-nodes", swap_nodes_action); + define_action("break-node", break_node_action); + define_action("join-buffer", join_buffer_action); + define_action("move-before", move_before_action); + define_action("move-after", move_after_action); "#, ); let context = demo_context(); @@ -250,6 +273,67 @@ fn action_helpers_roundtrip_to_typed_actions() { message: "hello".to_owned(), }] ); + let ctx = demo_context(); + assert_eq!( + engine + .run_named_action("open-history", ctx.clone()) + .unwrap(), + vec![Action::OpenBufferHistory { + buffer_id: BufferId(4), + scope: BufferHistoryScope::Visible, + placement: BufferHistoryPlacement::Floating, + }] + ); + assert_eq!( + engine.run_named_action("zoom", ctx.clone()).unwrap(), + vec![Action::ZoomNode { node_id: None }] + ); + assert_eq!( + engine.run_named_action("unzoom", ctx.clone()).unwrap(), + vec![Action::UnzoomNode { session_id: None }] + ); + assert_eq!( + engine.run_named_action("toggle-zoom", ctx.clone()).unwrap(), + vec![Action::ToggleZoomNode { + node_id: Some(NodeId(7)), + }] + ); + assert_eq!( + engine.run_named_action("swap-nodes", ctx.clone()).unwrap(), + vec![Action::SwapSiblingNodes { + first_node_id: None, + second_node_id: NodeId(8), + }] + ); + assert_eq!( + engine.run_named_action("break-node", ctx.clone()).unwrap(), + vec![Action::BreakNode { + node_id: None, + destination: NodeBreakDestination::Tab, + }] + ); + assert_eq!( + engine.run_named_action("join-buffer", ctx.clone()).unwrap(), + vec![Action::JoinBufferAtNode { + node_id: None, + buffer_id: BufferId(9), + placement: NodeJoinPlacement::TabAfter, + }] + ); + assert_eq!( + engine.run_named_action("move-before", ctx.clone()).unwrap(), + vec![Action::MoveNodeBefore { + node_id: None, + sibling_node_id: NodeId(10), + }] + ); + assert_eq!( + engine.run_named_action("move-after", ctx).unwrap(), + vec![Action::MoveNodeAfter { + node_id: Some(NodeId(11)), + sibling_node_id: NodeId(12), + }] + ); } #[test] @@ -303,6 +387,215 @@ fn invalid_action_shapes_fail_cleanly() { ); } +#[test] +fn open_buffer_history_rejects_invalid_scope() { + let engine = load_engine( + r#" + fn bad_scope(ctx) { action.open_buffer_history(1, "invalid-scope", "floating") } + define_action("bad-scope", bad_scope); + "#, + ); + + let error = engine + .run_named_action("bad-scope", demo_context()) + .expect_err("invalid scope should fail"); + + assert!(error.to_string().contains("invalid scope")); +} + +#[test] +fn open_buffer_history_rejects_invalid_placement() { + let engine = load_engine( + r#" + fn bad_placement(ctx) { action.open_buffer_history(1, "visible", "invalid-placement") } + define_action("bad-placement", bad_placement); + "#, + ); + + let error = engine + .run_named_action("bad-placement", demo_context()) + .expect_err("invalid placement should fail"); + + assert!(error.to_string().contains("invalid placement")); +} + +#[test] +fn break_node_rejects_invalid_destination() { + let engine = load_engine( + r#" + fn bad_dest(ctx) { action.break_current_node("invalid-dest") } + define_action("bad-dest", bad_dest); + "#, + ); + + let error = engine + .run_named_action("bad-dest", demo_context()) + .expect_err("invalid destination should fail"); + + assert!(error.to_string().contains("invalid destination")); +} + +#[test] +fn join_buffer_rejects_invalid_placement() { + let engine = load_engine( + r#" + fn bad_place(ctx) { action.join_buffer_here(1, "invalid-place") } + define_action("bad-place", bad_place); + "#, + ); + + let error = engine + .run_named_action("bad-place", demo_context()) + .expect_err("invalid placement should fail"); + + assert!(error.to_string().contains("invalid placement")); +} + +#[test] +fn commit_search_and_cancel_search_roundtrip() { + let engine = load_engine( + r#" + fn commit_search_action(ctx) { action.commit_search() } + fn cancel_search_action(ctx) { action.cancel_search() } + define_action("commit-search", commit_search_action); + define_action("cancel-search", cancel_search_action); + "#, + ); + + assert_eq!( + engine + .run_named_action("commit-search", demo_context()) + .unwrap(), + vec![Action::CommitSearch] + ); + assert_eq!( + engine + .run_named_action("cancel-search", demo_context()) + .unwrap(), + vec![Action::CancelSearch] + ); +} + +#[test] +fn toggle_zoom_node_rejects_negative_node_id() { + let engine = load_engine( + r#" + fn bad_zoom(ctx) { action.toggle_zoom_node(-1) } + define_action("bad-zoom", bad_zoom); + "#, + ); + + let error = engine + .run_named_action("bad-zoom", demo_context()) + .expect_err("negative node id should fail"); + + assert!( + error + .to_string() + .contains("node id must be zero or greater") + ); +} + +#[test] +fn swap_current_node_rejects_negative_node_id() { + let engine = load_engine( + r#" + fn bad_swap(ctx) { action.swap_current_node(-5) } + define_action("bad-swap", bad_swap); + "#, + ); + + let error = engine + .run_named_action("bad-swap", demo_context()) + .expect_err("negative node id should fail"); + + assert!( + error + .to_string() + .contains("node id must be zero or greater") + ); +} + +#[test] +fn move_current_node_before_rejects_negative_sibling_id() { + let engine = load_engine( + r#" + fn bad_move(ctx) { action.move_current_node_before(-3) } + define_action("bad-move", bad_move); + "#, + ); + + let error = engine + .run_named_action("bad-move", demo_context()) + .expect_err("negative node id should fail"); + + assert!( + error + .to_string() + .contains("node id must be zero or greater") + ); +} + +#[test] +fn move_node_after_rejects_negative_sibling_id() { + let engine = load_engine( + r#" + fn bad_move(ctx) { action.move_node_after(-1, 5) } + define_action("bad-move", bad_move); + "#, + ); + + let error = engine + .run_named_action("bad-move", demo_context()) + .expect_err("negative node id should fail"); + + assert!( + error + .to_string() + .contains("node id must be zero or greater") + ); +} + +#[test] +fn move_node_after_rejects_negative_target_id() { + let engine = load_engine( + r#" + fn bad_move(ctx) { action.move_node_after(1, -5) } + define_action("bad-move", bad_move); + "#, + ); + + let error = engine + .run_named_action("bad-move", demo_context()) + .expect_err("negative node id should fail"); + + assert!( + error + .to_string() + .contains("node id must be zero or greater") + ); +} + +#[test] +fn open_buffer_history_rejects_negative_buffer_id() { + let engine = load_engine( + r#" + fn bad_history(ctx) { action.open_buffer_history(-1, "visible", "floating") } + define_action("bad-history", bad_history); + "#, + ); + + let error = engine + .run_named_action("bad-history", demo_context()) + .expect_err("negative buffer id should fail"); + + assert!( + error + .to_string() + .contains("buffer id must be zero or greater") + ); +} + #[test] fn query_api_supports_smart_nav_style_scripts() { let engine = load_engine( diff --git a/crates/embers-client/tests/support/mod.rs b/crates/embers-client/tests/support/mod.rs index 444d685..d08d39b 100644 --- a/crates/embers-client/tests/support/mod.rs +++ b/crates/embers-client/tests/support/mod.rs @@ -5,8 +5,9 @@ use embers_core::{ ActivityState, BufferId, FloatGeometry, FloatingId, NodeId, PtySize, SessionId, SplitDirection, }; use embers_protocol::{ - BufferRecord, BufferRecordState, BufferViewRecord, FloatingRecord, NodeRecord, NodeRecordKind, - SessionRecord, SessionSnapshot, SplitRecord, TabRecord, TabsRecord, VisibleSnapshotResponse, + BufferHistoryScope, BufferRecord, BufferRecordKind, BufferRecordState, BufferViewRecord, + FloatingRecord, NodeRecord, NodeRecordKind, SessionRecord, SessionSnapshot, SplitRecord, + TabRecord, TabsRecord, VisibleSnapshotResponse, }; pub const SESSION_ID: SessionId = SessionId(1); @@ -86,6 +87,7 @@ fn demo_snapshot(focused_floating: Option<(FloatingId, NodeId)>) -> SessionSnaps floating_ids: vec![FLOATING_ID], focused_leaf_id, focused_floating_id, + zoomed_node_id: None, }, nodes: vec![ NodeRecord { @@ -212,6 +214,7 @@ fn root_buffer_snapshot() -> SessionSnapshot { floating_ids: Vec::new(), focused_leaf_id: Some(ROOT_BUFFER_LEAF_ID), focused_floating_id: None, + zoomed_node_id: None, }, nodes: vec![buffer_view_node(ROOT_BUFFER_LEAF_ID, None, BufferId(7))], buffers: vec![buffer( @@ -233,6 +236,7 @@ fn root_split_snapshot() -> SessionSnapshot { floating_ids: Vec::new(), focused_leaf_id: Some(ROOT_SPLIT_RIGHT_LEAF_ID), focused_floating_id: None, + zoomed_node_id: None, }, nodes: vec![ NodeRecord { @@ -306,10 +310,14 @@ fn buffer( title: title.to_owned(), command: vec!["/bin/sh".to_owned()], cwd: Some("/tmp".to_owned()), + kind: BufferRecordKind::Pty, pid: None, env: Default::default(), state: BufferRecordState::Running, attachment_node_id, + read_only: false, + helper_source_buffer_id: None, + helper_scope: None::, pty_size: PtySize::new(80, 24), activity, last_snapshot_seq: 0, diff --git a/crates/embers-core/Cargo.toml b/crates/embers-core/Cargo.toml index 26b475d..6c404b6 100644 --- a/crates/embers-core/Cargo.toml +++ b/crates/embers-core/Cargo.toml @@ -5,6 +5,9 @@ license.workspace = true rust-version.workspace = true version.workspace = true +[lib] +doctest = false + [dependencies] serde.workspace = true thiserror.workspace = true diff --git a/crates/embers-protocol/Cargo.toml b/crates/embers-protocol/Cargo.toml index 864d562..6bc377a 100644 --- a/crates/embers-protocol/Cargo.toml +++ b/crates/embers-protocol/Cargo.toml @@ -7,6 +7,9 @@ license.workspace = true rust-version.workspace = true version.workspace = true +[lib] +doctest = false + [dependencies] flatbuffers.workspace = true embers-core = { path = "../embers-core" } diff --git a/crates/embers-protocol/schema/embers.fbs b/crates/embers-protocol/schema/embers.fbs index 19909ec..8293754 100644 --- a/crates/embers-protocol/schema/embers.fbs +++ b/crates/embers-protocol/schema/embers.fbs @@ -27,6 +27,8 @@ enum MessageKind : ubyte { ScrollbackSliceResponse = 32, ClientsResponse = 33, ClientResponse = 34, + BufferLocationResponse = 35, + BufferWithLocationResponse = 36, SessionCreatedEvent = 40, SessionClosedEvent = 41, @@ -73,6 +75,10 @@ enum BufferOp : ubyte { Capture = 5, CaptureVisible = 6, ScrollbackSlice = 7, + GetLocation = 8, + Reveal = 9, + OpenHistory = 10, + Inspect = 11, } enum NodeOp : ubyte { @@ -89,6 +95,14 @@ enum NodeOp : ubyte { CreateTabs = 10, ReplaceNode = 11, WrapInSplit = 12, + Zoom = 13, + Unzoom = 14, + ToggleZoom = 15, + SwapSiblings = 16, + BreakNode = 17, + JoinBufferAtNode = 18, + MoveNodeBefore = 19, + MoveNodeAfter = 20, } enum FloatingOp : ubyte { @@ -125,9 +139,39 @@ enum BufferStateWire : ubyte { Created = 0, Running = 1, Exited = 2, + // Value 3 is intentionally reserved; the codec rejects unknown states explicitly. Interrupted = 4, } +enum BufferKindWire : ubyte { + Pty = 0, + Helper = 1, +} + +enum BufferHistoryScopeWire : ubyte { + Full = 0, + Visible = 1, +} + +enum BufferHistoryPlacementWire : ubyte { + Tab = 0, + Floating = 1, +} + +enum NodeBreakDestinationWire : ubyte { + Tab = 0, + Floating = 1, +} + +enum NodeJoinPlacementWire : ubyte { + Left = 0, + Right = 1, + Up = 2, + Down = 3, + TabBefore = 4, + TabAfter = 5, +} + enum NodeRecordKindWire : ubyte { BufferView = 0, Split = 1, @@ -145,73 +189,81 @@ table PingRequest { } table SessionRequest { - op:SessionOp = Create; - session_id:ulong = 0; - buffer_id:ulong = 0; - child_node_id:ulong = 0; - name:string; - title:string; - force:bool = false; - index:uint = 0; + op:SessionOp = Create (id: 0); + session_id:ulong = 0 (id: 1); + buffer_id:ulong = 0 (id: 2); + child_node_id:ulong = 0 (id: 3); + name:string (id: 4); + title:string (id: 5); + force:bool = false (id: 6); + index:uint = 0 (id: 7); } table BufferRequest { - op:BufferOp = Create; - buffer_id:ulong = 0; - session_id:ulong = 0; - attached_only:bool = false; - detached_only:bool = false; - force:bool = false; - start_line:ulong = 0; - line_count:uint = 0; - title:string; - command:[string]; - cwd:string; - env_keys:[string]; - env_values:[string]; + op:BufferOp = Create (id: 0); + buffer_id:ulong = 0 (id: 1); + session_id:ulong = 0 (id: 2); + attached_only:bool = false (id: 3); + detached_only:bool = false (id: 4); + force:bool = false (id: 5); + start_line:ulong = 0 (id: 6); + line_count:uint = 0 (id: 7); + title:string (id: 8); + command:[string] (id: 9); + cwd:string (id: 10); + env_keys:[string] (id: 11); + env_values:[string] (id: 12); + client_id:ulong = 0 (id: 13); + history_scope:BufferHistoryScopeWire = Full (id: 14); + history_placement:BufferHistoryPlacementWire = Tab (id: 15); } table NodeRequest { - op:NodeOp = GetTree; - session_id:ulong = 0; - node_id:ulong = 0; - leaf_node_id:ulong = 0; - tabs_node_id:ulong = 0; - child_node_id:ulong = 0; - target_leaf_node_id:ulong = 0; - buffer_id:ulong = 0; - new_buffer_id:ulong = 0; - title:string; - index:uint = 0; - active:uint = 0; - direction:SplitDirectionWire = Horizontal; - sizes:[ushort]; - child_node_ids:[ulong]; - titles:[string]; - insert_before:bool = false; + op:NodeOp = GetTree (id: 0); + session_id:ulong = 0 (id: 1); + node_id:ulong = 0 (id: 2); + leaf_node_id:ulong = 0 (id: 3); + tabs_node_id:ulong = 0 (id: 4); + child_node_id:ulong = 0 (id: 5); + target_leaf_node_id:ulong = 0 (id: 6); + buffer_id:ulong = 0 (id: 7); + new_buffer_id:ulong = 0 (id: 8); + title:string (id: 9); + index:uint = 0 (id: 10); + active:uint = 0 (id: 11); + direction:SplitDirectionWire = Horizontal (id: 12); + sizes:[ushort] (id: 13); + child_node_ids:[ulong] (id: 14); + titles:[string] (id: 15); + insert_before:bool = false (id: 16); + break_destination:NodeBreakDestinationWire = Tab (id: 17); + join_placement:NodeJoinPlacementWire = Left (id: 18); + first_node_id:ulong = 0 (id: 19); + second_node_id:ulong = 0 (id: 20); + sibling_node_id:ulong = 0 (id: 21); } table FloatingRequest { - op:FloatingOp = Create; - floating_id:ulong = 0; - session_id:ulong = 0; - root_node_id:ulong = 0; - buffer_id:ulong = 0; - title:string; - x:ushort = 0; - y:ushort = 0; - width:ushort = 0; - height:ushort = 0; - focus:bool = true; - close_on_empty:bool = true; + op:FloatingOp = Create (id: 0); + floating_id:ulong = 0 (id: 1); + session_id:ulong = 0 (id: 2); + root_node_id:ulong = 0 (id: 3); + buffer_id:ulong = 0 (id: 4); + title:string (id: 5); + x:ushort = 0 (id: 6); + y:ushort = 0 (id: 7); + width:ushort = 0 (id: 8); + height:ushort = 0 (id: 9); + focus:bool = true (id: 10); + close_on_empty:bool = true (id: 11); } table InputRequest { - op:InputOp = Send; - buffer_id:ulong = 0; - bytes:[ubyte]; - cols:ushort = 0; - rows:ushort = 0; + op:InputOp = Send (id: 0); + buffer_id:ulong = 0 (id: 1); + bytes:[ubyte] (id: 2); + cols:ushort = 0 (id: 3); + rows:ushort = 0 (id: 4); } table SubscribeRequest { @@ -240,31 +292,37 @@ table ErrorResponse { } table SessionRecord { - id:ulong; - name:string; - root_node_id:ulong; - floating_ids:[ulong]; - focused_leaf_id:ulong = 0; - focused_floating_id:ulong = 0; + id:ulong (id: 0); + name:string (id: 1); + root_node_id:ulong (id: 2); + floating_ids:[ulong] (id: 3); + focused_leaf_id:ulong = 0 (id: 4); + focused_floating_id:ulong = 0 (id: 5); + zoomed_node_id:ulong = 0 (id: 6); } table BufferRecord { - id:ulong; - title:string; - command:[string]; - cwd:string; - state:BufferStateWire = Created; - pid:uint = 0; - has_pid:bool = false; - attachment_node_id:ulong = 0; - pty_cols:ushort = 0; - pty_rows:ushort = 0; - activity:ActivityStateWire = Idle; - last_snapshot_seq:ulong = 0; - exit_code:int = 0; - has_exit_code:bool = false; - env_keys:[string]; - env_values:[string]; + id:ulong (id: 0); + title:string (id: 1); + command:[string] (id: 2); + cwd:string (id: 3); + state:BufferStateWire = Created (id: 4); + pid:uint = 0 (id: 5); + has_pid:bool = false (id: 6); + attachment_node_id:ulong = 0 (id: 7); + pty_cols:ushort = 0 (id: 8); + pty_rows:ushort = 0 (id: 9); + activity:ActivityStateWire = Idle (id: 10); + last_snapshot_seq:ulong = 0 (id: 11); + exit_code:int = 0 (id: 12); + has_exit_code:bool = false (id: 13); + env_keys:[string] (id: 14); + env_values:[string] (id: 15); + kind:BufferKindWire = Pty (id: 16); + read_only:bool = false (id: 17); + helper_source_buffer_id:ulong = 0 (id: 18); + helper_scope:BufferHistoryScopeWire = Full (id: 19); + has_helper_scope:bool = false (id: 20); } table BufferViewRecord { @@ -366,6 +424,23 @@ table ClientResponse { client:ClientRecord; } +table BufferLocation { + buffer_id:ulong (id: 0); + session_id:ulong = 0 (id: 1); + node_id:ulong = 0 (id: 2); + floating_id:ulong = 0 (id: 3); +} + +table BufferLocationResponse { + location:BufferLocation (id: 0); +} + +table BufferWithLocationResponse { + buffer:BufferRecord (id: 0); + location:BufferLocation (id: 1); + at_root_tab:bool = false (id: 2); +} + table CursorState { row:ushort = 0; col:ushort = 0; @@ -452,44 +527,46 @@ table ClientChangedEvent { } table Envelope { - request_id:ulong = 0; - kind:MessageKind = None; - ping_request:PingRequest; - session_request:SessionRequest; - buffer_request:BufferRequest; - node_request:NodeRequest; - floating_request:FloatingRequest; - input_request:InputRequest; - subscribe_request:SubscribeRequest; - unsubscribe_request:UnsubscribeRequest; - - ping_response:PingResponse; - ok_response:OkResponse; - error_response:ErrorResponse; - sessions_response:SessionsResponse; - session_snapshot_response:SessionSnapshotResponse; - buffers_response:BuffersResponse; - buffer_response:BufferResponse; - floating_list_response:FloatingListResponse; - floating_response:FloatingResponse; - subscription_ack_response:SubscriptionAckResponse; - snapshot_response:SnapshotResponse; - visible_snapshot_response:VisibleSnapshotResponse; - scrollback_slice_response:ScrollbackSliceResponse; - - session_created_event:SessionCreatedEvent; - session_closed_event:SessionClosedEvent; - buffer_created_event:BufferCreatedEvent; - buffer_detached_event:BufferDetachedEvent; - node_changed_event:NodeChangedEvent; - floating_changed_event:FloatingChangedEvent; - focus_changed_event:FocusChangedEvent; - render_invalidated_event:RenderInvalidatedEvent; - session_renamed_event:SessionRenamedEvent; - client_changed_event:ClientChangedEvent; - client_request:ClientRequest; - clients_response:ClientsResponse; - client_response:ClientResponse; + request_id:ulong = 0 (id: 0); + kind:MessageKind = None (id: 1); + ping_request:PingRequest (id: 2); + session_request:SessionRequest (id: 3); + buffer_request:BufferRequest (id: 4); + node_request:NodeRequest (id: 5); + floating_request:FloatingRequest (id: 6); + input_request:InputRequest (id: 7); + subscribe_request:SubscribeRequest (id: 8); + unsubscribe_request:UnsubscribeRequest (id: 9); + client_request:ClientRequest (id: 10); + + ping_response:PingResponse (id: 11); + ok_response:OkResponse (id: 12); + error_response:ErrorResponse (id: 13); + sessions_response:SessionsResponse (id: 14); + session_snapshot_response:SessionSnapshotResponse (id: 15); + buffers_response:BuffersResponse (id: 16); + buffer_response:BufferResponse (id: 17); + floating_list_response:FloatingListResponse (id: 18); + floating_response:FloatingResponse (id: 19); + subscription_ack_response:SubscriptionAckResponse (id: 20); + snapshot_response:SnapshotResponse (id: 21); + visible_snapshot_response:VisibleSnapshotResponse (id: 22); + scrollback_slice_response:ScrollbackSliceResponse (id: 23); + clients_response:ClientsResponse (id: 24); + client_response:ClientResponse (id: 25); + + session_created_event:SessionCreatedEvent (id: 26); + session_closed_event:SessionClosedEvent (id: 27); + buffer_created_event:BufferCreatedEvent (id: 28); + buffer_detached_event:BufferDetachedEvent (id: 29); + node_changed_event:NodeChangedEvent (id: 30); + floating_changed_event:FloatingChangedEvent (id: 31); + focus_changed_event:FocusChangedEvent (id: 32); + render_invalidated_event:RenderInvalidatedEvent (id: 33); + session_renamed_event:SessionRenamedEvent (id: 34); + client_changed_event:ClientChangedEvent (id: 35); + buffer_location_response:BufferLocationResponse (id: 36); + buffer_with_location_response:BufferWithLocationResponse (id: 37); } root_type Envelope; diff --git a/crates/embers-protocol/src/client.rs b/crates/embers-protocol/src/client.rs index 81ad56b..27a2cee 100644 --- a/crates/embers-protocol/src/client.rs +++ b/crates/embers-protocol/src/client.rs @@ -2,33 +2,63 @@ use std::path::Path; use embers_core::RequestId; use tokio::net::UnixStream; +use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf}; +use tokio::sync::mpsc; +use tokio::task::AbortHandle; use crate::codec::{ProtocolError, decode_server_envelope, encode_client_message}; use crate::framing::{FrameType, RawFrame, read_frame, write_frame}; use crate::types::{ClientMessage, ServerEnvelope, ServerResponse}; +type ReaderItem = Result, ProtocolError>; + #[derive(Debug)] pub struct ProtocolClient { - stream: UnixStream, + writer: OwnedWriteHalf, + reader_rx: mpsc::Receiver, + reader_reached_eof: bool, + reader_abort_handle: AbortHandle, } impl ProtocolClient { + const READER_CHANNEL_CAPACITY: usize = 64; + pub async fn connect(path: impl AsRef) -> Result { let stream = UnixStream::connect(path).await?; - Ok(Self { stream }) + Ok(Self::from_stream(stream)) } - pub async fn send(&mut self, message: &ClientMessage) -> Result<(), ProtocolError> { - let payload = encode_client_message(message)?; - let frame = RawFrame::new(FrameType::Request, message.request_id(), payload); - write_frame(&mut self.stream, &frame).await + fn from_stream(stream: UnixStream) -> Self { + let (reader, writer) = stream.into_split(); + let (reader_tx, reader_rx) = mpsc::channel(Self::READER_CHANNEL_CAPACITY); + let reader_task = tokio::spawn(async move { + Self::run_reader(reader, reader_tx).await; + }); + let reader_abort_handle = reader_task.abort_handle(); + + Self { + writer, + reader_rx, + reader_reached_eof: false, + reader_abort_handle, + } } - pub async fn recv(&mut self) -> Result, ProtocolError> { - let Some(frame) = read_frame(&mut self.stream).await? else { - return Ok(None); - }; + async fn run_reader(mut reader: OwnedReadHalf, reader_tx: mpsc::Sender) { + loop { + let next = match read_frame(&mut reader).await { + Ok(Some(frame)) => Self::decode_frame(frame).map(Some), + Ok(None) => Ok(None), + Err(error) => Err(error), + }; + let terminal = !matches!(next, Ok(Some(_))); + if reader_tx.send(next).await.is_err() || terminal { + break; + } + } + } + fn decode_frame(frame: RawFrame) -> Result { let envelope = decode_server_envelope(&frame.payload)?; match (frame.frame_type, envelope) { @@ -40,7 +70,7 @@ impl ProtocolClient { actual: response_id, }); } - Ok(Some(ServerEnvelope::Response(response))) + Ok(ServerEnvelope::Response(response)) } (FrameType::Event, ServerEnvelope::Event(event)) => { if frame.request_id != RequestId(0) { @@ -49,7 +79,7 @@ impl ProtocolClient { actual: frame.request_id, }); } - Ok(Some(ServerEnvelope::Event(event))) + Ok(ServerEnvelope::Event(event)) } (FrameType::Response, ServerEnvelope::Event(_)) => { Err(ProtocolError::UnexpectedFrameKind { @@ -67,6 +97,24 @@ impl ProtocolClient { } } + pub async fn send(&mut self, message: &ClientMessage) -> Result<(), ProtocolError> { + let payload = encode_client_message(message)?; + let frame = RawFrame::new(FrameType::Request, message.request_id(), payload); + write_frame(&mut self.writer, &frame).await + } + + pub async fn recv(&mut self) -> Result, ProtocolError> { + match self.reader_rx.recv().await { + Some(Ok(None)) => { + self.reader_reached_eof = true; + Ok(None) + } + Some(result) => result, + None if self.reader_reached_eof => Ok(None), + None => Err(ProtocolError::ReaderTaskExited), + } + } + pub async fn request( &mut self, message: &ClientMessage, @@ -98,22 +146,46 @@ impl ProtocolClient { } } +impl Drop for ProtocolClient { + fn drop(&mut self) { + self.reader_abort_handle.abort(); + } +} + +#[cfg(test)] +impl ProtocolClient { + fn abort_reader_task(&self) { + self.reader_abort_handle.abort(); + } + + fn drain_recv_buffer(&mut self) { + while let Ok(item) = self.reader_rx.try_recv() { + if matches!(item, Ok(None)) { + self.reader_reached_eof = true; + } + } + } +} + #[cfg(test)] mod tests { use super::ProtocolClient; - use embers_core::{ErrorCode, RequestId, WireError}; + use embers_core::{ErrorCode, RequestId, SessionId, WireError}; + use tokio::io::AsyncWriteExt; use tokio::net::UnixStream; + use tokio::time::{Duration, timeout}; - use crate::codec::encode_server_envelope; + use crate::codec::{ProtocolError, encode_server_envelope}; use crate::framing::{FrameType, RawFrame, read_frame, write_frame}; - use crate::types::{ClientMessage, ErrorResponse, PingRequest, ServerEnvelope, ServerResponse}; + use crate::types::{ + ClientMessage, ErrorResponse, PingRequest, ServerEnvelope, ServerEvent, ServerResponse, + SessionClosedEvent, + }; #[tokio::test] async fn request_accepts_unscoped_error_response() { let (mut server, client_stream) = UnixStream::pair().expect("create unix stream pair"); - let mut client = ProtocolClient { - stream: client_stream, - }; + let mut client = ProtocolClient::from_stream(client_stream); let request = ClientMessage::Ping(PingRequest { request_id: RequestId(7), @@ -153,4 +225,62 @@ mod tests { server_task.await.expect("server task joins"); } + + #[tokio::test] + async fn recv_timeout_does_not_cancel_in_progress_frame_read() { + let (mut server, client_stream) = UnixStream::pair().expect("create unix stream pair"); + let mut client = ProtocolClient::from_stream(client_stream); + + let payload = encode_server_envelope(&ServerEnvelope::Event(ServerEvent::SessionClosed( + SessionClosedEvent { + session_id: SessionId(9), + }, + ))) + .expect("encode event"); + let mut frame_bytes = Vec::with_capacity(13 + payload.len()); + frame_bytes.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + frame_bytes.push(FrameType::Event as u8); + frame_bytes.extend_from_slice(&0_u64.to_le_bytes()); + frame_bytes.extend_from_slice(&payload); + + server + .write_all(&frame_bytes[..5]) + .await + .expect("write partial frame"); + + let timed_out = timeout(Duration::from_millis(20), client.recv()).await; + assert!(timed_out.is_err(), "partial frame should keep recv pending"); + + server + .write_all(&frame_bytes[5..]) + .await + .expect("write remainder"); + + let envelope = timeout(Duration::from_secs(1), client.recv()) + .await + .expect("recv finishes after remainder arrives") + .expect("recv succeeds") + .expect("connection remains open"); + assert!(matches!( + envelope, + ServerEnvelope::Event(ServerEvent::SessionClosed(SessionClosedEvent { + session_id + })) if session_id == SessionId(9) + )); + } + + #[tokio::test] + async fn recv_reports_reader_task_exit_when_channel_closes_without_eof() { + let (_server, client_stream) = UnixStream::pair().expect("create unix stream pair"); + let mut client = ProtocolClient::from_stream(client_stream); + + client.abort_reader_task(); + client.drain_recv_buffer(); + + let error = timeout(Duration::from_secs(1), client.recv()) + .await + .expect("recv returns after reader abort") + .expect_err("closed reader channel should error"); + assert!(matches!(error, ProtocolError::ReaderTaskExited)); + } } diff --git a/crates/embers-protocol/src/codec.rs b/crates/embers-protocol/src/codec.rs index a4fcf97..37fa943 100644 --- a/crates/embers-protocol/src/codec.rs +++ b/crates/embers-protocol/src/codec.rs @@ -1,3 +1,5 @@ +use std::num::NonZeroU64; + use embers_core::{ ActivityState, BufferId, CursorShape, CursorState, ErrorCode, FloatGeometry, FloatingId, NodeId, PtySize, RequestId, SessionId, SplitDirection, WireError, @@ -35,12 +37,353 @@ pub enum ProtocolError { expected: RequestId, actual: RequestId, }, + #[error("reader task exited unexpectedly")] + ReaderTaskExited, } fn required(value: Option, field: &'static str) -> Result { value.ok_or(ProtocolError::InvalidMessage(field)) } +fn decode_required_buffer_id( + buffer_id: u64, + field: &'static str, +) -> Result { + if buffer_id == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field} must be non-zero" + ))); + } + Ok(BufferId(buffer_id)) +} + +fn decode_required_node_id(node_id: u64, field: &'static str) -> Result { + if node_id == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field} must be non-zero" + ))); + } + Ok(NodeId(node_id)) +} + +fn decode_required_session_id( + session_id: u64, + field: &'static str, +) -> Result { + if session_id == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field} must be non-zero" + ))); + } + Ok(SessionId(session_id)) +} + +fn decode_required_floating_id( + floating_id: u64, + field: &'static str, +) -> Result { + if floating_id == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field} must be non-zero" + ))); + } + Ok(FloatingId(floating_id)) +} + +fn validate_nonzero_id(value: u64, field: &'static str) -> Result<(), ProtocolError> { + if value == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field} must be non-zero" + ))); + } + Ok(()) +} + +fn validate_required_buffer_id( + buffer_id: BufferId, + field: &'static str, +) -> Result<(), ProtocolError> { + validate_nonzero_id(buffer_id.0, field) +} + +fn validate_required_node_id(node_id: NodeId, field: &'static str) -> Result<(), ProtocolError> { + validate_nonzero_id(node_id.0, field) +} + +fn validate_required_session_id( + session_id: SessionId, + field: &'static str, +) -> Result<(), ProtocolError> { + validate_nonzero_id(session_id.0, field) +} + +fn validate_optional_session_id( + session_id: Option, + field: &'static str, +) -> Result<(), ProtocolError> { + if let Some(session_id) = session_id { + validate_required_session_id(session_id, field)?; + } + Ok(()) +} + +fn validate_required_floating_id( + floating_id: FloatingId, + field: &'static str, +) -> Result<(), ProtocolError> { + validate_nonzero_id(floating_id.0, field) +} + +fn validate_optional_buffer_id( + buffer_id: Option, + field: &'static str, +) -> Result<(), ProtocolError> { + if let Some(buffer_id) = buffer_id { + validate_required_buffer_id(buffer_id, field)?; + } + Ok(()) +} + +fn validate_optional_node_id( + node_id: Option, + field: &'static str, +) -> Result<(), ProtocolError> { + if let Some(node_id) = node_id { + validate_required_node_id(node_id, field)?; + } + Ok(()) +} + +fn validate_optional_floating_id( + floating_id: Option, + field: &'static str, +) -> Result<(), ProtocolError> { + if let Some(floating_id) = floating_id { + validate_required_floating_id(floating_id, field)?; + } + Ok(()) +} + +fn validate_required_session_ids( + session_ids: &[SessionId], + field: &'static str, +) -> Result<(), ProtocolError> { + for session_id in session_ids { + validate_required_session_id(*session_id, field)?; + } + Ok(()) +} + +fn validate_required_floating_ids( + floating_ids: &[FloatingId], + field: &'static str, +) -> Result<(), ProtocolError> { + for floating_id in floating_ids { + validate_required_floating_id(*floating_id, field)?; + } + Ok(()) +} + +fn validate_required_node_ids( + node_ids: &[NodeId], + field: &'static str, +) -> Result<(), ProtocolError> { + for node_id in node_ids { + validate_required_node_id(*node_id, field)?; + } + Ok(()) +} + +fn validate_session_request(req: &SessionRequest) -> Result<(), ProtocolError> { + match req { + SessionRequest::Create { .. } | SessionRequest::List { .. } => Ok(()), + SessionRequest::AddRootTab { + session_id, + buffer_id, + child_node_id, + .. + } => { + validate_required_session_id(*session_id, "session_request.session_id")?; + validate_optional_buffer_id(*buffer_id, "session_request.buffer_id")?; + validate_optional_node_id(*child_node_id, "session_request.child_node_id") + } + SessionRequest::Get { session_id, .. } + | SessionRequest::Close { session_id, .. } + | SessionRequest::Rename { session_id, .. } + | SessionRequest::SelectRootTab { session_id, .. } + | SessionRequest::RenameRootTab { session_id, .. } + | SessionRequest::CloseRootTab { session_id, .. } => { + validate_required_session_id(*session_id, "session_request.session_id") + } + } +} + +fn validate_buffer_request(req: &BufferRequest) -> Result<(), ProtocolError> { + match req { + BufferRequest::Create { .. } => Ok(()), + BufferRequest::List { session_id, .. } => { + validate_optional_session_id(*session_id, "buffer_request.session_id") + } + BufferRequest::Get { buffer_id, .. } + | BufferRequest::Inspect { buffer_id, .. } + | BufferRequest::Detach { buffer_id, .. } + | BufferRequest::Kill { buffer_id, .. } + | BufferRequest::Capture { buffer_id, .. } + | BufferRequest::CaptureVisible { buffer_id, .. } + | BufferRequest::ScrollbackSlice { buffer_id, .. } + | BufferRequest::GetLocation { buffer_id, .. } + | BufferRequest::Reveal { buffer_id, .. } + | BufferRequest::OpenHistory { buffer_id, .. } => { + validate_required_buffer_id(*buffer_id, "buffer_request.buffer_id") + } + } +} + +fn validate_node_request(req: &NodeRequest) -> Result<(), ProtocolError> { + match req { + NodeRequest::GetTree { session_id, .. } | NodeRequest::Unzoom { session_id, .. } => { + validate_required_session_id(*session_id, "node_request.session_id") + } + NodeRequest::Split { + leaf_node_id, + new_buffer_id, + .. + } => { + validate_required_node_id(*leaf_node_id, "node_request.leaf_node_id")?; + validate_required_buffer_id(*new_buffer_id, "node_request.new_buffer_id") + } + NodeRequest::CreateSplit { + session_id, + child_node_ids, + .. + } + | NodeRequest::CreateTabs { + session_id, + child_node_ids, + .. + } => { + validate_required_session_id(*session_id, "node_request.session_id")?; + validate_required_node_ids(child_node_ids, "node_request.child_node_ids") + } + NodeRequest::ReplaceNode { + node_id, + child_node_id, + .. + } + | NodeRequest::WrapInSplit { + node_id, + child_node_id, + .. + } => { + validate_required_node_id(*node_id, "node_request.node_id")?; + validate_required_node_id(*child_node_id, "node_request.child_node_id") + } + NodeRequest::WrapInTabs { node_id, .. } + | NodeRequest::Close { node_id, .. } + | NodeRequest::Resize { node_id, .. } + | NodeRequest::Zoom { node_id, .. } + | NodeRequest::ToggleZoom { node_id, .. } + | NodeRequest::BreakNode { node_id, .. } => { + validate_required_node_id(*node_id, "node_request.node_id") + } + NodeRequest::AddTab { + tabs_node_id, + buffer_id, + child_node_id, + .. + } => { + validate_required_node_id(*tabs_node_id, "node_request.tabs_node_id")?; + validate_optional_buffer_id(*buffer_id, "node_request.buffer_id")?; + validate_optional_node_id(*child_node_id, "node_request.child_node_id") + } + NodeRequest::SelectTab { tabs_node_id, .. } => { + validate_required_node_id(*tabs_node_id, "node_request.tabs_node_id") + } + NodeRequest::Focus { + session_id, + node_id, + .. + } => { + validate_required_session_id(*session_id, "node_request.session_id")?; + validate_required_node_id(*node_id, "node_request.node_id") + } + NodeRequest::MoveBufferToNode { + buffer_id, + target_leaf_node_id, + .. + } => { + validate_required_buffer_id(*buffer_id, "node_request.buffer_id")?; + validate_required_node_id(*target_leaf_node_id, "node_request.target_leaf_node_id") + } + NodeRequest::SwapSiblings { + first_node_id, + second_node_id, + .. + } => { + validate_required_node_id(*first_node_id, "node_request.first_node_id")?; + validate_required_node_id(*second_node_id, "node_request.second_node_id") + } + NodeRequest::JoinBufferAtNode { + node_id, buffer_id, .. + } => { + validate_required_node_id(*node_id, "node_request.node_id")?; + validate_required_buffer_id(*buffer_id, "node_request.buffer_id") + } + NodeRequest::MoveNodeBefore { + node_id, + sibling_node_id, + .. + } + | NodeRequest::MoveNodeAfter { + node_id, + sibling_node_id, + .. + } => { + validate_required_node_id(*node_id, "node_request.node_id")?; + validate_required_node_id(*sibling_node_id, "node_request.sibling_node_id") + } + } +} + +fn validate_client_request(req: &ClientRequest) -> Result<(), ProtocolError> { + match req { + ClientRequest::List { .. } | ClientRequest::Get { .. } | ClientRequest::Detach { .. } => { + Ok(()) + } + ClientRequest::Switch { session_id, .. } => { + validate_required_session_id(*session_id, "client_request.session_id") + } + } +} + +fn validate_floating_request(req: &FloatingRequest) -> Result<(), ProtocolError> { + match req { + FloatingRequest::Create { + session_id, + root_node_id, + buffer_id, + .. + } => { + validate_required_session_id(*session_id, "floating_request.session_id")?; + validate_optional_node_id(*root_node_id, "floating_request.root_node_id")?; + validate_optional_buffer_id(*buffer_id, "floating_request.buffer_id") + } + FloatingRequest::Close { floating_id, .. } + | FloatingRequest::Move { floating_id, .. } + | FloatingRequest::Focus { floating_id, .. } => { + validate_required_floating_id(*floating_id, "floating_request.floating_id") + } + } +} + +fn validate_input_request(req: &InputRequest) -> Result<(), ProtocolError> { + match req { + InputRequest::Send { buffer_id, .. } | InputRequest::Resize { buffer_id, .. } => { + validate_required_buffer_id(*buffer_id, "input_request.buffer_id") + } + } +} + fn create_string_vector<'a>( builder: &mut FlatBufferBuilder<'a>, values: &[String], @@ -96,6 +439,92 @@ fn encode_cursor_state<'a>( ) } +fn encode_buffer_history_scope(scope: BufferHistoryScope) -> fb::BufferHistoryScopeWire { + match scope { + BufferHistoryScope::Full => fb::BufferHistoryScopeWire::Full, + BufferHistoryScope::Visible => fb::BufferHistoryScopeWire::Visible, + } +} + +fn decode_buffer_history_scope( + scope: fb::BufferHistoryScopeWire, +) -> Result { + match scope { + fb::BufferHistoryScopeWire::Full => Ok(BufferHistoryScope::Full), + fb::BufferHistoryScopeWire::Visible => Ok(BufferHistoryScope::Visible), + _ => Err(ProtocolError::InvalidMessage( + "unknown buffer history scope", + )), + } +} + +fn encode_buffer_history_placement( + placement: BufferHistoryPlacement, +) -> fb::BufferHistoryPlacementWire { + match placement { + BufferHistoryPlacement::Tab => fb::BufferHistoryPlacementWire::Tab, + BufferHistoryPlacement::Floating => fb::BufferHistoryPlacementWire::Floating, + } +} + +fn decode_buffer_history_placement( + placement: fb::BufferHistoryPlacementWire, +) -> Result { + match placement { + fb::BufferHistoryPlacementWire::Tab => Ok(BufferHistoryPlacement::Tab), + fb::BufferHistoryPlacementWire::Floating => Ok(BufferHistoryPlacement::Floating), + _ => Err(ProtocolError::InvalidMessage( + "unknown buffer history placement", + )), + } +} + +fn encode_node_break_destination( + destination: NodeBreakDestination, +) -> fb::NodeBreakDestinationWire { + match destination { + NodeBreakDestination::Tab => fb::NodeBreakDestinationWire::Tab, + NodeBreakDestination::Floating => fb::NodeBreakDestinationWire::Floating, + } +} + +fn decode_node_break_destination( + destination: fb::NodeBreakDestinationWire, +) -> Result { + match destination { + fb::NodeBreakDestinationWire::Tab => Ok(NodeBreakDestination::Tab), + fb::NodeBreakDestinationWire::Floating => Ok(NodeBreakDestination::Floating), + _ => Err(ProtocolError::InvalidMessage( + "unknown node break destination", + )), + } +} + +fn encode_node_join_placement(placement: NodeJoinPlacement) -> fb::NodeJoinPlacementWire { + match placement { + NodeJoinPlacement::Left => fb::NodeJoinPlacementWire::Left, + NodeJoinPlacement::Right => fb::NodeJoinPlacementWire::Right, + NodeJoinPlacement::Up => fb::NodeJoinPlacementWire::Up, + NodeJoinPlacement::Down => fb::NodeJoinPlacementWire::Down, + NodeJoinPlacement::TabBefore => fb::NodeJoinPlacementWire::TabBefore, + NodeJoinPlacement::TabAfter => fb::NodeJoinPlacementWire::TabAfter, + } +} + +fn decode_node_join_placement( + placement: fb::NodeJoinPlacementWire, +) -> Result { + match placement { + fb::NodeJoinPlacementWire::Left => Ok(NodeJoinPlacement::Left), + fb::NodeJoinPlacementWire::Right => Ok(NodeJoinPlacement::Right), + fb::NodeJoinPlacementWire::Up => Ok(NodeJoinPlacement::Up), + fb::NodeJoinPlacementWire::Down => Ok(NodeJoinPlacement::Down), + fb::NodeJoinPlacementWire::TabBefore => Ok(NodeJoinPlacement::TabBefore), + fb::NodeJoinPlacementWire::TabAfter => Ok(NodeJoinPlacement::TabAfter), + _ => Err(ProtocolError::InvalidMessage("unknown node join placement")), + } +} + fn decode_cursor_state(cursor: fb::CursorState<'_>) -> Result { let shape = match cursor.shape() { fb::CursorShapeWire::Block => CursorShape::Block, @@ -119,14 +548,14 @@ pub fn encode_client_message(message: &ClientMessage) -> Result, Protoco let envelope = match message { ClientMessage::Ping(req) => encode_ping_request(&mut builder, req), - ClientMessage::Session(req) => encode_session_request(&mut builder, req), - ClientMessage::Buffer(req) => encode_buffer_request(&mut builder, req), - ClientMessage::Node(req) => encode_node_request(&mut builder, req), - ClientMessage::Floating(req) => encode_floating_request(&mut builder, req), - ClientMessage::Input(req) => encode_input_request(&mut builder, req), + ClientMessage::Session(req) => encode_session_request(&mut builder, req)?, + ClientMessage::Buffer(req) => encode_buffer_request(&mut builder, req)?, + ClientMessage::Node(req) => encode_node_request(&mut builder, req)?, + ClientMessage::Floating(req) => encode_floating_request(&mut builder, req)?, + ClientMessage::Input(req) => encode_input_request(&mut builder, req)?, ClientMessage::Subscribe(req) => encode_subscribe_request(&mut builder, req), ClientMessage::Unsubscribe(req) => encode_unsubscribe_request(&mut builder, req), - ClientMessage::Client(req) => encode_client_request(&mut builder, req), + ClientMessage::Client(req) => encode_client_request(&mut builder, req)?, }; builder.finish(envelope, Some("EMBR")); @@ -158,7 +587,8 @@ fn encode_ping_request<'a>( fn encode_client_request<'a>( builder: &mut FlatBufferBuilder<'a>, req: &ClientRequest, -) -> flatbuffers::WIPOffset> { +) -> Result>, ProtocolError> { + validate_client_request(req)?; let (op, client_id, session_id) = match req { ClientRequest::List { .. } => (fb::ClientOp::List, 0, 0), ClientRequest::Get { client_id, .. } => ( @@ -191,7 +621,7 @@ fn encode_client_request<'a>( }, ); - fb::Envelope::create( + Ok(fb::Envelope::create( builder, &fb::EnvelopeArgs { request_id: req.request_id().into(), @@ -199,13 +629,14 @@ fn encode_client_request<'a>( client_request: Some(client_req), ..Default::default() }, - ) + )) } fn encode_session_request<'a>( builder: &mut FlatBufferBuilder<'a>, req: &SessionRequest, -) -> flatbuffers::WIPOffset> { +) -> Result>, ProtocolError> { + validate_session_request(req)?; let (op, session_id, buffer_id, child_node_id, name_str, title_str, force, index) = match req { SessionRequest::Create { name, .. } => ( fb::SessionOp::Create, @@ -325,7 +756,7 @@ fn encode_session_request<'a>( }, ); - fb::Envelope::create( + Ok(fb::Envelope::create( builder, &fb::EnvelopeArgs { request_id: req.request_id().into(), @@ -333,22 +764,26 @@ fn encode_session_request<'a>( session_request: Some(session_req), ..Default::default() }, - ) + )) } fn encode_buffer_request<'a>( builder: &mut FlatBufferBuilder<'a>, req: &BufferRequest, -) -> flatbuffers::WIPOffset> { +) -> Result>, ProtocolError> { + validate_buffer_request(req)?; let ( op, buffer_id, session_id, + client_id, attached_only, detached_only, force, start_line, line_count, + history_scope, + history_placement, title_str, command_vec, cwd_str, @@ -364,11 +799,14 @@ fn encode_buffer_request<'a>( fb::BufferOp::Create, 0, 0, + 0, false, false, false, 0, 0, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, title.as_deref(), Some(command), cwd.as_deref(), @@ -383,11 +821,14 @@ fn encode_buffer_request<'a>( fb::BufferOp::List, 0, session_id.map(|s| s.into()).unwrap_or(0), + 0, *attached_only, *detached_only, false, 0, 0, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, None, None, None, @@ -397,11 +838,31 @@ fn encode_buffer_request<'a>( fb::BufferOp::Get, (*buffer_id).into(), 0, + 0, + false, + false, + false, + 0, + 0, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, + None, + None, + None, + None, + ), + BufferRequest::Inspect { buffer_id, .. } => ( + fb::BufferOp::Inspect, + (*buffer_id).into(), + 0, + 0, false, false, false, 0, 0, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, None, None, None, @@ -411,11 +872,14 @@ fn encode_buffer_request<'a>( fb::BufferOp::Detach, (*buffer_id).into(), 0, + 0, false, false, false, 0, 0, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, None, None, None, @@ -427,11 +891,14 @@ fn encode_buffer_request<'a>( fb::BufferOp::Kill, (*buffer_id).into(), 0, + 0, false, false, *force, 0, 0, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, None, None, None, @@ -441,11 +908,14 @@ fn encode_buffer_request<'a>( fb::BufferOp::Capture, (*buffer_id).into(), 0, + 0, false, false, false, 0, 0, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, None, None, None, @@ -455,11 +925,14 @@ fn encode_buffer_request<'a>( fb::BufferOp::CaptureVisible, (*buffer_id).into(), 0, + 0, false, false, false, 0, 0, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, None, None, None, @@ -474,44 +947,111 @@ fn encode_buffer_request<'a>( fb::BufferOp::ScrollbackSlice, (*buffer_id).into(), 0, + 0, false, false, false, *start_line, *line_count, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, None, None, None, None, ), - }; - - let title = title_str.map(|s| builder.create_string(s)); - let cwd = cwd_str.map(|s| builder.create_string(s)); - let command = command_vec.map(|cmd_vec| { - let strings: Vec<_> = cmd_vec.iter().map(|s| builder.create_string(s)).collect(); - builder.create_vector(&strings) - }); - let env_keys = env_entries.map(|env| { - let keys = env.keys().cloned().collect::>(); - create_string_vector(builder, &keys) - }); - let env_values = env_entries.map(|env| { - let values = env.values().cloned().collect::>(); - create_string_vector(builder, &values) - }); - - let buffer_req = fb::BufferRequest::create( - builder, - &fb::BufferRequestArgs { - op, + BufferRequest::GetLocation { buffer_id, .. } => ( + fb::BufferOp::GetLocation, + (*buffer_id).into(), + 0, + 0, + false, + false, + false, + 0, + 0, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, + None, + None, + None, + None, + ), + BufferRequest::Reveal { buffer_id, - session_id, - attached_only, - detached_only, + client_id, + .. + } => ( + fb::BufferOp::Reveal, + (*buffer_id).into(), + 0, + encode_optional_buffer_client_id(*client_id)?, + false, + false, + false, + 0, + 0, + fb::BufferHistoryScopeWire::Full, + fb::BufferHistoryPlacementWire::Tab, + None, + None, + None, + None, + ), + BufferRequest::OpenHistory { + buffer_id, + scope, + placement, + client_id, + .. + } => ( + fb::BufferOp::OpenHistory, + (*buffer_id).into(), + 0, + encode_optional_buffer_client_id(*client_id)?, + false, + false, + false, + 0, + 0, + encode_buffer_history_scope(*scope), + encode_buffer_history_placement(*placement), + None, + None, + None, + None, + ), + }; + + let title = title_str.map(|s| builder.create_string(s)); + let cwd = cwd_str.map(|s| builder.create_string(s)); + let command = command_vec.map(|cmd_vec| { + let strings: Vec<_> = cmd_vec.iter().map(|s| builder.create_string(s)).collect(); + builder.create_vector(&strings) + }); + let env_keys = env_entries.map(|env| { + let keys = env.keys().cloned().collect::>(); + create_string_vector(builder, &keys) + }); + let env_values = env_entries.map(|env| { + let values = env.values().cloned().collect::>(); + create_string_vector(builder, &values) + }); + + let buffer_req = fb::BufferRequest::create( + builder, + &fb::BufferRequestArgs { + op, + buffer_id, + session_id, + client_id, + attached_only, + detached_only, force, start_line, line_count, + history_scope, + history_placement, title, command, cwd, @@ -520,7 +1060,7 @@ fn encode_buffer_request<'a>( }, ); - fb::Envelope::create( + Ok(fb::Envelope::create( builder, &fb::EnvelopeArgs { request_id: req.request_id().into(), @@ -528,13 +1068,329 @@ fn encode_buffer_request<'a>( buffer_request: Some(buffer_req), ..Default::default() }, + )) +} + +fn encode_optional_buffer_client_id(client_id: Option) -> Result { + Ok(client_id.map(NonZeroU64::get).unwrap_or(0)) +} + +fn validate_buffer_with_location_response( + response: &BufferWithLocationResponse, +) -> Result<(), ProtocolError> { + validate_buffer_location( + response.location(), + "buffer_with_location_response.location", + )?; + validate_buffer_record(response.buffer())?; + BufferWithLocationResponse::new( + response.request_id(), + response.buffer().clone(), + *response.location(), + response.at_root_tab(), + ) + .map(|_| ()) + .map_err(ProtocolError::InvalidMessageOwned) +} + +fn validate_buffer_location( + location: &BufferLocation, + field: &'static str, +) -> Result<(), ProtocolError> { + if location.buffer_id.0 == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field}.buffer_id must be non-zero" + ))); + } + + match location.attachment { + BufferLocationAttachment::Detached => Ok(()), + BufferLocationAttachment::Session { + session_id, + node_id, + } => { + if session_id.0 == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field}.session_id must be non-zero" + ))); + } + if node_id.0 == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field}.node_id must be non-zero" + ))); + } + Ok(()) + } + BufferLocationAttachment::Floating { + session_id, + node_id, + floating_id, + } => { + if session_id.0 == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field}.session_id must be non-zero" + ))); + } + if node_id.0 == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field}.node_id must be non-zero" + ))); + } + if floating_id.0 == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field}.floating_id must be non-zero" + ))); + } + Ok(()) + } + } +} + +fn validate_session_record(record: &SessionRecord) -> Result<(), ProtocolError> { + validate_required_session_id(record.id, "session_record.id")?; + validate_required_node_id(record.root_node_id, "session_record.root_node_id")?; + validate_required_floating_ids(&record.floating_ids, "session_record.floating_ids")?; + validate_optional_node_id(record.focused_leaf_id, "session_record.focused_leaf_id")?; + validate_optional_floating_id( + record.focused_floating_id, + "session_record.focused_floating_id", + )?; + validate_optional_node_id(record.zoomed_node_id, "session_record.zoomed_node_id") +} + +fn validate_buffer_record(record: &BufferRecord) -> Result<(), ProtocolError> { + validate_required_buffer_id(record.id, "buffer_record.id")?; + validate_optional_node_id( + record.attachment_node_id, + "buffer_record.attachment_node_id", + )?; + validate_optional_buffer_id( + record.helper_source_buffer_id, + "buffer_record.helper_source_buffer_id", + )?; + record + .validate() + .map_err(ProtocolError::InvalidMessageOwned) +} + +fn validate_node_record(record: &NodeRecord) -> Result<(), ProtocolError> { + validate_required_node_id(record.id, "node_record.id")?; + validate_required_session_id(record.session_id, "node_record.session_id")?; + validate_optional_node_id(record.parent_id, "node_record.parent_id")?; + if let Some(buffer_view) = &record.buffer_view { + validate_required_buffer_id(buffer_view.buffer_id, "node_record.buffer_view.buffer_id")?; + } + if let Some(split) = &record.split { + validate_required_node_ids(&split.child_ids, "node_record.split.child_ids")?; + } + if let Some(tabs) = &record.tabs { + for tab in &tabs.tabs { + validate_required_node_id(tab.child_id, "node_record.tabs.tabs.child_id")?; + } + } + Ok(()) +} + +fn validate_floating_record(record: &FloatingRecord) -> Result<(), ProtocolError> { + validate_required_floating_id(record.id, "floating_record.id")?; + validate_required_session_id(record.session_id, "floating_record.session_id")?; + validate_required_node_id(record.root_node_id, "floating_record.root_node_id") +} + +fn validate_client_record(record: &ClientRecord) -> Result<(), ProtocolError> { + validate_nonzero_id(record.id, "client_record.id")?; + validate_optional_session_id( + record.current_session_id, + "client_record.current_session_id", + )?; + validate_required_session_ids( + &record.subscribed_session_ids, + "client_record.subscribed_session_ids", ) } +fn validate_session_snapshot(snapshot: &SessionSnapshot) -> Result<(), ProtocolError> { + validate_session_record(&snapshot.session)?; + for node in &snapshot.nodes { + validate_node_record(node)?; + } + for buffer in &snapshot.buffers { + validate_buffer_record(buffer)?; + } + for floating in &snapshot.floating { + validate_floating_record(floating)?; + } + Ok(()) +} + +fn validate_server_response(response: &ServerResponse) -> Result<(), ProtocolError> { + match response { + ServerResponse::Pong(_) | ServerResponse::Ok(_) | ServerResponse::Error(_) => Ok(()), + ServerResponse::Sessions(response) => { + for session in &response.sessions { + validate_session_record(session)?; + } + Ok(()) + } + ServerResponse::SessionSnapshot(response) => validate_session_snapshot(&response.snapshot), + ServerResponse::Buffers(response) => { + for buffer in &response.buffers { + validate_buffer_record(buffer)?; + } + Ok(()) + } + ServerResponse::Buffer(response) => validate_buffer_record(&response.buffer), + ServerResponse::BufferWithLocation(response) => { + validate_buffer_with_location_response(response) + } + ServerResponse::FloatingList(response) => { + for floating in &response.floating { + validate_floating_record(floating)?; + } + Ok(()) + } + ServerResponse::Floating(response) => validate_floating_record(&response.floating), + ServerResponse::SubscriptionAck(_) => Ok(()), + ServerResponse::Clients(response) => { + for client in &response.clients { + validate_client_record(client)?; + } + Ok(()) + } + ServerResponse::Client(response) => validate_client_record(&response.client), + ServerResponse::BufferLocation(response) => { + validate_buffer_location(&response.location, "buffer_location_response.location") + } + ServerResponse::Snapshot(response) => { + validate_required_buffer_id(response.buffer_id, "snapshot_response.buffer_id") + } + ServerResponse::VisibleSnapshot(response) => { + validate_required_buffer_id(response.buffer_id, "visible_snapshot_response.buffer_id") + } + ServerResponse::ScrollbackSlice(response) => { + validate_required_buffer_id(response.buffer_id, "scrollback_slice_response.buffer_id") + } + } +} + +fn validate_server_event(event: &ServerEvent) -> Result<(), ProtocolError> { + match event { + ServerEvent::SessionCreated(event) => validate_session_record(&event.session), + ServerEvent::SessionClosed(event) => { + validate_required_session_id(event.session_id, "session_closed_event.session_id") + } + ServerEvent::SessionRenamed(event) => { + validate_required_session_id(event.session_id, "session_renamed_event.session_id") + } + ServerEvent::BufferCreated(event) => validate_buffer_record(&event.buffer), + ServerEvent::BufferDetached(event) => { + validate_required_buffer_id(event.buffer_id, "buffer_detached_event.buffer_id") + } + ServerEvent::NodeChanged(event) => { + validate_required_session_id(event.session_id, "node_changed_event.session_id") + } + ServerEvent::FloatingChanged(event) => { + validate_required_session_id(event.session_id, "floating_changed_event.session_id")?; + validate_optional_floating_id(event.floating_id, "floating_changed_event.floating_id") + } + ServerEvent::FocusChanged(event) => { + validate_required_session_id(event.session_id, "focus_changed_event.session_id")?; + validate_optional_node_id( + event.focused_leaf_id, + "focus_changed_event.focused_leaf_id", + )?; + validate_optional_floating_id( + event.focused_floating_id, + "focus_changed_event.focused_floating_id", + ) + } + ServerEvent::RenderInvalidated(event) => { + validate_required_buffer_id(event.buffer_id, "render_invalidated_event.buffer_id") + } + ServerEvent::ClientChanged(event) => { + validate_client_record(&event.client)?; + validate_optional_session_id( + event.previous_session_id, + "client_changed_event.previous_session_id", + ) + } + } +} + +fn validate_server_envelope(envelope: &ServerEnvelope) -> Result<(), ProtocolError> { + match envelope { + ServerEnvelope::Response(response) => validate_server_response(response), + ServerEnvelope::Event(event) => validate_server_event(event), + } +} + +fn encode_buffer_location_fields(location: &BufferLocation) -> (u64, u64, u64, u64) { + match location.attachment { + BufferLocationAttachment::Detached => (location.buffer_id.into(), 0, 0, 0), + BufferLocationAttachment::Session { + session_id, + node_id, + } => ( + location.buffer_id.into(), + session_id.into(), + node_id.into(), + 0, + ), + BufferLocationAttachment::Floating { + session_id, + node_id, + floating_id, + } => ( + location.buffer_id.into(), + session_id.into(), + node_id.into(), + floating_id.into(), + ), + } +} + +fn decode_buffer_location( + buffer_id: u64, + session_id: u64, + node_id: u64, + floating_id: u64, + field: &'static str, +) -> Result { + if buffer_id == 0 { + return Err(ProtocolError::InvalidMessageOwned(format!( + "{field}.buffer_id must be non-zero" + ))); + } + + let buffer_id = BufferId(buffer_id); + match (session_id, node_id, floating_id) { + (0, 0, 0) => Ok(BufferLocation::detached(buffer_id)), + (session_id, node_id, 0) if session_id != 0 && node_id != 0 => Ok(BufferLocation::session( + buffer_id, + SessionId(session_id), + NodeId(node_id), + )), + (session_id, node_id, floating_id) + if session_id != 0 && node_id != 0 && floating_id != 0 => + { + Ok(BufferLocation::floating( + buffer_id, + SessionId(session_id), + NodeId(node_id), + FloatingId(floating_id), + )) + } + _ => Err(ProtocolError::InvalidMessageOwned(format!( + "{field} has an invalid attachment combination" + ))), + } +} + fn encode_node_request<'a>( builder: &mut FlatBufferBuilder<'a>, req: &NodeRequest, -) -> flatbuffers::WIPOffset> { +) -> Result>, ProtocolError> { + validate_node_request(req)?; type EncodedNodeRequest<'a> = ( fb::NodeOp, u64, @@ -549,10 +1405,15 @@ fn encode_node_request<'a>( u32, u32, fb::SplitDirectionWire, + fb::NodeBreakDestinationWire, + fb::NodeJoinPlacementWire, Option<&'a Vec>, Option>, Option>, bool, + u64, + u64, + u64, ); let ( @@ -569,10 +1430,15 @@ fn encode_node_request<'a>( index, active, direction, + break_destination, + join_placement, sizes_vec, child_node_ids_vec, titles_vec, insert_before, + first_node_id, + second_node_id, + sibling_node_id, ): EncodedNodeRequest<'_> = match req { NodeRequest::GetTree { session_id, .. } => ( fb::NodeOp::GetTree, @@ -588,10 +1454,15 @@ fn encode_node_request<'a>( 0, 0, fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, None, None, false, + 0, + 0, + 0, ), NodeRequest::Split { leaf_node_id, @@ -617,10 +1488,15 @@ fn encode_node_request<'a>( 0, 0, dir, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, None, None, false, + 0, + 0, + 0, ) } NodeRequest::CreateSplit { @@ -648,6 +1524,8 @@ fn encode_node_request<'a>( 0, 0, dir, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, Some(sizes), Some( child_node_ids @@ -657,6 +1535,9 @@ fn encode_node_request<'a>( ), None, false, + 0, + 0, + 0, ) } NodeRequest::CreateTabs { @@ -679,6 +1560,8 @@ fn encode_node_request<'a>( 0, *active, fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, Some( child_node_ids @@ -688,6 +1571,9 @@ fn encode_node_request<'a>( ), Some(titles.clone()), false, + 0, + 0, + 0, ), NodeRequest::ReplaceNode { node_id, @@ -707,10 +1593,15 @@ fn encode_node_request<'a>( 0, 0, fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, None, None, false, + 0, + 0, + 0, ), NodeRequest::WrapInSplit { node_id, @@ -737,10 +1628,15 @@ fn encode_node_request<'a>( 0, 0, dir, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, None, None, *insert_before, + 0, + 0, + 0, ) } NodeRequest::WrapInTabs { node_id, title, .. } => ( @@ -757,10 +1653,15 @@ fn encode_node_request<'a>( 0, 0, fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, None, None, false, + 0, + 0, + 0, ), NodeRequest::AddTab { tabs_node_id, @@ -783,10 +1684,15 @@ fn encode_node_request<'a>( *index, 0, fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, None, None, false, + 0, + 0, + 0, ), NodeRequest::SelectTab { tabs_node_id, @@ -806,10 +1712,15 @@ fn encode_node_request<'a>( *index, 0, fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, None, None, false, + 0, + 0, + 0, ), NodeRequest::Focus { session_id, @@ -829,10 +1740,15 @@ fn encode_node_request<'a>( 0, 0, fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, None, None, false, + 0, + 0, + 0, ), NodeRequest::Close { node_id, .. } => ( fb::NodeOp::Close, @@ -848,10 +1764,15 @@ fn encode_node_request<'a>( 0, 0, fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, None, None, false, + 0, + 0, + 0, ), NodeRequest::MoveBufferToNode { buffer_id, @@ -871,10 +1792,15 @@ fn encode_node_request<'a>( 0, 0, fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, None, None, None, false, + 0, + 0, + 0, ), NodeRequest::Resize { node_id, sizes, .. } => ( fb::NodeOp::Resize, @@ -890,55 +1816,279 @@ fn encode_node_request<'a>( 0, 0, fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, Some(sizes), None, None, false, + 0, + 0, + 0, ), - }; - - let title = title_str.map(|s| builder.create_string(s)); - let sizes = sizes_vec.map(|sizes| builder.create_vector(sizes)); - let child_node_ids = child_node_ids_vec.map(|ids| builder.create_vector(&ids)); - let titles = titles_vec.map(|values| create_string_vector(builder, &values)); - let node_req = fb::NodeRequest::create( - builder, - &fb::NodeRequestArgs { - op, - session_id, - node_id, - leaf_node_id, - tabs_node_id, - child_node_id, - target_leaf_node_id, - buffer_id, - new_buffer_id, - title, - index, - active, - direction, - sizes, - child_node_ids, - titles, - insert_before, - }, - ); - - fb::Envelope::create( - builder, - &fb::EnvelopeArgs { - request_id: req.request_id().into(), - kind: fb::MessageKind::NodeRequest, - node_request: Some(node_req), - ..Default::default() - }, - ) -} - -fn encode_floating_request<'a>( - builder: &mut FlatBufferBuilder<'a>, - req: &FloatingRequest, -) -> flatbuffers::WIPOffset> { + NodeRequest::Zoom { node_id, .. } => ( + fb::NodeOp::Zoom, + 0, + (*node_id).into(), + 0, + 0, + 0, + 0, + 0, + 0, + None, + 0, + 0, + fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, + None, + None, + None, + false, + 0, + 0, + 0, + ), + NodeRequest::Unzoom { session_id, .. } => ( + fb::NodeOp::Unzoom, + (*session_id).into(), + 0, + 0, + 0, + 0, + 0, + 0, + 0, + None, + 0, + 0, + fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, + None, + None, + None, + false, + 0, + 0, + 0, + ), + NodeRequest::ToggleZoom { node_id, .. } => ( + fb::NodeOp::ToggleZoom, + 0, + (*node_id).into(), + 0, + 0, + 0, + 0, + 0, + 0, + None, + 0, + 0, + fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, + None, + None, + None, + false, + 0, + 0, + 0, + ), + NodeRequest::SwapSiblings { + first_node_id, + second_node_id, + .. + } => ( + fb::NodeOp::SwapSiblings, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + None, + 0, + 0, + fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, + None, + None, + None, + false, + (*first_node_id).into(), + (*second_node_id).into(), + 0, + ), + NodeRequest::BreakNode { + node_id, + destination, + .. + } => ( + fb::NodeOp::BreakNode, + 0, + (*node_id).into(), + 0, + 0, + 0, + 0, + 0, + 0, + None, + 0, + 0, + fb::SplitDirectionWire::Horizontal, + encode_node_break_destination(*destination), + fb::NodeJoinPlacementWire::Left, + None, + None, + None, + false, + 0, + 0, + 0, + ), + NodeRequest::JoinBufferAtNode { + node_id, + buffer_id, + placement, + .. + } => ( + fb::NodeOp::JoinBufferAtNode, + 0, + (*node_id).into(), + 0, + 0, + 0, + 0, + (*buffer_id).into(), + 0, + None, + 0, + 0, + fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + encode_node_join_placement(*placement), + None, + None, + None, + false, + 0, + 0, + 0, + ), + NodeRequest::MoveNodeBefore { + node_id, + sibling_node_id, + .. + } => ( + fb::NodeOp::MoveNodeBefore, + 0, + (*node_id).into(), + 0, + 0, + 0, + 0, + 0, + 0, + None, + 0, + 0, + fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, + None, + None, + None, + false, + 0, + 0, + (*sibling_node_id).into(), + ), + NodeRequest::MoveNodeAfter { + node_id, + sibling_node_id, + .. + } => ( + fb::NodeOp::MoveNodeAfter, + 0, + (*node_id).into(), + 0, + 0, + 0, + 0, + 0, + 0, + None, + 0, + 0, + fb::SplitDirectionWire::Horizontal, + fb::NodeBreakDestinationWire::Tab, + fb::NodeJoinPlacementWire::Left, + None, + None, + None, + false, + 0, + 0, + (*sibling_node_id).into(), + ), + }; + + let title = title_str.map(|s| builder.create_string(s)); + let sizes = sizes_vec.map(|sizes| builder.create_vector(sizes)); + let child_node_ids = child_node_ids_vec.map(|ids| builder.create_vector(&ids)); + let titles = titles_vec.map(|values| create_string_vector(builder, &values)); + let node_req = fb::NodeRequest::create( + builder, + &fb::NodeRequestArgs { + op, + session_id, + node_id, + leaf_node_id, + tabs_node_id, + child_node_id, + target_leaf_node_id, + buffer_id, + new_buffer_id, + title, + index, + active, + direction, + break_destination, + join_placement, + sizes, + child_node_ids, + titles, + insert_before, + first_node_id, + second_node_id, + sibling_node_id, + }, + ); + + Ok(fb::Envelope::create( + builder, + &fb::EnvelopeArgs { + request_id: req.request_id().into(), + kind: fb::MessageKind::NodeRequest, + node_request: Some(node_req), + ..Default::default() + }, + )) +} + +fn encode_floating_request<'a>( + builder: &mut FlatBufferBuilder<'a>, + req: &FloatingRequest, +) -> Result>, ProtocolError> { + validate_floating_request(req)?; let ( op, floating_id, @@ -1032,7 +2182,7 @@ fn encode_floating_request<'a>( }, ); - fb::Envelope::create( + Ok(fb::Envelope::create( builder, &fb::EnvelopeArgs { request_id: req.request_id().into(), @@ -1040,13 +2190,14 @@ fn encode_floating_request<'a>( floating_request: Some(floating_req), ..Default::default() }, - ) + )) } fn encode_input_request<'a>( builder: &mut FlatBufferBuilder<'a>, req: &InputRequest, -) -> flatbuffers::WIPOffset> { +) -> Result>, ProtocolError> { + validate_input_request(req)?; let (op, buffer_id, bytes_vec, cols, rows) = match req { InputRequest::Send { buffer_id, bytes, .. @@ -1071,7 +2222,7 @@ fn encode_input_request<'a>( }, ); - fb::Envelope::create( + Ok(fb::Envelope::create( builder, &fb::EnvelopeArgs { request_id: req.request_id().into(), @@ -1079,7 +2230,7 @@ fn encode_input_request<'a>( input_request: Some(input_req), ..Default::default() }, - ) + )) } fn encode_subscribe_request<'a>( @@ -1128,6 +2279,7 @@ fn encode_unsubscribe_request<'a>( pub fn encode_server_envelope(envelope: &ServerEnvelope) -> Result, ProtocolError> { let mut builder = FlatBufferBuilder::new(); + validate_server_envelope(envelope)?; let fb_envelope = match envelope { ServerEnvelope::Response(response) => encode_server_response(&mut builder, response), @@ -1274,6 +2426,40 @@ fn encode_server_response<'a>( }, ) } + ServerResponse::BufferWithLocation(r) => { + let buffer_record = r.buffer(); + let location_record = r.location(); + let buffer = encode_buffer_record(builder, buffer_record); + debug_assert_eq!(buffer_record.id, location_record.buffer_id); + let (buffer_id, session_id, node_id, floating_id) = + encode_buffer_location_fields(location_record); + let location = fb::BufferLocation::create( + builder, + &fb::BufferLocationArgs { + buffer_id, + session_id, + node_id, + floating_id, + }, + ); + let response = fb::BufferWithLocationResponse::create( + builder, + &fb::BufferWithLocationResponseArgs { + buffer: Some(buffer), + location: Some(location), + at_root_tab: r.at_root_tab(), + }, + ); + fb::Envelope::create( + builder, + &fb::EnvelopeArgs { + request_id: r.request_id().into(), + kind: fb::MessageKind::BufferWithLocationResponse, + buffer_with_location_response: Some(response), + ..Default::default() + }, + ) + } ServerResponse::FloatingList(r) => { let floating_vec: Vec<_> = r .floating @@ -1373,6 +2559,34 @@ fn encode_server_response<'a>( }, ) } + ServerResponse::BufferLocation(r) => { + let (buffer_id, session_id, node_id, floating_id) = + encode_buffer_location_fields(&r.location); + let location = fb::BufferLocation::create( + builder, + &fb::BufferLocationArgs { + buffer_id, + session_id, + node_id, + floating_id, + }, + ); + let response = fb::BufferLocationResponse::create( + builder, + &fb::BufferLocationResponseArgs { + location: Some(location), + }, + ); + fb::Envelope::create( + builder, + &fb::EnvelopeArgs { + request_id: r.request_id.into(), + kind: fb::MessageKind::BufferLocationResponse, + buffer_location_response: Some(response), + ..Default::default() + }, + ) + } ServerResponse::Snapshot(r) => { let title = r.title.as_ref().map(|t| builder.create_string(t)); let cwd = r.cwd.as_ref().map(|c| builder.create_string(c)); @@ -1669,6 +2883,7 @@ fn encode_session_record<'a>( floating_ids: Some(floating_ids_vec), focused_leaf_id: record.focused_leaf_id.map(|n| n.into()).unwrap_or(0), focused_floating_id: record.focused_floating_id.map(|f| f.into()).unwrap_or(0), + zoomed_node_id: record.zoomed_node_id.map(|n| n.into()).unwrap_or(0), }, ) } @@ -1702,6 +2917,14 @@ fn encode_buffer_record<'a>( ActivityState::Activity => fb::ActivityStateWire::Activity, ActivityState::Bell => fb::ActivityStateWire::Bell, }; + let kind = match record.kind { + BufferRecordKind::Pty => fb::BufferKindWire::Pty, + BufferRecordKind::Helper => fb::BufferKindWire::Helper, + }; + let helper_scope = record + .helper_scope + .map(encode_buffer_history_scope) + .unwrap_or(fb::BufferHistoryScopeWire::Full); fb::BufferRecord::create( builder, @@ -1710,10 +2933,18 @@ fn encode_buffer_record<'a>( title: Some(title), command: Some(command), cwd, + kind, state, pid: record.pid.unwrap_or(0), has_pid: record.pid.is_some(), attachment_node_id: record.attachment_node_id.map(|n| n.into()).unwrap_or(0), + read_only: record.read_only, + helper_source_buffer_id: record + .helper_source_buffer_id + .map(|id| id.into()) + .unwrap_or(0), + helper_scope, + has_helper_scope: record.helper_scope.is_some(), pty_cols: record.pty_size.cols, pty_rows: record.pty_size.rows, activity, @@ -1918,39 +3149,60 @@ pub fn decode_client_message(bytes: &[u8]) -> Result SessionRequest::List { request_id }, fb::SessionOp::Get => SessionRequest::Get { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "session_request.session_id", + )?, }, fb::SessionOp::Close => SessionRequest::Close { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "session_request.session_id", + )?, force: req.force(), }, fb::SessionOp::Rename => SessionRequest::Rename { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "session_request.session_id", + )?, name: required(req.name(), "session_request.name")?.to_owned(), }, fb::SessionOp::AddRootTab => SessionRequest::AddRootTab { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "session_request.session_id", + )?, title: required(req.title(), "session_request.title")?.to_owned(), buffer_id: (req.buffer_id() != 0).then(|| BufferId(req.buffer_id())), child_node_id: (req.child_node_id() != 0).then(|| NodeId(req.child_node_id())), }, fb::SessionOp::SelectRootTab => SessionRequest::SelectRootTab { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "session_request.session_id", + )?, index: req.index(), }, fb::SessionOp::RenameRootTab => SessionRequest::RenameRootTab { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "session_request.session_id", + )?, index: req.index(), title: required(req.title(), "session_request.title")?.to_owned(), }, fb::SessionOp::CloseRootTab => SessionRequest::CloseRootTab { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "session_request.session_id", + )?, index: req.index(), }, _ => return Err(ProtocolError::InvalidMessage("unknown session op")), @@ -1985,31 +3237,81 @@ pub fn decode_client_message(bytes: &[u8]) -> Result BufferRequest::Get { request_id, - buffer_id: BufferId(req.buffer_id()), + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "buffer_request.buffer_id", + )?, + }, + fb::BufferOp::Inspect => BufferRequest::Inspect { + request_id, + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "buffer_request.buffer_id", + )?, }, fb::BufferOp::Detach => BufferRequest::Detach { request_id, - buffer_id: BufferId(req.buffer_id()), + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "buffer_request.buffer_id", + )?, }, fb::BufferOp::Kill => BufferRequest::Kill { request_id, - buffer_id: BufferId(req.buffer_id()), + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "buffer_request.buffer_id", + )?, force: req.force(), }, fb::BufferOp::Capture => BufferRequest::Capture { request_id, - buffer_id: BufferId(req.buffer_id()), + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "buffer_request.buffer_id", + )?, }, fb::BufferOp::CaptureVisible => BufferRequest::CaptureVisible { request_id, - buffer_id: BufferId(req.buffer_id()), + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "buffer_request.buffer_id", + )?, }, fb::BufferOp::ScrollbackSlice => BufferRequest::ScrollbackSlice { request_id, - buffer_id: BufferId(req.buffer_id()), + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "buffer_request.buffer_id", + )?, start_line: req.start_line(), line_count: req.line_count(), }, + fb::BufferOp::GetLocation => BufferRequest::GetLocation { + request_id, + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "buffer_request.buffer_id", + )?, + }, + fb::BufferOp::Reveal => BufferRequest::Reveal { + request_id, + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "buffer_request.buffer_id", + )?, + client_id: NonZeroU64::new(req.client_id()), + }, + fb::BufferOp::OpenHistory => BufferRequest::OpenHistory { + request_id, + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "buffer_request.buffer_id", + )?, + scope: decode_buffer_history_scope(req.history_scope())?, + placement: decode_buffer_history_placement(req.history_placement())?, + client_id: NonZeroU64::new(req.client_id()), + }, _ => return Err(ProtocolError::InvalidMessage("unknown buffer op")), }; Ok(ClientMessage::Buffer(buffer_request)) @@ -2019,7 +3321,10 @@ pub fn decode_client_message(bytes: &[u8]) -> Result NodeRequest::GetTree { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "node_request.session_id", + )?, }, fb::NodeOp::Split => { let direction = match req.direction() { @@ -2029,9 +3334,15 @@ pub fn decode_client_message(bytes: &[u8]) -> Result { @@ -2043,15 +3354,20 @@ pub fn decode_client_message(bytes: &[u8]) -> Result>()?; let sizes = req .sizes() .map(|sizes| sizes.iter().collect()) .unwrap_or_default(); NodeRequest::CreateSplit { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "node_request.session_id", + )?, direction, child_node_ids, sizes, @@ -2061,15 +3377,20 @@ pub fn decode_client_message(bytes: &[u8]) -> Result>()?; let titles = required(req.titles(), "node_request.titles")? .iter() .map(|title| title.to_owned()) .collect(); NodeRequest::CreateTabs { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "node_request.session_id", + )?, child_node_ids, titles, active: req.active(), @@ -2077,8 +3398,11 @@ pub fn decode_client_message(bytes: &[u8]) -> Result NodeRequest::ReplaceNode { request_id, - node_id: NodeId(req.node_id()), - child_node_id: NodeId(req.child_node_id()), + node_id: decode_required_node_id(req.node_id(), "node_request.node_id")?, + child_node_id: decode_required_node_id( + req.child_node_id(), + "node_request.child_node_id", + )?, }, fb::NodeOp::WrapInSplit => { let direction = match req.direction() { @@ -2088,8 +3412,11 @@ pub fn decode_client_message(bytes: &[u8]) -> Result Result Result Result NodeRequest::SelectTab { request_id, - tabs_node_id: NodeId(req.tabs_node_id()), + tabs_node_id: decode_required_node_id( + req.tabs_node_id(), + "node_request.tabs_node_id", + )?, index: req.index(), }, fb::NodeOp::Focus => NodeRequest::Focus { request_id, - session_id: SessionId(req.session_id()), - node_id: NodeId(req.node_id()), + session_id: decode_required_session_id( + req.session_id(), + "node_request.session_id", + )?, + node_id: decode_required_node_id(req.node_id(), "node_request.node_id")?, }, fb::NodeOp::Close => NodeRequest::Close { request_id, - node_id: NodeId(req.node_id()), + node_id: decode_required_node_id(req.node_id(), "node_request.node_id")?, }, fb::NodeOp::MoveBufferToNode => NodeRequest::MoveBufferToNode { request_id, - buffer_id: BufferId(req.buffer_id()), - target_leaf_node_id: NodeId(req.target_leaf_node_id()), + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "node_request.buffer_id", + )?, + target_leaf_node_id: decode_required_node_id( + req.target_leaf_node_id(), + "node_request.target_leaf_node_id", + )?, }, fb::NodeOp::Resize => { let sizes = required(req.sizes(), "node_request.sizes")?; NodeRequest::Resize { request_id, - node_id: NodeId(req.node_id()), + node_id: decode_required_node_id(req.node_id(), "node_request.node_id")?, sizes: sizes.iter().collect(), } } + fb::NodeOp::Zoom => NodeRequest::Zoom { + request_id, + node_id: decode_required_node_id(req.node_id(), "node_request.node_id")?, + }, + fb::NodeOp::Unzoom => NodeRequest::Unzoom { + request_id, + session_id: decode_required_session_id( + req.session_id(), + "node_request.session_id", + )?, + }, + fb::NodeOp::ToggleZoom => NodeRequest::ToggleZoom { + request_id, + node_id: decode_required_node_id(req.node_id(), "node_request.node_id")?, + }, + fb::NodeOp::SwapSiblings => NodeRequest::SwapSiblings { + request_id, + first_node_id: decode_required_node_id( + req.first_node_id(), + "node_request.first_node_id", + )?, + second_node_id: decode_required_node_id( + req.second_node_id(), + "node_request.second_node_id", + )?, + }, + fb::NodeOp::BreakNode => NodeRequest::BreakNode { + request_id, + node_id: decode_required_node_id(req.node_id(), "node_request.node_id")?, + destination: decode_node_break_destination(req.break_destination())?, + }, + fb::NodeOp::JoinBufferAtNode => NodeRequest::JoinBufferAtNode { + request_id, + node_id: decode_required_node_id(req.node_id(), "node_request.node_id")?, + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "node_request.buffer_id", + )?, + placement: decode_node_join_placement(req.join_placement())?, + }, + fb::NodeOp::MoveNodeBefore => NodeRequest::MoveNodeBefore { + request_id, + node_id: decode_required_node_id(req.node_id(), "node_request.node_id")?, + sibling_node_id: decode_required_node_id( + req.sibling_node_id(), + "node_request.sibling_node_id", + )?, + }, + fb::NodeOp::MoveNodeAfter => NodeRequest::MoveNodeAfter { + request_id, + node_id: decode_required_node_id(req.node_id(), "node_request.node_id")?, + sibling_node_id: decode_required_node_id( + req.sibling_node_id(), + "node_request.sibling_node_id", + )?, + }, _ => return Err(ProtocolError::InvalidMessage("unknown node op")), }; Ok(ClientMessage::Node(node_request)) @@ -2150,7 +3548,10 @@ pub fn decode_client_message(bytes: &[u8]) -> Result FloatingRequest::Create { request_id, - session_id: SessionId(req.session_id()), + session_id: decode_required_session_id( + req.session_id(), + "floating_request.session_id", + )?, root_node_id: (req.root_node_id() != 0).then(|| NodeId(req.root_node_id())), buffer_id: (req.buffer_id() != 0).then(|| BufferId(req.buffer_id())), geometry: FloatGeometry { @@ -2165,11 +3566,17 @@ pub fn decode_client_message(bytes: &[u8]) -> Result FloatingRequest::Close { request_id, - floating_id: FloatingId(req.floating_id()), + floating_id: decode_required_floating_id( + req.floating_id(), + "floating_request.floating_id", + )?, }, fb::FloatingOp::Move => FloatingRequest::Move { request_id, - floating_id: FloatingId(req.floating_id()), + floating_id: decode_required_floating_id( + req.floating_id(), + "floating_request.floating_id", + )?, geometry: FloatGeometry { x: req.x(), y: req.y(), @@ -2179,7 +3586,10 @@ pub fn decode_client_message(bytes: &[u8]) -> Result FloatingRequest::Focus { request_id, - floating_id: FloatingId(req.floating_id()), + floating_id: decode_required_floating_id( + req.floating_id(), + "floating_request.floating_id", + )?, }, _ => return Err(ProtocolError::InvalidMessage("unknown floating op")), }; @@ -2192,13 +3602,19 @@ pub fn decode_client_message(bytes: &[u8]) -> Result InputRequest::Resize { request_id, - buffer_id: BufferId(req.buffer_id()), + buffer_id: decode_required_buffer_id( + req.buffer_id(), + "input_request.buffer_id", + )?, cols: req.cols(), rows: req.rows(), }, @@ -2246,7 +3662,10 @@ pub fn decode_client_message(bytes: &[u8]) -> Result return Err(ProtocolError::InvalidMessage("unknown client op")), @@ -2339,6 +3758,44 @@ pub fn decode_server_envelope(bytes: &[u8]) -> Result { + let resp = required( + envelope.buffer_with_location_response(), + "buffer_with_location_response", + )?; + let buffer = required(resp.buffer(), "buffer_with_location_response.buffer")?; + let location = required(resp.location(), "buffer_with_location_response.location")?; + if location.buffer_id() == 0 { + return Err(ProtocolError::InvalidMessageOwned( + "buffer_with_location_response.location.buffer_id must be non-zero".to_owned(), + )); + } + let buffer = decode_buffer_record(buffer)?; + let location = decode_buffer_location( + location.buffer_id(), + location.session_id(), + location.node_id(), + location.floating_id(), + "buffer_with_location_response.location", + )?; + if buffer.id != location.buffer_id { + return Err(ProtocolError::InvalidMessageOwned(format!( + "buffer_with_location_response buffer id {} does not match location buffer id {}", + buffer.id.0, location.buffer_id.0, + ))); + } + Ok(ServerEnvelope::Response( + ServerResponse::BufferWithLocation( + BufferWithLocationResponse::new( + RequestId(envelope.request_id()), + buffer, + location, + resp.at_root_tab(), + ) + .map_err(ProtocolError::InvalidMessageOwned)?, + ), + )) + } fb::MessageKind::FloatingListResponse => { let resp = required(envelope.floating_list_response(), "floating_list_response")?; let floating = required(resp.floating(), "floating_list_response.floating")?; @@ -2394,6 +3851,30 @@ pub fn decode_server_envelope(bytes: &[u8]) -> Result { + let resp = required( + envelope.buffer_location_response(), + "buffer_location_response", + )?; + let location = required(resp.location(), "buffer_location_response.location")?; + if location.buffer_id() == 0 { + return Err(ProtocolError::InvalidMessageOwned( + "buffer_location_response.location.buffer_id must be non-zero".to_owned(), + )); + } + Ok(ServerEnvelope::Response(ServerResponse::BufferLocation( + BufferLocationResponse { + request_id: RequestId(envelope.request_id()), + location: decode_buffer_location( + location.buffer_id(), + location.session_id(), + location.node_id(), + location.floating_id(), + "buffer_location_response.location", + )?, + }, + ))) + } fb::MessageKind::SnapshotResponse => { let resp = required(envelope.snapshot_response(), "snapshot_response")?; let lines = required(resp.lines(), "snapshot_response.lines")?; @@ -2601,6 +4082,11 @@ fn decode_session_record(record: fb::SessionRecord) -> Result Result return Err(ProtocolError::InvalidMessage("unknown activity state")), }; let env = decode_string_map(record.env_keys(), record.env_values(), "buffer_record.env")?; + let kind = match record.kind() { + fb::BufferKindWire::Pty => BufferRecordKind::Pty, + fb::BufferKindWire::Helper => BufferRecordKind::Helper, + _ => return Err(ProtocolError::InvalidMessage("unknown buffer kind")), + }; + let helper_scope = if record.has_helper_scope() { + Some(decode_buffer_history_scope(record.helper_scope())?) + } else { + None + }; - Ok(BufferRecord { + let record = BufferRecord { id: BufferId(record.id()), title: title.to_owned(), command, cwd: record.cwd().map(|c| c.to_owned()), + kind, state, pid: record.has_pid().then(|| record.pid()), attachment_node_id: if record.attachment_node_id() == 0 { @@ -2637,6 +4134,13 @@ fn decode_buffer_record(record: fb::BufferRecord) -> Result Result Result { @@ -3194,4 +4702,932 @@ mod tests { assert_eq!(decoded, original); } + + #[test] + fn encode_buffer_location_rejects_zero_buffer_id() { + let error = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::BufferLocation(BufferLocationResponse { + request_id: RequestId(1), + location: BufferLocation::session(BufferId(0), SessionId(2), NodeId(3)), + }), + )) + .expect_err("zero location buffer id should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_location_response.location.buffer_id must be non-zero" + )); + } + + #[test] + fn encode_buffer_with_location_rejects_zero_buffer_id() { + let error = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::BufferWithLocation(BufferWithLocationResponse { + request_id: RequestId(1), + buffer: BufferRecord { + id: BufferId(0), + title: "helper".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(3)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + location: BufferLocation::session(BufferId(0), SessionId(2), NodeId(3)), + at_root_tab: false, + }), + )) + .expect_err("zero location buffer id should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_with_location_response.location.buffer_id must be non-zero" + )); + } + + #[test] + fn encode_buffer_with_location_rejects_mismatched_buffer_ids() { + let error = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::BufferWithLocation(BufferWithLocationResponse { + request_id: RequestId(1), + buffer: BufferRecord { + id: BufferId(7), + title: "helper".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(3)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + location: BufferLocation::session(BufferId(8), SessionId(2), NodeId(3)), + at_root_tab: false, + }), + )) + .expect_err("mismatched buffer ids should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_with_location_response.buffer.id must equal location.buffer_id" + )); + } + + #[test] + fn encode_buffer_with_location_rejects_attachment_mismatches() { + let attached_buffer_with_detached_location = + encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::BufferWithLocation(BufferWithLocationResponse { + request_id: RequestId(1), + buffer: BufferRecord { + id: BufferId(7), + title: "helper".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(3)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + location: BufferLocation::detached(BufferId(7)), + at_root_tab: false, + }), + )) + .expect_err("attached buffers require an attached location"); + let detached_buffer_with_attached_location = + encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::BufferWithLocation(BufferWithLocationResponse { + request_id: RequestId(1), + buffer: BufferRecord { + id: BufferId(7), + title: "helper".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: None, + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + location: BufferLocation::session(BufferId(7), SessionId(2), NodeId(3)), + at_root_tab: false, + }), + )) + .expect_err("detached buffers require a detached location"); + let mismatched_node_ids = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::BufferWithLocation(BufferWithLocationResponse { + request_id: RequestId(1), + buffer: BufferRecord { + id: BufferId(7), + title: "helper".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(4)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + location: BufferLocation::session(BufferId(7), SessionId(2), NodeId(3)), + at_root_tab: false, + }), + )) + .expect_err("attached node ids must match"); + + assert!(matches!( + attached_buffer_with_detached_location, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_with_location_response.buffer.attachment_node_id 3 requires an attached location" + )); + assert!(matches!( + detached_buffer_with_attached_location, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_with_location_response.location node_id 3 requires buffer.attachment_node_id" + )); + assert!(matches!( + mismatched_node_ids, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_with_location_response.buffer.attachment_node_id 4 must equal location node_id 3" + )); + } + + #[test] + fn encode_buffer_with_location_rejects_root_tab_flag_without_session_attachment() { + let error = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::BufferWithLocation(BufferWithLocationResponse { + request_id: RequestId(1), + buffer: BufferRecord { + id: BufferId(7), + title: "helper".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(3)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + location: BufferLocation::floating( + BufferId(7), + SessionId(2), + NodeId(3), + FloatingId(9), + ), + at_root_tab: true, + }), + )) + .expect_err("root-tab flag requires a session location"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_with_location_response.at_root_tab requires a session location" + )); + } + + #[test] + fn buffer_with_location_round_trips_root_tab_flag() { + let response = ServerEnvelope::Response(ServerResponse::BufferWithLocation( + BufferWithLocationResponse::new( + RequestId(1), + BufferRecord { + id: BufferId(7), + title: "helper".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(3)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + BufferLocation::session(BufferId(7), SessionId(2), NodeId(3)), + true, + ) + .expect("session locations can carry root-tab metadata"), + )); + let encoded = encode_server_envelope(&response).expect("encode response"); + let decoded = decode_server_envelope(&encoded).expect("decode response"); + + assert!(matches!( + decoded, + ServerEnvelope::Response(ServerResponse::BufferWithLocation(response)) + if response.at_root_tab() + )); + } + + #[test] + fn buffer_with_location_constructor_rejects_invalid_nested_helper_buffer() { + let error = BufferWithLocationResponse::new( + RequestId(1), + BufferRecord { + id: BufferId(7), + title: "helper".to_owned(), + command: Vec::new(), + cwd: None, + kind: BufferRecordKind::Helper, + state: BufferRecordState::Created, + pid: None, + attachment_node_id: Some(NodeId(3)), + read_only: true, + helper_source_buffer_id: Some(BufferId(6)), + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + BufferLocation::session(BufferId(7), SessionId(2), NodeId(3)), + false, + ) + .expect_err("invalid helper buffer should be rejected"); + + assert_eq!( + error, + "buffer_record.kind=helper must set helper_source_buffer_id and helper_scope together" + ); + } + + #[test] + fn encode_buffer_rejects_helper_fields_on_pty_records() { + let invalid_source = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::Buffer(BufferResponse { + request_id: RequestId(1), + buffer: BufferRecord { + id: BufferId(7), + title: "shell".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(3)), + read_only: false, + helper_source_buffer_id: Some(BufferId(12)), + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + }), + )) + .expect_err("pty records must not carry helper source ids"); + let invalid_scope = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::Buffer(BufferResponse { + request_id: RequestId(1), + buffer: BufferRecord { + id: BufferId(7), + title: "shell".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(3)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: Some(BufferHistoryScope::Visible), + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + }), + )) + .expect_err("pty records must not carry helper scopes"); + + assert!(matches!( + invalid_source, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_record.kind=pty cannot set helper_source_buffer_id" + )); + assert!(matches!( + invalid_scope, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_record.kind=pty cannot set helper_scope" + )); + } + + #[test] + fn encode_buffer_rejects_incomplete_helper_metadata() { + let missing_scope = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::Buffer(BufferResponse { + request_id: RequestId(1), + buffer: BufferRecord { + id: BufferId(7), + title: "helper".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Helper, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(3)), + read_only: true, + helper_source_buffer_id: Some(BufferId(12)), + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + }), + )) + .expect_err("helper records must not omit helper scope"); + let missing_source = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::Buffer(BufferResponse { + request_id: RequestId(1), + buffer: BufferRecord { + id: BufferId(7), + title: "helper".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Helper, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(3)), + read_only: true, + helper_source_buffer_id: None, + helper_scope: Some(BufferHistoryScope::Visible), + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + }), + )) + .expect_err("helper records must not omit helper source ids"); + + assert!(matches!( + missing_scope, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_record.kind=helper must set helper_source_buffer_id and helper_scope together" + )); + assert!(matches!( + missing_source, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_record.kind=helper must set helper_source_buffer_id and helper_scope together" + )); + } + + #[test] + fn encode_buffer_location_rejects_zero_attached_ids() { + let zero_session = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::BufferLocation(BufferLocationResponse { + request_id: RequestId(1), + location: BufferLocation { + buffer_id: BufferId(5), + attachment: BufferLocationAttachment::Session { + session_id: SessionId(0), + node_id: NodeId(3), + }, + }, + }), + )) + .expect_err("zero session id should be rejected"); + let zero_floating = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::BufferLocation(BufferLocationResponse { + request_id: RequestId(2), + location: BufferLocation { + buffer_id: BufferId(5), + attachment: BufferLocationAttachment::Floating { + session_id: SessionId(2), + node_id: NodeId(3), + floating_id: FloatingId(0), + }, + }, + }), + )) + .expect_err("zero floating id should be rejected"); + + assert!(matches!( + zero_session, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_location_response.location.session_id must be non-zero" + )); + assert!(matches!( + zero_floating, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_location_response.location.floating_id must be non-zero" + )); + } + + #[test] + fn encode_records_reject_zero_optional_ids() { + let zero_zoomed_node = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::Sessions(SessionsResponse { + request_id: RequestId(1), + sessions: vec![SessionRecord { + id: SessionId(1), + name: "main".to_owned(), + root_node_id: NodeId(2), + floating_ids: Vec::new(), + focused_leaf_id: None, + focused_floating_id: None, + zoomed_node_id: Some(NodeId(0)), + }], + }), + )) + .expect_err("zero zoomed node id should be rejected"); + let zero_attachment_node = encode_server_envelope(&ServerEnvelope::Response( + ServerResponse::Buffer(BufferResponse { + request_id: RequestId(2), + buffer: BufferRecord { + id: BufferId(7), + title: "helper".to_owned(), + command: vec!["echo".to_owned()], + cwd: None, + kind: BufferRecordKind::Pty, + state: BufferRecordState::Running, + pid: None, + attachment_node_id: Some(NodeId(0)), + read_only: false, + helper_source_buffer_id: None, + helper_scope: None, + pty_size: PtySize::new(80, 24), + activity: ActivityState::Idle, + last_snapshot_seq: 0, + exit_code: None, + env: Default::default(), + }, + }), + )) + .expect_err("zero attachment node id should be rejected"); + + assert!(matches!( + zero_zoomed_node, + ProtocolError::InvalidMessageOwned(message) + if message == "session_record.zoomed_node_id must be non-zero" + )); + assert!(matches!( + zero_attachment_node, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_record.attachment_node_id must be non-zero" + )); + } + + #[test] + fn decode_buffer_location_rejects_invalid_attachment_combinations() { + let mut builder = FlatBufferBuilder::new(); + let location = fb::BufferLocation::create( + &mut builder, + &fb::BufferLocationArgs { + buffer_id: 5, + session_id: 0, + node_id: 3, + floating_id: 0, + }, + ); + let response = fb::BufferLocationResponse::create( + &mut builder, + &fb::BufferLocationResponseArgs { + location: Some(location), + }, + ); + let envelope = fb::Envelope::create( + &mut builder, + &fb::EnvelopeArgs { + request_id: 1, + kind: fb::MessageKind::BufferLocationResponse, + buffer_location_response: Some(response), + ..Default::default() + }, + ); + builder.finish(envelope, Some("EMBR")); + + let error = decode_server_envelope(builder.finished_data()) + .expect_err("invalid attachment combination should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_location_response.location has an invalid attachment combination" + )); + } + + #[test] + fn encode_buffer_request_rejects_zero_required_buffer_id() { + let error = encode_client_message(&ClientMessage::Buffer(BufferRequest::Inspect { + request_id: RequestId(1), + buffer_id: BufferId(0), + })) + .expect_err("zero buffer id should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_request.buffer_id must be non-zero" + )); + } + + #[test] + fn encode_session_request_rejects_zero_required_session_id() { + let error = encode_client_message(&ClientMessage::Session(SessionRequest::Get { + request_id: RequestId(1), + session_id: SessionId(0), + })) + .expect_err("zero session id should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "session_request.session_id must be non-zero" + )); + } + + #[test] + fn encode_session_request_rejects_zero_optional_ids() { + let zero_buffer_id = + encode_client_message(&ClientMessage::Session(SessionRequest::AddRootTab { + request_id: RequestId(1), + session_id: SessionId(1), + title: "tab".to_string(), + buffer_id: Some(BufferId(0)), + child_node_id: None, + })) + .expect_err("zero optional buffer id should be rejected"); + let zero_child_node_id = + encode_client_message(&ClientMessage::Session(SessionRequest::AddRootTab { + request_id: RequestId(2), + session_id: SessionId(1), + title: "tab".to_string(), + buffer_id: None, + child_node_id: Some(NodeId(0)), + })) + .expect_err("zero optional child node id should be rejected"); + + assert!(matches!( + zero_buffer_id, + ProtocolError::InvalidMessageOwned(message) + if message == "session_request.buffer_id must be non-zero" + )); + assert!(matches!( + zero_child_node_id, + ProtocolError::InvalidMessageOwned(message) + if message == "session_request.child_node_id must be non-zero" + )); + } + + #[test] + fn encode_node_request_rejects_zero_required_node_id() { + let error = encode_client_message(&ClientMessage::Node(NodeRequest::Zoom { + request_id: RequestId(1), + node_id: NodeId(0), + })) + .expect_err("zero node id should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "node_request.node_id must be non-zero" + )); + } + + #[test] + fn encode_node_request_rejects_zero_optional_ids() { + let zero_buffer_id = encode_client_message(&ClientMessage::Node(NodeRequest::AddTab { + request_id: RequestId(1), + tabs_node_id: NodeId(1), + index: 0, + title: "tab".to_string(), + buffer_id: Some(BufferId(0)), + child_node_id: None, + })) + .expect_err("zero optional buffer id should be rejected"); + let zero_child_node_id = encode_client_message(&ClientMessage::Node(NodeRequest::AddTab { + request_id: RequestId(2), + tabs_node_id: NodeId(1), + index: 0, + title: "tab".to_string(), + buffer_id: None, + child_node_id: Some(NodeId(0)), + })) + .expect_err("zero optional child node id should be rejected"); + + assert!(matches!( + zero_buffer_id, + ProtocolError::InvalidMessageOwned(message) + if message == "node_request.buffer_id must be non-zero" + )); + assert!(matches!( + zero_child_node_id, + ProtocolError::InvalidMessageOwned(message) + if message == "node_request.child_node_id must be non-zero" + )); + } + + #[test] + fn encode_client_request_rejects_zero_switch_session_id() { + let error = encode_client_message(&ClientMessage::Client(ClientRequest::Switch { + request_id: RequestId(1), + client_id: None, + session_id: SessionId(0), + })) + .expect_err("zero switch session id should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "client_request.session_id must be non-zero" + )); + } + + #[test] + fn encode_buffer_request_rejects_zero_optional_session_id() { + let error = encode_client_message(&ClientMessage::Buffer(BufferRequest::List { + request_id: RequestId(1), + session_id: Some(SessionId(0)), + attached_only: false, + detached_only: false, + })) + .expect_err("zero optional session id should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_request.session_id must be non-zero" + )); + } + + #[test] + fn encode_floating_request_rejects_zero_required_ids() { + let create_error = + encode_client_message(&ClientMessage::Floating(FloatingRequest::Create { + request_id: RequestId(1), + session_id: SessionId(0), + root_node_id: None, + buffer_id: None, + geometry: FloatGeometry::new(1, 1, 10, 10), + title: None, + focus: true, + close_on_empty: true, + })) + .expect_err("zero create session id should be rejected"); + let close_error = encode_client_message(&ClientMessage::Floating(FloatingRequest::Close { + request_id: RequestId(2), + floating_id: FloatingId(0), + })) + .expect_err("zero close floating id should be rejected"); + + assert!(matches!( + create_error, + ProtocolError::InvalidMessageOwned(message) + if message == "floating_request.session_id must be non-zero" + )); + assert!(matches!( + close_error, + ProtocolError::InvalidMessageOwned(message) + if message == "floating_request.floating_id must be non-zero" + )); + } + + #[test] + fn encode_input_request_rejects_zero_required_buffer_id() { + let send_error = encode_client_message(&ClientMessage::Input(InputRequest::Send { + request_id: RequestId(1), + buffer_id: BufferId(0), + bytes: b"x".to_vec(), + })) + .expect_err("zero send buffer id should be rejected"); + let resize_error = encode_client_message(&ClientMessage::Input(InputRequest::Resize { + request_id: RequestId(2), + buffer_id: BufferId(0), + cols: 80, + rows: 24, + })) + .expect_err("zero resize buffer id should be rejected"); + + assert!(matches!( + send_error, + ProtocolError::InvalidMessageOwned(message) + if message == "input_request.buffer_id must be non-zero" + )); + assert!(matches!( + resize_error, + ProtocolError::InvalidMessageOwned(message) + if message == "input_request.buffer_id must be non-zero" + )); + } + + #[test] + fn decode_buffer_request_rejects_zero_required_buffer_id() { + let mut builder = FlatBufferBuilder::new(); + let request = fb::BufferRequest::create( + &mut builder, + &fb::BufferRequestArgs { + op: fb::BufferOp::Inspect, + buffer_id: 0, + ..Default::default() + }, + ); + let envelope = fb::Envelope::create( + &mut builder, + &fb::EnvelopeArgs { + request_id: 7, + kind: fb::MessageKind::BufferRequest, + buffer_request: Some(request), + ..Default::default() + }, + ); + builder.finish(envelope, Some("EMBR")); + + let error = decode_client_message(builder.finished_data()) + .expect_err("zero buffer id should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "buffer_request.buffer_id must be non-zero" + )); + } + + #[test] + fn decode_session_request_rejects_zero_required_session_id() { + let mut builder = FlatBufferBuilder::new(); + let request = fb::SessionRequest::create( + &mut builder, + &fb::SessionRequestArgs { + op: fb::SessionOp::Get, + session_id: 0, + ..Default::default() + }, + ); + let envelope = fb::Envelope::create( + &mut builder, + &fb::EnvelopeArgs { + request_id: 8, + kind: fb::MessageKind::SessionRequest, + session_request: Some(request), + ..Default::default() + }, + ); + builder.finish(envelope, Some("EMBR")); + + let error = decode_client_message(builder.finished_data()) + .expect_err("zero session id should be rejected"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "session_request.session_id must be non-zero" + )); + } + + #[test] + fn decode_node_request_rejects_zero_required_node_id() { + let mut builder = FlatBufferBuilder::new(); + let request = fb::NodeRequest::create( + &mut builder, + &fb::NodeRequestArgs { + op: fb::NodeOp::Zoom, + node_id: 0, + ..Default::default() + }, + ); + let envelope = fb::Envelope::create( + &mut builder, + &fb::EnvelopeArgs { + request_id: 9, + kind: fb::MessageKind::NodeRequest, + node_request: Some(request), + ..Default::default() + }, + ); + builder.finish(envelope, Some("EMBR")); + + let error = + decode_client_message(builder.finished_data()).expect_err("zero node id should reject"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "node_request.node_id must be non-zero" + )); + } + + #[test] + fn decode_floating_request_rejects_zero_required_floating_id() { + let mut builder = FlatBufferBuilder::new(); + let request = fb::FloatingRequest::create( + &mut builder, + &fb::FloatingRequestArgs { + op: fb::FloatingOp::Focus, + floating_id: 0, + ..Default::default() + }, + ); + let envelope = fb::Envelope::create( + &mut builder, + &fb::EnvelopeArgs { + request_id: 10, + kind: fb::MessageKind::FloatingRequest, + floating_request: Some(request), + ..Default::default() + }, + ); + builder.finish(envelope, Some("EMBR")); + + let error = decode_client_message(builder.finished_data()) + .expect_err("zero floating id should reject"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "floating_request.floating_id must be non-zero" + )); + } + + #[test] + fn decode_input_request_rejects_zero_required_buffer_id() { + let mut builder = FlatBufferBuilder::new(); + let bytes = builder.create_vector(b"x"); + let request = fb::InputRequest::create( + &mut builder, + &fb::InputRequestArgs { + op: fb::InputOp::Send, + buffer_id: 0, + bytes: Some(bytes), + ..Default::default() + }, + ); + let envelope = fb::Envelope::create( + &mut builder, + &fb::EnvelopeArgs { + request_id: 11, + kind: fb::MessageKind::InputRequest, + input_request: Some(request), + ..Default::default() + }, + ); + builder.finish(envelope, Some("EMBR")); + + let error = decode_client_message(builder.finished_data()) + .expect_err("zero input buffer id should reject"); + + assert!(matches!( + error, + ProtocolError::InvalidMessageOwned(message) + if message == "input_request.buffer_id must be non-zero" + )); + } } diff --git a/crates/embers-protocol/src/lib.rs b/crates/embers-protocol/src/lib.rs index b9a6c08..65858f4 100644 --- a/crates/embers-protocol/src/lib.rs +++ b/crates/embers-protocol/src/lib.rs @@ -24,14 +24,16 @@ pub use framing::{ FrameType, MAX_FRAME_LEN, RawFrame, read_frame, write_frame, write_frame_no_flush, }; pub use types::{ - BufferCreatedEvent, BufferDetachedEvent, BufferRecord, BufferRecordState, BufferRequest, - BufferResponse, BufferViewRecord, BuffersResponse, ClientChangedEvent, ClientMessage, - ClientRecord, ClientRequest, ClientResponse, ClientsResponse, ErrorResponse, - FloatingChangedEvent, FloatingListResponse, FloatingRecord, FloatingRequest, FloatingResponse, - FocusChangedEvent, InputRequest, NodeChangedEvent, NodeRecord, NodeRecordKind, NodeRequest, - OkResponse, PingRequest, PingResponse, RenderInvalidatedEvent, ScrollbackSliceResponse, - ServerEnvelope, ServerEvent, ServerResponse, SessionClosedEvent, SessionCreatedEvent, - SessionRecord, SessionRenamedEvent, SessionRequest, SessionSnapshot, SessionSnapshotResponse, - SessionsResponse, SnapshotResponse, SplitRecord, SubscribeRequest, SubscriptionAckResponse, - TabRecord, TabsRecord, UnsubscribeRequest, VisibleSnapshotResponse, + BufferCreatedEvent, BufferDetachedEvent, BufferHistoryPlacement, BufferHistoryScope, + BufferLocation, BufferLocationAttachment, BufferLocationResponse, BufferRecord, + BufferRecordKind, BufferRecordState, BufferRequest, BufferResponse, BufferViewRecord, + BufferWithLocationResponse, BuffersResponse, ClientChangedEvent, ClientMessage, ClientRecord, + ClientRequest, ClientResponse, ClientsResponse, ErrorResponse, FloatingChangedEvent, + FloatingListResponse, FloatingRecord, FloatingRequest, FloatingResponse, FocusChangedEvent, + InputRequest, NodeBreakDestination, NodeChangedEvent, NodeJoinPlacement, NodeRecord, + NodeRecordKind, NodeRequest, OkResponse, PingRequest, PingResponse, RenderInvalidatedEvent, + ScrollbackSliceResponse, ServerEnvelope, ServerEvent, ServerResponse, SessionClosedEvent, + SessionCreatedEvent, SessionRecord, SessionRenamedEvent, SessionRequest, SessionSnapshot, + SessionSnapshotResponse, SessionsResponse, SnapshotResponse, SplitRecord, SubscribeRequest, + SubscriptionAckResponse, TabRecord, TabsRecord, UnsubscribeRequest, VisibleSnapshotResponse, }; diff --git a/crates/embers-protocol/src/types.rs b/crates/embers-protocol/src/types.rs index 9fa72c4..f888d71 100644 --- a/crates/embers-protocol/src/types.rs +++ b/crates/embers-protocol/src/types.rs @@ -101,6 +101,10 @@ pub enum BufferRequest { request_id: RequestId, buffer_id: BufferId, }, + Inspect { + request_id: RequestId, + buffer_id: BufferId, + }, Detach { request_id: RequestId, buffer_id: BufferId, @@ -124,6 +128,22 @@ pub enum BufferRequest { start_line: u64, line_count: u32, }, + GetLocation { + request_id: RequestId, + buffer_id: BufferId, + }, + Reveal { + request_id: RequestId, + buffer_id: BufferId, + client_id: Option, + }, + OpenHistory { + request_id: RequestId, + buffer_id: BufferId, + scope: BufferHistoryScope, + placement: BufferHistoryPlacement, + client_id: Option, + }, } impl BufferRequest { @@ -132,15 +152,123 @@ impl BufferRequest { Self::Create { request_id, .. } | Self::List { request_id, .. } | Self::Get { request_id, .. } + | Self::Inspect { request_id, .. } | Self::Detach { request_id, .. } | Self::Kill { request_id, .. } | Self::Capture { request_id, .. } | Self::CaptureVisible { request_id, .. } - | Self::ScrollbackSlice { request_id, .. } => *request_id, + | Self::ScrollbackSlice { request_id, .. } + | Self::GetLocation { request_id, .. } + | Self::Reveal { request_id, .. } + | Self::OpenHistory { request_id, .. } => *request_id, } } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BufferHistoryScope { + Full, + Visible, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BufferHistoryPlacement { + Tab, + Floating, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BufferLocationAttachment { + Detached, + Session { + session_id: SessionId, + node_id: NodeId, + }, + Floating { + session_id: SessionId, + node_id: NodeId, + floating_id: FloatingId, + }, +} + +impl BufferLocationAttachment { + pub const fn session_id(self) -> Option { + match self { + Self::Detached => None, + Self::Session { session_id, .. } | Self::Floating { session_id, .. } => { + Some(session_id) + } + } + } + + pub const fn node_id(self) -> Option { + match self { + Self::Detached => None, + Self::Session { node_id, .. } | Self::Floating { node_id, .. } => Some(node_id), + } + } + + pub const fn floating_id(self) -> Option { + match self { + Self::Floating { floating_id, .. } => Some(floating_id), + Self::Detached | Self::Session { .. } => None, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BufferLocation { + pub buffer_id: BufferId, + pub attachment: BufferLocationAttachment, +} + +impl BufferLocation { + pub const fn detached(buffer_id: BufferId) -> Self { + Self { + buffer_id, + attachment: BufferLocationAttachment::Detached, + } + } + + pub const fn session(buffer_id: BufferId, session_id: SessionId, node_id: NodeId) -> Self { + Self { + buffer_id, + attachment: BufferLocationAttachment::Session { + session_id, + node_id, + }, + } + } + + pub const fn floating( + buffer_id: BufferId, + session_id: SessionId, + node_id: NodeId, + floating_id: FloatingId, + ) -> Self { + Self { + buffer_id, + attachment: BufferLocationAttachment::Floating { + session_id, + node_id, + floating_id, + }, + } + } + + pub const fn session_id(self) -> Option { + self.attachment.session_id() + } + + pub const fn node_id(self) -> Option { + self.attachment.node_id() + } + + pub const fn floating_id(self) -> Option { + self.attachment.floating_id() + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum NodeRequest { GetTree { @@ -216,6 +344,44 @@ pub enum NodeRequest { node_id: NodeId, sizes: Vec, }, + Zoom { + request_id: RequestId, + node_id: NodeId, + }, + Unzoom { + request_id: RequestId, + session_id: SessionId, + }, + ToggleZoom { + request_id: RequestId, + node_id: NodeId, + }, + SwapSiblings { + request_id: RequestId, + first_node_id: NodeId, + second_node_id: NodeId, + }, + BreakNode { + request_id: RequestId, + node_id: NodeId, + destination: NodeBreakDestination, + }, + JoinBufferAtNode { + request_id: RequestId, + node_id: NodeId, + buffer_id: BufferId, + placement: NodeJoinPlacement, + }, + MoveNodeBefore { + request_id: RequestId, + node_id: NodeId, + sibling_node_id: NodeId, + }, + MoveNodeAfter { + request_id: RequestId, + node_id: NodeId, + sibling_node_id: NodeId, + }, } impl NodeRequest { @@ -233,11 +399,35 @@ impl NodeRequest { | Self::Focus { request_id, .. } | Self::Close { request_id, .. } | Self::MoveBufferToNode { request_id, .. } - | Self::Resize { request_id, .. } => *request_id, + | Self::Resize { request_id, .. } + | Self::Zoom { request_id, .. } + | Self::Unzoom { request_id, .. } + | Self::ToggleZoom { request_id, .. } + | Self::SwapSiblings { request_id, .. } + | Self::BreakNode { request_id, .. } + | Self::JoinBufferAtNode { request_id, .. } + | Self::MoveNodeBefore { request_id, .. } + | Self::MoveNodeAfter { request_id, .. } => *request_id, } } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NodeBreakDestination { + Tab, + Floating, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NodeJoinPlacement { + Left, + Right, + Up, + Down, + TabBefore, + TabAfter, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum FloatingRequest { Create { @@ -387,6 +577,13 @@ pub struct SessionRecord { pub floating_ids: Vec, pub focused_leaf_id: Option, pub focused_floating_id: Option, + pub zoomed_node_id: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BufferRecordKind { + Pty, + Helper, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -395,9 +592,13 @@ pub struct BufferRecord { pub title: String, pub command: Vec, pub cwd: Option, + pub kind: BufferRecordKind, pub state: BufferRecordState, pub pid: Option, pub attachment_node_id: Option, + pub read_only: bool, + pub helper_source_buffer_id: Option, + pub helper_scope: Option, pub pty_size: PtySize, pub activity: ActivityState, pub last_snapshot_seq: u64, @@ -405,6 +606,32 @@ pub struct BufferRecord { pub env: BTreeMap, } +impl BufferRecord { + pub fn validate(&self) -> std::result::Result<(), String> { + match self.kind { + BufferRecordKind::Pty => { + if self.helper_source_buffer_id.is_some() { + return Err( + "buffer_record.kind=pty cannot set helper_source_buffer_id".to_owned() + ); + } + if self.helper_scope.is_some() { + return Err("buffer_record.kind=pty cannot set helper_scope".to_owned()); + } + } + BufferRecordKind::Helper => { + if self.helper_source_buffer_id.is_some() ^ self.helper_scope.is_some() { + return Err( + "buffer_record.kind=helper must set helper_source_buffer_id and helper_scope together" + .to_owned(), + ); + } + } + } + Ok(()) + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum NodeRecordKind { BufferView, @@ -544,6 +771,97 @@ pub struct ClientResponse { pub client: ClientRecord, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BufferLocationResponse { + pub request_id: RequestId, + pub location: BufferLocation, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BufferWithLocationResponse { + pub(crate) request_id: RequestId, + pub(crate) buffer: BufferRecord, + pub(crate) location: BufferLocation, + pub(crate) at_root_tab: bool, +} + +impl BufferWithLocationResponse { + pub fn new( + request_id: RequestId, + buffer: BufferRecord, + location: BufferLocation, + at_root_tab: bool, + ) -> std::result::Result { + if buffer.id != location.buffer_id { + return Err( + "buffer_with_location_response.buffer.id must equal location.buffer_id".to_owned(), + ); + } + buffer.validate()?; + match (buffer.attachment_node_id, location.node_id()) { + (Some(buffer_node_id), Some(location_node_id)) + if buffer_node_id == location_node_id => {} + (Some(buffer_node_id), Some(location_node_id)) => { + return Err(format!( + "buffer_with_location_response.buffer.attachment_node_id {buffer_node_id} must equal location node_id {location_node_id}" + )); + } + (Some(buffer_node_id), None) => { + return Err(format!( + "buffer_with_location_response.buffer.attachment_node_id {buffer_node_id} requires an attached location" + )); + } + (None, Some(location_node_id)) => { + return Err(format!( + "buffer_with_location_response.location node_id {location_node_id} requires buffer.attachment_node_id" + )); + } + (None, None) => {} + } + if at_root_tab + && !matches!( + location.attachment, + BufferLocationAttachment::Session { .. } + ) + { + return Err( + "buffer_with_location_response.at_root_tab requires a session location".to_owned(), + ); + } + Ok(Self { + request_id, + buffer, + location, + at_root_tab, + }) + } + + pub fn request_id(&self) -> RequestId { + self.request_id + } + + pub fn buffer(&self) -> &BufferRecord { + &self.buffer + } + + pub fn location(&self) -> &BufferLocation { + &self.location + } + + pub fn at_root_tab(&self) -> bool { + self.at_root_tab + } + + pub fn into_parts(self) -> (RequestId, BufferRecord, BufferLocation, bool) { + ( + self.request_id, + self.buffer, + self.location, + self.at_root_tab, + ) + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct SnapshotResponse { pub request_id: RequestId, @@ -596,6 +914,8 @@ pub enum ServerResponse { SubscriptionAck(SubscriptionAckResponse), Clients(ClientsResponse), Client(ClientResponse), + BufferLocation(BufferLocationResponse), + BufferWithLocation(BufferWithLocationResponse), Snapshot(SnapshotResponse), VisibleSnapshot(VisibleSnapshotResponse), ScrollbackSlice(ScrollbackSliceResponse), @@ -616,6 +936,8 @@ impl ServerResponse { Self::SubscriptionAck(response) => Some(response.request_id), Self::Clients(response) => Some(response.request_id), Self::Client(response) => Some(response.request_id), + Self::BufferLocation(response) => Some(response.request_id), + Self::BufferWithLocation(response) => Some(response.request_id()), Self::Snapshot(response) => Some(response.request_id), Self::VisibleSnapshot(response) => Some(response.request_id), Self::ScrollbackSlice(response) => Some(response.request_id), diff --git a/crates/embers-protocol/tests/family_round_trip.rs b/crates/embers-protocol/tests/family_round_trip.rs index 84c03c6..6de6fa3 100644 --- a/crates/embers-protocol/tests/family_round_trip.rs +++ b/crates/embers-protocol/tests/family_round_trip.rs @@ -1,3 +1,5 @@ +use std::num::NonZeroU64; + use embers_core::{ ActivityState, BufferId, CursorPosition, CursorShape, CursorState, ErrorCode, FloatGeometry, FloatingId, NodeId, PtySize, RequestId, SessionId, SplitDirection, WireError, @@ -67,6 +69,10 @@ fn client_message_families_round_trip() { request_id: RequestId(12), buffer_id: BufferId(20), }), + ClientMessage::Buffer(BufferRequest::Inspect { + request_id: RequestId(120), + buffer_id: BufferId(20), + }), ClientMessage::Buffer(BufferRequest::Detach { request_id: RequestId(13), buffer_id: BufferId(20), @@ -90,6 +96,34 @@ fn client_message_families_round_trip() { start_line: 4, line_count: 8, }), + ClientMessage::Buffer(BufferRequest::GetLocation { + request_id: RequestId(153), + buffer_id: BufferId(20), + }), + ClientMessage::Buffer(BufferRequest::Reveal { + request_id: RequestId(154), + buffer_id: BufferId(20), + client_id: Some(NonZeroU64::new(7).expect("non-zero client id")), + }), + ClientMessage::Buffer(BufferRequest::OpenHistory { + request_id: RequestId(155), + buffer_id: BufferId(20), + scope: BufferHistoryScope::Visible, + placement: BufferHistoryPlacement::Floating, + client_id: None, + }), + ClientMessage::Buffer(BufferRequest::Reveal { + request_id: RequestId(156), + buffer_id: BufferId(21), + client_id: None, + }), + ClientMessage::Buffer(BufferRequest::OpenHistory { + request_id: RequestId(157), + buffer_id: BufferId(21), + scope: BufferHistoryScope::Full, + placement: BufferHistoryPlacement::Tab, + client_id: Some(NonZeroU64::new(8).expect("non-zero client id")), + }), ClientMessage::Node(NodeRequest::GetTree { request_id: RequestId(16), session_id: SessionId(10), @@ -163,6 +197,79 @@ fn client_message_families_round_trip() { node_id: NodeId(35), sizes: vec![3, 2, 1], }), + ClientMessage::Node(NodeRequest::Zoom { + request_id: RequestId(241), + node_id: NodeId(35), + }), + ClientMessage::Node(NodeRequest::Unzoom { + request_id: RequestId(242), + session_id: SessionId(10), + }), + ClientMessage::Node(NodeRequest::ToggleZoom { + request_id: RequestId(243), + node_id: NodeId(35), + }), + ClientMessage::Node(NodeRequest::SwapSiblings { + request_id: RequestId(244), + first_node_id: NodeId(50), + second_node_id: NodeId(51), + }), + ClientMessage::Node(NodeRequest::BreakNode { + request_id: RequestId(245), + node_id: NodeId(35), + destination: NodeBreakDestination::Floating, + }), + ClientMessage::Node(NodeRequest::BreakNode { + request_id: RequestId(249), + node_id: NodeId(36), + destination: NodeBreakDestination::Tab, + }), + ClientMessage::Node(NodeRequest::JoinBufferAtNode { + request_id: RequestId(246), + node_id: NodeId(35), + buffer_id: BufferId(22), + placement: NodeJoinPlacement::TabAfter, + }), + ClientMessage::Node(NodeRequest::JoinBufferAtNode { + request_id: RequestId(250), + node_id: NodeId(36), + buffer_id: BufferId(23), + placement: NodeJoinPlacement::TabBefore, + }), + ClientMessage::Node(NodeRequest::JoinBufferAtNode { + request_id: RequestId(251), + node_id: NodeId(37), + buffer_id: BufferId(24), + placement: NodeJoinPlacement::Left, + }), + ClientMessage::Node(NodeRequest::JoinBufferAtNode { + request_id: RequestId(252), + node_id: NodeId(38), + buffer_id: BufferId(25), + placement: NodeJoinPlacement::Right, + }), + ClientMessage::Node(NodeRequest::JoinBufferAtNode { + request_id: RequestId(253), + node_id: NodeId(39), + buffer_id: BufferId(26), + placement: NodeJoinPlacement::Up, + }), + ClientMessage::Node(NodeRequest::JoinBufferAtNode { + request_id: RequestId(254), + node_id: NodeId(40), + buffer_id: BufferId(27), + placement: NodeJoinPlacement::Down, + }), + ClientMessage::Node(NodeRequest::MoveNodeBefore { + request_id: RequestId(247), + node_id: NodeId(35), + sibling_node_id: NodeId(36), + }), + ClientMessage::Node(NodeRequest::MoveNodeAfter { + request_id: RequestId(248), + node_id: NodeId(35), + sibling_node_id: NodeId(36), + }), ClientMessage::Floating(FloatingRequest::Create { request_id: RequestId(25), session_id: SessionId(10), @@ -217,8 +324,13 @@ fn client_message_families_round_trip() { #[test] fn server_envelope_families_round_trip() { let snapshot = sample_snapshot(); + let snapshot_without_zoom = sample_snapshot_without_zoom(); let session = snapshot.session.clone(); let buffers = snapshot.buffers.clone(); + let detached_buffer = BufferRecord { + attachment_node_id: None, + ..buffers[2].clone() + }; let floating = snapshot.floating.clone(); let envelopes = vec![ @@ -245,6 +357,10 @@ fn server_envelope_families_round_trip() { request_id: RequestId(34), snapshot: snapshot.clone(), })), + ServerEnvelope::Response(ServerResponse::SessionSnapshot(SessionSnapshotResponse { + request_id: RequestId(341), + snapshot: snapshot_without_zoom.clone(), + })), ServerEnvelope::Response(ServerResponse::Buffers(BuffersResponse { request_id: RequestId(35), buffers: buffers.clone(), @@ -253,6 +369,41 @@ fn server_envelope_families_round_trip() { request_id: RequestId(36), buffer: buffers[0].clone(), })), + ServerEnvelope::Response(ServerResponse::BufferWithLocation( + BufferWithLocationResponse::new( + RequestId(360), + buffers[1].clone(), + BufferLocation::floating(BufferId(12), SessionId(10), NodeId(23), FloatingId(70)), + false, + ) + .expect("buffer and location ids match"), + )), + ServerEnvelope::Response(ServerResponse::BufferWithLocation( + BufferWithLocationResponse::new( + RequestId(363), + detached_buffer, + BufferLocation::detached(buffers[2].id), + false, + ) + .expect("buffer and location ids match"), + )), + ServerEnvelope::Response(ServerResponse::BufferWithLocation( + BufferWithLocationResponse::new( + RequestId(364), + buffers[0].clone(), + BufferLocation::session(BufferId(11), SessionId(10), NodeId(21)), + false, + ) + .expect("buffer and location ids match"), + )), + ServerEnvelope::Response(ServerResponse::BufferLocation(BufferLocationResponse { + request_id: RequestId(361), + location: BufferLocation::session(BufferId(11), SessionId(10), NodeId(21)), + })), + ServerEnvelope::Response(ServerResponse::BufferLocation(BufferLocationResponse { + request_id: RequestId(362), + location: BufferLocation::detached(BufferId(13)), + })), ServerEnvelope::Response(ServerResponse::FloatingList(FloatingListResponse { request_id: RequestId(37), floating: floating.clone(), @@ -345,6 +496,7 @@ fn sample_snapshot() -> SessionSnapshot { floating_ids: vec![FloatingId(30)], focused_leaf_id: Some(NodeId(21)), focused_floating_id: Some(FloatingId(30)), + zoomed_node_id: Some(NodeId(24)), }, nodes: vec![ NodeRecord { @@ -434,6 +586,7 @@ fn sample_snapshot() -> SessionSnapshot { BufferRecordState::Running, ActivityState::Activity, None, + false, ), sample_buffer_record( BufferId(12), @@ -441,6 +594,7 @@ fn sample_snapshot() -> SessionSnapshot { BufferRecordState::Exited, ActivityState::Bell, Some(0), + false, ), sample_buffer_record( BufferId(13), @@ -448,6 +602,7 @@ fn sample_snapshot() -> SessionSnapshot { BufferRecordState::Created, ActivityState::Idle, None, + true, ), ], floating: vec![FloatingRecord { @@ -463,21 +618,47 @@ fn sample_snapshot() -> SessionSnapshot { } } +fn sample_snapshot_without_zoom() -> SessionSnapshot { + let mut snapshot = sample_snapshot(); + snapshot.session.zoomed_node_id = None; + for node in &mut snapshot.nodes { + if let Some(buffer_view) = node.buffer_view.as_mut() { + buffer_view.zoomed = false; + } + } + snapshot +} + fn sample_buffer_record( id: BufferId, attachment_node_id: Option, state: BufferRecordState, activity: ActivityState, exit_code: Option, + is_helper: bool, ) -> BufferRecord { + let (kind, read_only, helper_source_buffer_id, helper_scope) = if is_helper { + ( + BufferRecordKind::Helper, + true, + Some(BufferId(12)), + Some(BufferHistoryScope::Visible), + ) + } else { + (BufferRecordKind::Pty, false, None, None) + }; BufferRecord { id, title: format!("buffer-{id}"), command: vec!["bash".to_owned(), "-lc".to_owned(), "echo mux".to_owned()], cwd: Some("/tmp".to_owned()), + kind, state, pid: Some(4242), attachment_node_id, + read_only, + helper_source_buffer_id, + helper_scope, pty_size: PtySize::new(120, 40), activity, last_snapshot_seq: 9, diff --git a/crates/embers-server/Cargo.toml b/crates/embers-server/Cargo.toml index 6960cf3..7ac0a85 100644 --- a/crates/embers-server/Cargo.toml +++ b/crates/embers-server/Cargo.toml @@ -6,6 +6,9 @@ license.workspace = true rust-version.workspace = true version.workspace = true +[lib] +doctest = false + [dependencies] alacritty_terminal = "0.25.1" base64.workspace = true diff --git a/crates/embers-server/src/buffer_runtime.rs b/crates/embers-server/src/buffer_runtime.rs index 4acda30..e901a29 100644 --- a/crates/embers-server/src/buffer_runtime.rs +++ b/crates/embers-server/src/buffer_runtime.rs @@ -367,19 +367,16 @@ impl BufferRuntimeHandle { impl BufferRuntimeInner { fn join_threads_blocking(&self) { self.stop.store(true, Ordering::Relaxed); - let mut threads = match self.threads.lock() { - Ok(threads) => threads, + let poller = match self.threads.lock() { + Ok(mut threads) => threads.poller.take(), Err(poisoned) => { error!( %self.buffer_id, "buffer runtime thread registry lock poisoned during shutdown" ); - poisoned.into_inner() + poisoned.into_inner().poller.take() } }; - let poller = threads.poller.take(); - drop(threads); - if let Some(poller) = poller && poller.thread().id() != thread::current().id() { @@ -528,6 +525,12 @@ impl KeeperSurface { } } +/// Maximum retries for PTY allocation in runtime keeper +const KEEPER_PTY_MAX_RETRIES: usize = 3; + +/// Delay between PTY allocation retries +const KEEPER_PTY_RETRY_DELAY: Duration = Duration::from_millis(100); + pub fn run_runtime_keeper(cli: RuntimeKeeperCli) -> Result<()> { let Some(program) = cli.command.first() else { return Err(MuxError::invalid_input( @@ -545,9 +548,36 @@ pub fn run_runtime_keeper(cli: RuntimeKeeperCli) -> Result<()> { let _cleanup = SocketCleanup::new(cli.socket_path.clone()); let pty_system = NativePtySystem::default(); - let pair = pty_system - .openpty(to_portable_size(cli.size)) - .map_err(|error| MuxError::pty(error.to_string()))?; + let mut last_error = None; + + // Try to open PTY with retries. + let mut pair = None; + for attempt in 0..=KEEPER_PTY_MAX_RETRIES { + match pty_system.openpty(to_portable_size(cli.size)) { + Ok(opened_pair) => { + pair = Some(opened_pair); + break; + } + Err(error) => { + last_error = Some(error); + if attempt < KEEPER_PTY_MAX_RETRIES { + thread::sleep(KEEPER_PTY_RETRY_DELAY * 2_u32.pow(attempt as u32)); + } + } + } + } + let pair = match pair { + Some(pair) => pair, + None => { + // Safe: each failed retry stores the PTY allocation error before continuing. + let error = + last_error.expect("openpty retry loop must capture an error before failing"); + return Err(MuxError::pty(format!( + "failed to openpty after {} attempts: {error}", + KEEPER_PTY_MAX_RETRIES + 1 + ))); + } + }; let mut command_builder = CommandBuilder::new(program); command_builder.args(&cli.command[1..]); @@ -677,6 +707,18 @@ fn handle_keeper_request( } impl KeeperRuntime { + fn ensure_running(&self) -> Result<()> { + if self + .exit_code + .lock() + .map_err(|_| MuxError::internal("runtime keeper exit lock poisoned"))? + .is_some() + { + return Err(MuxError::conflict("buffer runtime has already exited")); + } + Ok(()) + } + fn status(&self) -> Result { let exit_code = *self .exit_code @@ -703,14 +745,7 @@ impl KeeperRuntime { } fn write(&self, bytes: Vec) -> Result<()> { - if self - .exit_code - .lock() - .map_err(|_| MuxError::internal("runtime keeper exit lock poisoned"))? - .is_some() - { - return Err(MuxError::conflict("buffer runtime has already exited")); - } + self.ensure_running()?; let mut writer = self .writer .lock() @@ -721,6 +756,7 @@ impl KeeperRuntime { } fn resize(&self, size: PtySize) -> Result<()> { + self.ensure_running()?; let master = self .master .lock() @@ -766,6 +802,7 @@ impl KeeperRuntime { } fn kill(&self) -> Result<()> { + self.ensure_running()?; let mut killer = self .killer .lock() diff --git a/crates/embers-server/src/model.rs b/crates/embers-server/src/model.rs index dffa4d5..56dc91b 100644 --- a/crates/embers-server/src/model.rs +++ b/crates/embers-server/src/model.rs @@ -15,9 +15,30 @@ pub struct Session { pub floating: Vec, pub focused_leaf: Option, pub focused_floating: Option, + pub zoomed_node: Option, pub created_at: Timestamp, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HelperBuffer { + pub source_buffer_id: BufferId, + pub scope: HelperBufferScope, + pub lines: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HelperBufferScope { + Full, + Visible, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum BufferKind { + #[default] + Pty, + Helper(HelperBuffer), +} + #[derive(Clone)] pub struct Buffer { pub id: BufferId, @@ -31,6 +52,7 @@ pub struct Buffer { pub pty_size: PtySize, pub activity: ActivityState, pub last_snapshot_seq: u64, + pub kind: BufferKind, pub created_at: Timestamp, } @@ -54,6 +76,7 @@ impl Buffer { pty_size: PtySize::new(80, 24), activity: ActivityState::Idle, last_snapshot_seq: 0, + kind: BufferKind::Pty, created_at: Timestamp::now(), } } @@ -81,6 +104,7 @@ impl fmt::Debug for Buffer { .field("pty_size", &self.pty_size) .field("activity", &self.activity) .field("last_snapshot_seq", &self.last_snapshot_seq) + .field("kind", &self.kind) .field("created_at", &self.created_at) .finish() } @@ -98,6 +122,7 @@ impl PartialEq for Buffer { && self.pty_size == other.pty_size && self.activity == other.activity && self.last_snapshot_seq == other.last_snapshot_seq + && self.kind == other.kind && self.created_at == other.created_at } } diff --git a/crates/embers-server/src/persist.rs b/crates/embers-server/src/persist.rs index 302abb7..8c99e5c 100644 --- a/crates/embers-server/src/persist.rs +++ b/crates/embers-server/src/persist.rs @@ -14,13 +14,15 @@ use embers_core::{ use serde::{Deserialize, Serialize}; use crate::model::{ - Buffer, BufferAttachment, BufferState, BufferViewNode, BufferViewState, ExitedBuffer, - FloatingWindow, InterruptedBuffer, Node, Session, SplitNode, TabEntry, TabsNode, + Buffer, BufferAttachment, BufferKind, BufferState, BufferViewNode, BufferViewState, + ExitedBuffer, FloatingWindow, HelperBuffer, HelperBufferScope, InterruptedBuffer, Node, + Session, SplitNode, TabEntry, TabsNode, }; use crate::state::ServerState; const LEGACY_FORMAT_VERSION: u32 = 0; -pub const CURRENT_FORMAT_VERSION: u32 = 1; +const FIRST_VERSIONED_FORMAT_VERSION: u32 = 1; +pub const CURRENT_FORMAT_VERSION: u32 = 2; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PersistedWorkspace { @@ -44,6 +46,8 @@ pub struct PersistedSession { pub floating: Vec, pub focused_leaf: Option, pub focused_floating: Option, + #[serde(default)] + pub zoomed_node: Option, pub created_at_ms: u64, } @@ -61,9 +65,31 @@ pub struct PersistedBuffer { pub pty_size: PtySize, pub activity: PersistedActivityState, pub last_snapshot_seq: u64, + #[serde(default)] + pub kind: PersistedBufferKind, pub created_at_ms: u64, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum PersistedBufferKind { + #[default] + Pty, + Helper { + source_buffer_id: u64, + scope: PersistedHelperBufferScope, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + lines: Vec, + }, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum PersistedHelperBufferScope { + Full, + Visible, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum PersistedBufferState { @@ -256,7 +282,36 @@ fn migrate_workspace( version: u32, ) -> Result { match version { - LEGACY_FORMAT_VERSION => { + LEGACY_FORMAT_VERSION | FIRST_VERSIONED_FORMAT_VERSION => { + let mut legacy_zoomed_nodes = BTreeMap::>::new(); + for node in &workspace.nodes { + if let PersistedNode::BufferView { + id, + session_id, + zoomed: true, + .. + } = node + { + legacy_zoomed_nodes + .entry(*session_id) + .or_default() + .push(*id); + } + } + for session in &mut workspace.sessions { + if session.zoomed_node.is_none() { + match legacy_zoomed_nodes.get(&session.id).map(Vec::as_slice) { + Some([]) | None => {} + Some([node_id]) => session.zoomed_node = Some(*node_id), + Some(node_ids) => { + return Err(MuxError::internal(format!( + "workspace session {} has multiple legacy zoomed nodes: {:?}", + session.id, node_ids + ))); + } + } + } + } workspace.format_version = Some(CURRENT_FORMAT_VERSION); Ok(workspace) } @@ -274,6 +329,7 @@ pub fn persisted_session(session: &Session) -> PersistedSession { floating: session.floating.iter().map(|id| id.0).collect(), focused_leaf: session.focused_leaf.map(|id| id.0), focused_floating: session.focused_floating.map(|id| id.0), + zoomed_node: session.zoomed_node.map(|id| id.0), created_at_ms: timestamp_to_millis(session.created_at), } } @@ -286,6 +342,7 @@ pub fn restored_session(session: PersistedSession) -> Result { floating: session.floating.into_iter().map(FloatingId).collect(), focused_leaf: session.focused_leaf.map(NodeId), focused_floating: session.focused_floating.map(FloatingId), + zoomed_node: session.zoomed_node.map(NodeId), created_at: timestamp_from_millis(session.created_at_ms)?, }) } @@ -303,6 +360,7 @@ pub fn persisted_buffer(buffer: &Buffer) -> PersistedBuffer { pty_size: buffer.pty_size, activity: persisted_activity(buffer.activity), last_snapshot_seq: buffer.last_snapshot_seq, + kind: persisted_buffer_kind(&buffer.kind), created_at_ms: timestamp_to_millis(buffer.created_at), } } @@ -321,6 +379,7 @@ pub fn restored_buffer(buffer: PersistedBuffer) -> Result { restored.pty_size = buffer.pty_size; restored.activity = restored_activity(buffer.activity); restored.last_snapshot_seq = buffer.last_snapshot_seq; + restored.kind = restored_buffer_kind(buffer.kind); restored.created_at = timestamp_from_millis(buffer.created_at_ms)?; Ok(restored) } @@ -537,6 +596,46 @@ fn restored_activity(activity: PersistedActivityState) -> ActivityState { } } +fn persisted_buffer_kind(kind: &BufferKind) -> PersistedBufferKind { + match kind { + BufferKind::Pty => PersistedBufferKind::Pty, + BufferKind::Helper(helper) => PersistedBufferKind::Helper { + source_buffer_id: helper.source_buffer_id.0, + scope: persisted_helper_scope(helper.scope), + lines: Vec::new(), + }, + } +} + +fn restored_buffer_kind(kind: PersistedBufferKind) -> BufferKind { + match kind { + PersistedBufferKind::Pty => BufferKind::Pty, + PersistedBufferKind::Helper { + source_buffer_id, + scope, + lines, + } => BufferKind::Helper(HelperBuffer { + source_buffer_id: BufferId(source_buffer_id), + scope: restored_helper_scope(scope), + lines, + }), + } +} + +fn persisted_helper_scope(scope: HelperBufferScope) -> PersistedHelperBufferScope { + match scope { + HelperBufferScope::Full => PersistedHelperBufferScope::Full, + HelperBufferScope::Visible => PersistedHelperBufferScope::Visible, + } +} + +fn restored_helper_scope(scope: PersistedHelperBufferScope) -> HelperBufferScope { + match scope { + PersistedHelperBufferScope::Full => HelperBufferScope::Full, + PersistedHelperBufferScope::Visible => HelperBufferScope::Visible, + } +} + fn timestamp_to_millis(timestamp: Timestamp) -> u64 { timestamp .0 @@ -592,6 +691,78 @@ mod tests { assert_eq!(migrated.format_version, Some(CURRENT_FORMAT_VERSION)); } + #[test] + fn load_current_workspace_migrates_v1_workspace() { + let workspace: PersistedWorkspace = serde_json::from_str( + r#" + { + "format_version": 1, + "sessions": [ + { + "id": 1, + "name": "alpha", + "root_node": 10, + "floating": [], + "focused_leaf": 10, + "focused_floating": null, + "created_at_ms": 1234 + } + ], + "buffers": [ + { + "id": 20, + "title": "shell", + "command": ["sh"], + "cwd": null, + "env": {}, + "runtime_socket_path": null, + "state": { "kind": "created" }, + "attachment": { "kind": "detached" }, + "pty_size": { + "cols": 80, + "rows": 24, + "pixel_width": 0, + "pixel_height": 0 + }, + "activity": "idle", + "last_snapshot_seq": 0, + "created_at_ms": 5678 + } + ], + "nodes": [ + { + "kind": "buffer_view", + "id": 10, + "session_id": 1, + "parent": null, + "buffer_id": 20, + "focused": true, + "zoomed": true, + "follow_output": true, + "last_render_size": { + "cols": 80, + "rows": 24, + "pixel_width": 0, + "pixel_height": 0 + } + } + ], + "floating": [], + "next_session_id": 2, + "next_buffer_id": 21, + "next_node_id": 11, + "next_floating_id": 1 + } + "#, + ) + .expect("deserialize v1 workspace fixture"); + + let migrated = load_current_workspace(workspace).expect("v1 workspace migrates"); + assert_eq!(migrated.format_version, Some(CURRENT_FORMAT_VERSION)); + assert_eq!(migrated.sessions[0].zoomed_node, Some(10)); + assert_eq!(migrated.buffers[0].kind, PersistedBufferKind::Pty); + } + #[test] fn load_current_workspace_rejects_unknown_format_versions() { let workspace = PersistedWorkspace { @@ -616,6 +787,108 @@ mod tests { ); } + #[test] + fn helper_buffer_kind_round_trips_through_persistence() { + for scope in [HelperBufferScope::Full, HelperBufferScope::Visible] { + let kind = BufferKind::Helper(HelperBuffer { + source_buffer_id: BufferId(42), + scope, + lines: vec!["alpha".to_owned(), "beta".to_owned()], + }); + + let persisted = persisted_buffer_kind(&kind); + let restored = restored_buffer_kind(persisted.clone()); + + assert_eq!( + restored, + BufferKind::Helper(HelperBuffer { + source_buffer_id: BufferId(42), + scope, + lines: Vec::new(), + }) + ); + assert_eq!(restored_helper_scope(persisted_helper_scope(scope)), scope); + assert_eq!( + persisted, + PersistedBufferKind::Helper { + source_buffer_id: 42, + scope: persisted_helper_scope(scope), + lines: Vec::new(), + } + ); + } + } + + #[test] + fn helper_buffer_kind_restores_legacy_lines_when_present() { + let restored = restored_buffer_kind(PersistedBufferKind::Helper { + source_buffer_id: 42, + scope: PersistedHelperBufferScope::Visible, + lines: vec!["alpha".to_owned(), "beta".to_owned()], + }); + + assert_eq!( + restored, + BufferKind::Helper(HelperBuffer { + source_buffer_id: BufferId(42), + scope: HelperBufferScope::Visible, + lines: vec!["alpha".to_owned(), "beta".to_owned()], + }) + ); + } + + #[test] + fn load_current_workspace_rejects_duplicate_legacy_zoomed_nodes() { + let workspace = PersistedWorkspace { + format_version: Some(FIRST_VERSIONED_FORMAT_VERSION), + sessions: vec![PersistedSession { + id: 1, + name: "alpha".to_owned(), + root_node: 10, + floating: Vec::new(), + focused_leaf: Some(10), + focused_floating: None, + zoomed_node: None, + created_at_ms: 1234, + }], + buffers: Vec::new(), + nodes: vec![ + PersistedNode::BufferView { + id: 10, + session_id: 1, + parent: None, + buffer_id: 20, + focused: true, + zoomed: true, + follow_output: true, + last_render_size: PtySize::new(80, 24), + }, + PersistedNode::BufferView { + id: 11, + session_id: 1, + parent: None, + buffer_id: 21, + focused: false, + zoomed: true, + follow_output: true, + last_render_size: PtySize::new(80, 24), + }, + ], + floating: Vec::new(), + next_session_id: 2, + next_buffer_id: 22, + next_node_id: 12, + next_floating_id: 1, + }; + + let error = load_current_workspace(workspace) + .expect_err("duplicate legacy zoomed nodes should be rejected"); + assert!( + error.to_string().contains("multiple legacy zoomed nodes"), + "unexpected error: {error}" + ); + } + #[test] fn restored_running_buffers_become_interrupted() { let state = restored_buffer_state(PersistedBufferState::Running { pid: Some(42) }) diff --git a/crates/embers-server/src/protocol.rs b/crates/embers-server/src/protocol.rs index e907b88..b2916a0 100644 --- a/crates/embers-server/src/protocol.rs +++ b/crates/embers-server/src/protocol.rs @@ -1,10 +1,14 @@ use embers_core::{MuxError, Result}; use embers_protocol::{ - BufferRecord, BufferRecordState, BufferViewRecord, FloatingRecord, NodeRecord, NodeRecordKind, - SessionRecord, SessionSnapshot, SplitRecord, TabRecord, TabsRecord, + BufferHistoryScope, BufferLocation, BufferRecord, BufferRecordKind, BufferRecordState, + BufferViewRecord, FloatingRecord, NodeRecord, NodeRecordKind, SessionRecord, SessionSnapshot, + SplitRecord, TabRecord, TabsRecord, }; -use crate::model::{Buffer, BufferAttachment, BufferState, FloatingWindow, Node, Session}; +use crate::model::{ + Buffer, BufferAttachment, BufferKind, BufferState, FloatingWindow, HelperBufferScope, Node, + Session, +}; use crate::state::ServerState; pub fn session_record(session: &Session) -> SessionRecord { @@ -15,6 +19,7 @@ pub fn session_record(session: &Session) -> SessionRecord { floating_ids: session.floating.clone(), focused_leaf_id: session.focused_leaf, focused_floating_id: session.focused_floating, + zoomed_node_id: session.zoomed_node, } } @@ -29,6 +34,18 @@ pub fn buffer_record(buffer: &Buffer) -> BufferRecord { ), BufferState::Exited(exited) => (BufferRecordState::Exited, None, exited.exit_code), }; + let (kind, read_only, helper_source_buffer_id, helper_scope) = match &buffer.kind { + BufferKind::Pty => (BufferRecordKind::Pty, false, None, None), + BufferKind::Helper(helper) => ( + BufferRecordKind::Helper, + true, + Some(helper.source_buffer_id), + Some(match helper.scope { + HelperBufferScope::Full => BufferHistoryScope::Full, + HelperBufferScope::Visible => BufferHistoryScope::Visible, + }), + ), + }; BufferRecord { id: buffer.id, @@ -38,12 +55,16 @@ pub fn buffer_record(buffer: &Buffer) -> BufferRecord { .cwd .as_ref() .map(|path| path.to_string_lossy().into_owned()), + kind, state, pid, attachment_node_id: match buffer.attachment { BufferAttachment::Attached(node_id) => Some(node_id), BufferAttachment::Detached => None, }, + read_only, + helper_source_buffer_id, + helper_scope, pty_size: buffer.pty_size, activity: buffer.activity, last_snapshot_seq: buffer.last_snapshot_seq, @@ -52,6 +73,24 @@ pub fn buffer_record(buffer: &Buffer) -> BufferRecord { } } +pub fn buffer_location( + state: &ServerState, + buffer_id: embers_core::BufferId, +) -> Result { + let buffer = state.buffer(buffer_id)?; + let node_id = match buffer.attachment { + BufferAttachment::Attached(node_id) => node_id, + BufferAttachment::Detached => return Ok(BufferLocation::detached(buffer_id)), + }; + let session_id = state.node(node_id)?.session_id(); + let floating_id = state.floating_id_for_node(node_id)?; + + Ok(match floating_id { + Some(floating_id) => BufferLocation::floating(buffer_id, session_id, node_id, floating_id), + None => BufferLocation::session(buffer_id, session_id, node_id), + }) +} + pub fn node_record(node: &Node) -> NodeRecord { match node { Node::BufferView(view) => NodeRecord { diff --git a/crates/embers-server/src/server.rs b/crates/embers-server/src/server.rs index 04e9c4e..0f930f7 100644 --- a/crates/embers-server/src/server.rs +++ b/crates/embers-server/src/server.rs @@ -13,24 +13,29 @@ use embers_core::{ BufferId, ErrorCode, MuxError, PtySize, RequestId, Result, WireError, request_span, }; use embers_protocol::{ - BufferCreatedEvent, BufferDetachedEvent, BufferRequest, BufferResponse, BuffersResponse, - ClientChangedEvent, ClientMessage, ClientRecord, ClientRequest, ClientResponse, - ClientsResponse, ErrorResponse, FloatingChangedEvent, FloatingRequest, FloatingResponse, - FocusChangedEvent, FrameType, InputRequest, NodeChangedEvent, OkResponse, PingResponse, - ProtocolError, RawFrame, RenderInvalidatedEvent, ScrollbackSliceResponse, ServerEnvelope, - ServerEvent, ServerResponse, SessionClosedEvent, SessionCreatedEvent, SessionRenamedEvent, - SessionRequest, SessionSnapshotResponse, SessionsResponse, SnapshotResponse, - SubscriptionAckResponse, VisibleSnapshotResponse, decode_client_message, - encode_server_envelope, read_frame, write_frame_no_flush, + BufferCreatedEvent, BufferDetachedEvent, BufferHistoryPlacement, BufferHistoryScope, + BufferLocation, BufferLocationAttachment, BufferLocationResponse, BufferRequest, + BufferResponse, BufferWithLocationResponse, BuffersResponse, ClientChangedEvent, ClientMessage, + ClientRecord, ClientRequest, ClientResponse, ClientsResponse, ErrorResponse, + FloatingChangedEvent, FloatingRequest, FloatingResponse, FocusChangedEvent, FrameType, + InputRequest, NodeChangedEvent, OkResponse, PingResponse, ProtocolError, RawFrame, + RenderInvalidatedEvent, ScrollbackSliceResponse, ServerEnvelope, ServerEvent, ServerResponse, + SessionClosedEvent, SessionCreatedEvent, SessionRenamedEvent, SessionRequest, + SessionSnapshotResponse, SessionsResponse, SnapshotResponse, SubscriptionAckResponse, + VisibleSnapshotResponse, decode_client_message, encode_server_envelope, read_frame, + write_frame_no_flush, }; use tokio::net::UnixListener; use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf}; use tokio::sync::{Mutex, Notify, mpsc, oneshot, watch}; use tokio::task::JoinHandle; -use tracing::{debug, error, info}; +use tracing::{Instrument, debug, error, info}; +use crate::model::{BufferKind, HelperBufferScope, Node}; use crate::persist::{load_workspace, save_workspace}; -use crate::protocol::{buffer_record, floating_record, session_record, session_snapshot}; +use crate::protocol::{ + buffer_location, buffer_record, floating_record, session_record, session_snapshot, +}; use crate::{ BufferAttachment, BufferRuntimeCallbacks, BufferRuntimeHandle, BufferRuntimeStatus, BufferRuntimeUpdate, BufferState, ServerConfig, ServerState, TabEntry, @@ -531,7 +536,7 @@ impl Runtime { (resp, events, None) } ClientMessage::Buffer(request) => { - let (resp, events) = self.dispatch_buffer(request).await; + let (resp, events) = self.dispatch_buffer(connection_id, request).await; (resp, events, None) } ClientMessage::Node(request) => { @@ -855,6 +860,7 @@ impl Runtime { async fn dispatch_buffer( self: &Arc, + connection_id: u64, request: BufferRequest, ) -> (ServerResponse, Vec) { match request { @@ -977,6 +983,35 @@ impl Runtime { ), Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), }, + BufferRequest::Inspect { + request_id, + buffer_id, + } => { + let state = self.state.lock().await; + match state.buffer(buffer_id) { + Ok(buffer) => match buffer_location(&state, buffer_id).and_then(|location| { + location_is_root_tab(&state, &location) + .map(|at_root_tab| (location, at_root_tab)) + }) { + Ok((location, at_root_tab)) => match BufferWithLocationResponse::new( + request_id, + buffer_record(buffer), + location, + at_root_tab, + ) { + Ok(response) => { + (ServerResponse::BufferWithLocation(response), Vec::new()) + } + Err(error) => ( + mux_error_response(Some(request_id), MuxError::protocol(error)), + Vec::new(), + ), + }, + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + }, + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + } + } BufferRequest::Detach { request_id, buffer_id, @@ -1048,6 +1083,120 @@ impl Runtime { Ok(snapshot) => (ServerResponse::ScrollbackSlice(snapshot), Vec::new()), Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), }, + BufferRequest::GetLocation { + request_id, + buffer_id, + } => { + let state = self.state.lock().await; + match buffer_location(&state, buffer_id) { + Ok(location) => ( + ServerResponse::BufferLocation(BufferLocationResponse { + request_id, + location, + }), + Vec::new(), + ), + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + } + } + BufferRequest::Reveal { + request_id, + buffer_id, + client_id, + } => match self + .reveal_buffer( + connection_id, + client_id.map(std::num::NonZeroU64::get), + buffer_id, + ) + .await + { + Ok((location, events)) => ( + ServerResponse::BufferLocation(BufferLocationResponse { + request_id, + location, + }), + events, + ), + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + }, + BufferRequest::OpenHistory { + request_id, + buffer_id, + scope, + placement, + client_id, + } => match self + .open_history_buffer( + connection_id, + client_id.map(std::num::NonZeroU64::get), + buffer_id, + scope, + placement, + ) + .await + { + Ok((location, mut reveal_events)) => { + let mut events = Vec::new(); + let (buffer, at_root_tab) = { + let state = self.state.lock().await; + match state.buffer(location.buffer_id) { + Ok(buffer) => match location_is_root_tab(&state, &location) { + Ok(at_root_tab) => (buffer_record(buffer), at_root_tab), + Err(error) => { + return ( + mux_error_response(Some(request_id), error), + Vec::new(), + ); + } + }, + Err(error) => { + return (mux_error_response(Some(request_id), error), Vec::new()); + } + } + }; + events.push(ServerEvent::BufferCreated(BufferCreatedEvent { + buffer: buffer.clone(), + })); + match &location.attachment { + BufferLocationAttachment::Floating { + session_id, + floating_id, + .. + } => { + events.push(ServerEvent::FloatingChanged(FloatingChangedEvent { + session_id: *session_id, + floating_id: Some(*floating_id), + })); + } + BufferLocationAttachment::Session { session_id, .. } + if !reveal_events.iter().any(|event| { + matches!( + event, + ServerEvent::NodeChanged(NodeChangedEvent { session_id: changed }) + if *changed == *session_id + ) + }) => + { + events.push(ServerEvent::NodeChanged(NodeChangedEvent { + session_id: *session_id, + })); + } + BufferLocationAttachment::Detached + | BufferLocationAttachment::Session { .. } => {} + } + events.append(&mut reveal_events); + match BufferWithLocationResponse::new(request_id, buffer, location, at_root_tab) + { + Ok(response) => (ServerResponse::BufferWithLocation(response), events), + Err(error) => ( + mux_error_response(Some(request_id), MuxError::protocol(error)), + Vec::new(), + ), + } + } + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + }, } } @@ -1060,40 +1209,52 @@ impl Runtime { request_id, buffer_id, bytes, - } => match self.buffer_runtime(buffer_id).await { - Ok(runtime) => match runtime.write(bytes).await { - Ok(()) => (ServerResponse::Ok(OkResponse { request_id }), Vec::new()), + } => { + if let Err(error) = self.ensure_buffer_accepts_input(buffer_id).await { + return (mux_error_response(Some(request_id), error), Vec::new()); + } + match self.buffer_runtime(buffer_id).await { + Ok(runtime) => match runtime.write(bytes).await { + Ok(()) => (ServerResponse::Ok(OkResponse { request_id }), Vec::new()), + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + }, Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), - }, - Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), - }, + } + } InputRequest::Resize { request_id, buffer_id, cols, rows, } => { - let runtime = match self.buffer_runtime(buffer_id).await { - Ok(runtime) => runtime, - Err(error) => return (mux_error_response(Some(request_id), error), Vec::new()), - }; - let size = { + let (size, is_helper) = { let state = self.state.lock().await; match state.buffer(buffer_id) { - Ok(buffer) => PtySize { - cols, - rows, - pixel_width: buffer.pty_size.pixel_width, - pixel_height: buffer.pty_size.pixel_height, - }, + Ok(buffer) => ( + PtySize { + cols, + rows, + pixel_width: buffer.pty_size.pixel_width, + pixel_height: buffer.pty_size.pixel_height, + }, + matches!(&buffer.kind, BufferKind::Helper(_)), + ), Err(error) => { return (mux_error_response(Some(request_id), error), Vec::new()); } } }; - if let Err(error) = runtime.resize(size).await { - return (mux_error_response(Some(request_id), error), Vec::new()); + if !is_helper { + let runtime = match self.buffer_runtime(buffer_id).await { + Ok(runtime) => runtime, + Err(error) => { + return (mux_error_response(Some(request_id), error), Vec::new()); + } + }; + if let Err(error) = runtime.resize(size).await { + return (mux_error_response(Some(request_id), error), Vec::new()); + } } { @@ -1429,6 +1590,198 @@ impl Runtime { } layout_snapshot_response(&state, request_id, session_id) } + embers_protocol::NodeRequest::Zoom { + request_id, + node_id, + } => { + let session_id = match state.node(node_id) { + Ok(node) => node.session_id(), + Err(error) => return (mux_error_response(Some(request_id), error), Vec::new()), + }; + match state.zoom_node(node_id) { + Ok(()) => layout_ok_response(&state, request_id, session_id), + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + } + } + embers_protocol::NodeRequest::Unzoom { + request_id, + session_id, + } => match state.unzoom_session(session_id) { + Ok(()) => layout_ok_response(&state, request_id, session_id), + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + }, + embers_protocol::NodeRequest::ToggleZoom { + request_id, + node_id, + } => { + let session_id = match state.node(node_id) { + Ok(node) => node.session_id(), + Err(error) => return (mux_error_response(Some(request_id), error), Vec::new()), + }; + match state.toggle_zoom_node(node_id) { + Ok(()) => layout_ok_response(&state, request_id, session_id), + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + } + } + embers_protocol::NodeRequest::SwapSiblings { + request_id, + first_node_id, + second_node_id, + } => { + let session_id = match state.node(first_node_id) { + Ok(node) => node.session_id(), + Err(error) => return (mux_error_response(Some(request_id), error), Vec::new()), + }; + match state.swap_sibling_nodes(first_node_id, second_node_id) { + Ok(()) => { + let current_floating_id = match state.floating_id_for_node(first_node_id) { + Ok(floating_id) => floating_id, + Err(error) => { + return (mux_error_response(Some(request_id), error), Vec::new()); + } + }; + let (response, mut events) = + layout_ok_response(&state, request_id, session_id); + if current_floating_id.is_some() { + events.push(ServerEvent::FloatingChanged(FloatingChangedEvent { + session_id, + floating_id: current_floating_id, + })); + } + (response, events) + } + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + } + } + embers_protocol::NodeRequest::BreakNode { + request_id, + node_id, + destination, + } => { + let session_id = match state.node(node_id) { + Ok(node) => node.session_id(), + Err(error) => return (mux_error_response(Some(request_id), error), Vec::new()), + }; + let into_floating = + matches!(destination, embers_protocol::NodeBreakDestination::Floating); + let previous_floating_id = match state.floating_id_for_node(node_id) { + Ok(floating_id) => floating_id, + Err(error) => return (mux_error_response(Some(request_id), error), Vec::new()), + }; + match state.break_node(node_id, into_floating) { + Ok(()) => { + let current_floating_id = match state.floating_id_for_node(node_id) { + Ok(floating_id) => floating_id, + Err(error) => { + return (mux_error_response(Some(request_id), error), Vec::new()); + } + }; + let (response, mut events) = + layout_ok_response(&state, request_id, session_id); + if previous_floating_id != current_floating_id + || current_floating_id.is_some() + { + events.push(ServerEvent::FloatingChanged(FloatingChangedEvent { + session_id, + floating_id: current_floating_id, + })); + } + (response, events) + } + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + } + } + embers_protocol::NodeRequest::JoinBufferAtNode { + request_id, + node_id, + buffer_id, + placement, + } => { + let session_id = match state.node(node_id) { + Ok(node) => node.session_id(), + Err(error) => return (mux_error_response(Some(request_id), error), Vec::new()), + }; + match state.join_buffer_at_node(node_id, buffer_id, placement) { + Ok(()) => { + let current_floating_id = match state.floating_id_for_node(node_id) { + Ok(floating_id) => floating_id, + Err(error) => { + return (mux_error_response(Some(request_id), error), Vec::new()); + } + }; + let (response, mut events) = + layout_ok_response(&state, request_id, session_id); + if current_floating_id.is_some() { + events.push(ServerEvent::FloatingChanged(FloatingChangedEvent { + session_id, + floating_id: current_floating_id, + })); + } + (response, events) + } + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + } + } + embers_protocol::NodeRequest::MoveNodeBefore { + request_id, + node_id, + sibling_node_id, + } => { + let session_id = match state.node(node_id) { + Ok(node) => node.session_id(), + Err(error) => return (mux_error_response(Some(request_id), error), Vec::new()), + }; + match state.move_node_before(node_id, sibling_node_id) { + Ok(()) => { + let current_floating_id = match state.floating_id_for_node(node_id) { + Ok(floating_id) => floating_id, + Err(error) => { + return (mux_error_response(Some(request_id), error), Vec::new()); + } + }; + let (response, mut events) = + layout_ok_response(&state, request_id, session_id); + if current_floating_id.is_some() { + events.push(ServerEvent::FloatingChanged(FloatingChangedEvent { + session_id, + floating_id: current_floating_id, + })); + } + (response, events) + } + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + } + } + embers_protocol::NodeRequest::MoveNodeAfter { + request_id, + node_id, + sibling_node_id, + } => { + let session_id = match state.node(node_id) { + Ok(node) => node.session_id(), + Err(error) => return (mux_error_response(Some(request_id), error), Vec::new()), + }; + match state.move_node_after(node_id, sibling_node_id) { + Ok(()) => { + let current_floating_id = match state.floating_id_for_node(node_id) { + Ok(floating_id) => floating_id, + Err(error) => { + return (mux_error_response(Some(request_id), error), Vec::new()); + } + }; + let (response, mut events) = + layout_ok_response(&state, request_id, session_id); + if current_floating_id.is_some() { + events.push(ServerEvent::FloatingChanged(FloatingChangedEvent { + session_id, + floating_id: current_floating_id, + })); + } + (response, events) + } + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + } + } } } @@ -1644,17 +1997,382 @@ impl Runtime { } } + async fn ensure_buffer_accepts_input(&self, buffer_id: BufferId) -> Result<()> { + let state = self.state.lock().await; + let buffer = state.buffer(buffer_id)?; + if matches!(&buffer.kind, BufferKind::Helper(_)) { + return Err(MuxError::conflict(format!( + "buffer {buffer_id} is read-only" + ))); + } + Ok(()) + } + + async fn resolve_reveal_client_id( + &self, + connection_id: u64, + requested: Option, + ) -> Result { + if let Some(client_id) = requested { + if self.clients.lock().await.contains_key(&client_id) { + return Ok(client_id); + } + return Err(MuxError::not_found(format!("unknown client {client_id}"))); + } + + let clients = self.clients.lock().await; + if clients + .get(&connection_id) + .is_some_and(|client| client.current_session_id.is_some()) + { + return Ok(connection_id); + } + + let mut attached_clients = clients + .iter() + .filter_map(|(client_id, client)| client.current_session_id.map(|_| *client_id)); + match (attached_clients.next(), attached_clients.next()) { + (Some(client_id), None) => Ok(client_id), + (None, _) => Err(MuxError::conflict( + "no interactive client is currently attached", + )), + (Some(_), Some(_)) => Err(MuxError::conflict( + "multiple interactive clients are attached; specify a client", + )), + } + } + + async fn has_attached_client(&self) -> bool { + self.clients + .lock() + .await + .values() + .any(|client| client.current_session_id.is_some()) + } + + async fn resolve_optional_reveal_client_id( + &self, + connection_id: u64, + requested_client_id: Option, + ) -> Result> { + if requested_client_id.is_some() || self.has_attached_client().await { + self.resolve_reveal_client_id(connection_id, requested_client_id) + .await + .map(Some) + } else { + Ok(None) + } + } + + async fn focus_revealed_buffer( + &self, + buffer_id: BufferId, + client_id: Option, + ) -> Result<(embers_protocol::BufferLocation, Vec)> { + let (location, mut events, client_event) = { + let mut state = self.state.lock().await; + let location = buffer_location(&state, buffer_id)?; + let (session_id, node_id) = match location.attachment { + BufferLocationAttachment::Detached => { + return Ok((location, Vec::new())); + } + BufferLocationAttachment::Session { + session_id, + node_id, + } + | BufferLocationAttachment::Floating { + session_id, + node_id, + .. + } => (session_id, node_id), + }; + if let Some(client_id) = client_id { + let mut clients = self.clients.lock().await; + let previous_session_id = clients + .get(&client_id) + .ok_or_else(|| { + MuxError::not_found(format!("client {client_id} was not found")) + })? + .current_session_id; + state.focus_leaf(session_id, node_id)?; + let mut events = vec![ServerEvent::NodeChanged(NodeChangedEvent { session_id })]; + if let Some(focus_event) = focus_changed_event(&state, session_id) { + events.push(ServerEvent::FocusChanged(focus_event)); + } + clients + .get_mut(&client_id) + .expect("client existence was checked above") + .current_session_id = Some(session_id); + let subscriptions = self.subscriptions.lock().await; + let mut subscribed_all_sessions = false; + let mut subscribed_session_ids = Vec::new(); + for subscription in subscriptions.values() { + if subscription.connection_id != client_id { + continue; + } + match subscription.session_id { + Some(session_id) => subscribed_session_ids.push(session_id), + None => subscribed_all_sessions = true, + } + } + subscribed_session_ids.sort_by_key(|session_id| session_id.0); + subscribed_session_ids.dedup(); + let client_event = ServerEvent::ClientChanged(ClientChangedEvent { + client: ClientRecord { + id: client_id, + current_session_id: Some(session_id), + subscribed_all_sessions, + subscribed_session_ids, + }, + previous_session_id, + }); + let location = buffer_location(&state, buffer_id)?; + (location, events, Some(client_event)) + } else { + state.focus_leaf(session_id, node_id)?; + let mut events = vec![ServerEvent::NodeChanged(NodeChangedEvent { session_id })]; + if let Some(focus_event) = focus_changed_event(&state, session_id) { + events.push(ServerEvent::FocusChanged(focus_event)); + } + let location = buffer_location(&state, buffer_id)?; + (location, events, None) + } + }; + if let Some(client_event) = client_event { + events.push(client_event); + } + Ok((location, events)) + } + + async fn reveal_buffer( + &self, + connection_id: u64, + requested_client_id: Option, + buffer_id: BufferId, + ) -> Result<(embers_protocol::BufferLocation, Vec)> { + let target_client_id = self + .resolve_optional_reveal_client_id(connection_id, requested_client_id) + .await?; + let location = { + let state = self.state.lock().await; + buffer_location(&state, buffer_id)? + }; + if matches!(location.attachment, BufferLocationAttachment::Detached) { + return Ok((location, Vec::new())); + } + self.focus_revealed_buffer(buffer_id, target_client_id) + .await + } + + async fn open_history_buffer( + &self, + connection_id: u64, + requested_client_id: Option, + source_buffer_id: BufferId, + scope: BufferHistoryScope, + placement: BufferHistoryPlacement, + ) -> Result<(embers_protocol::BufferLocation, Vec)> { + let (source_title, source_cwd, source_location, helper_lines) = { + let state = self.state.lock().await; + let buffer = state.buffer(source_buffer_id)?; + let location = buffer_location(&state, source_buffer_id)?; + let helper_lines = match &buffer.kind { + BufferKind::Helper(helper) => Some(match scope { + BufferHistoryScope::Full => helper.lines.clone(), + BufferHistoryScope::Visible => { + let visible_limit = usize::from(buffer.pty_size.rows).max(1); + let start = helper.lines.len().saturating_sub(visible_limit); + helper.lines[start..].to_vec() + } + }), + BufferKind::Pty => None, + }; + ( + buffer.title.clone(), + buffer.cwd.clone(), + location, + helper_lines, + ) + }; + let target_client_id = self + .resolve_optional_reveal_client_id(connection_id, requested_client_id) + .await?; + + let target_session_id = match source_location.attachment { + BufferLocationAttachment::Session { session_id, .. } + | BufferLocationAttachment::Floating { session_id, .. } => session_id, + BufferLocationAttachment::Detached => { + let client_id = target_client_id.ok_or_else(|| { + MuxError::conflict( + "history for detached buffers requires an attached client session", + ) + })?; + let clients = self.clients.lock().await; + clients + .get(&client_id) + .and_then(|client| client.current_session_id) + .ok_or_else(|| { + MuxError::conflict( + "history for detached buffers requires an attached client session", + ) + })? + } + }; + + let lines = match helper_lines { + Some(lines) => lines, + None => match scope { + BufferHistoryScope::Full => { + self.capture_snapshot(RequestId(0), source_buffer_id) + .await? + .lines + } + BufferHistoryScope::Visible => { + self.capture_visible_snapshot(RequestId(0), source_buffer_id) + .await? + .lines + } + }, + }; + + let helper_scope = match scope { + BufferHistoryScope::Full => HelperBufferScope::Full, + BufferHistoryScope::Visible => HelperBufferScope::Visible, + }; + let helper_title = format!("{} history", source_title); + let helper_buffer_id = { + let mut state = self.state.lock().await; + let helper_buffer_id = state.create_helper_buffer( + helper_title.clone(), + source_buffer_id, + helper_scope, + source_cwd, + lines, + )?; + let placement_result = match placement { + BufferHistoryPlacement::Tab => { + state.add_root_tab_from_buffer( + target_session_id, + helper_title, + helper_buffer_id, + )?; + Ok(()) + } + BufferHistoryPlacement::Floating => { + let geometry = state.default_floating_geometry(target_session_id)?; + state.create_floating_from_buffer( + target_session_id, + helper_buffer_id, + geometry, + Some(helper_title), + )?; + Ok(()) + } + }; + if let Err(error) = placement_result { + let _ = state.remove_buffer(helper_buffer_id); + return Err(error); + } + helper_buffer_id + }; + + match self + .focus_revealed_buffer(helper_buffer_id, target_client_id) + .await + { + Ok(result) => Ok(result), + Err(error) => { + let mut state = self.state.lock().await; + let rollback_result = (|| -> Result<()> { + let helper_location = buffer_location(&state, helper_buffer_id)?; + match helper_location.attachment { + BufferLocationAttachment::Detached => {} + BufferLocationAttachment::Session { node_id, .. } => { + state.close_node(node_id)?; + } + BufferLocationAttachment::Floating { floating_id, .. } => { + state.close_floating(floating_id)?; + } + } + state.remove_buffer(helper_buffer_id)?; + Ok(()) + })(); + match rollback_result { + Ok(()) => Err(error), + Err(rollback_error) => { + debug!( + %helper_buffer_id, + %error, + %rollback_error, + "failed to roll back helper buffer after focus failure" + ); + Err(MuxError::internal(format!( + "{error}; rollback also failed: {rollback_error}" + ))) + } + } + } + } + } + async fn capture_snapshot( &self, request_id: RequestId, buffer_id: BufferId, ) -> Result { - let buffer = { + enum SnapshotSource { + Helper { + sequence: u64, + size: PtySize, + lines: Vec, + title: String, + cwd: Option, + }, + Runtime { + title: String, + cwd: Option, + }, + } + + let source = { let state = self.state.lock().await; - state.buffer(buffer_id)?.clone() + let buffer = state.buffer(buffer_id)?; + match &buffer.kind { + BufferKind::Helper(helper) => SnapshotSource::Helper { + sequence: buffer.last_snapshot_seq, + size: buffer.pty_size, + lines: helper.lines.clone(), + title: buffer.title.clone(), + cwd: buffer.cwd.as_ref().map(|path| path.display().to_string()), + }, + _ => SnapshotSource::Runtime { + title: buffer.title.clone(), + cwd: buffer.cwd.clone(), + }, + } + }; + let (buffer_title, buffer_cwd) = match source { + SnapshotSource::Helper { + sequence, + size, + lines, + title, + cwd, + } => { + return Ok(SnapshotResponse { + request_id, + buffer_id, + sequence, + size, + lines, + title: Some(title), + cwd, + }); + } + SnapshotSource::Runtime { title, cwd } => (title, cwd), }; let runtime = self.buffer_runtime(buffer_id).await?; - let snapshot = runtime.capture_snapshot(buffer.cwd.clone()).await?; + let snapshot = runtime.capture_snapshot(buffer_cwd.clone()).await?; self.sync_buffer_runtime_status(buffer_id, &runtime).await?; Ok(SnapshotResponse { @@ -1663,8 +2381,8 @@ impl Runtime { sequence: snapshot.sequence, size: snapshot.size, lines: snapshot.lines, - title: snapshot.title.or(Some(buffer.title)), - cwd: buffer.cwd.map(|path| path.display().to_string()), + title: snapshot.title.or(Some(buffer_title)), + cwd: buffer_cwd.map(|path| path.display().to_string()), }) } @@ -1673,12 +2391,74 @@ impl Runtime { request_id: RequestId, buffer_id: BufferId, ) -> Result { - let buffer = { + enum VisibleSnapshotSource { + Helper { + sequence: u64, + size: PtySize, + lines: Vec, + title: String, + cwd: Option, + viewport_top_line: u64, + total_lines: u64, + }, + Runtime { + cwd: Option, + }, + } + + let source = { let state = self.state.lock().await; - state.buffer(buffer_id)?.clone() + let buffer = state.buffer(buffer_id)?; + match &buffer.kind { + BufferKind::Helper(helper) => { + let viewport_height = usize::from(buffer.pty_size.rows.max(1)); + let viewport_start = helper.lines.len().saturating_sub(viewport_height); + VisibleSnapshotSource::Helper { + sequence: buffer.last_snapshot_seq, + size: buffer.pty_size, + lines: helper.lines.iter().skip(viewport_start).cloned().collect(), + title: buffer.title.clone(), + cwd: buffer.cwd.as_ref().map(|path| path.display().to_string()), + viewport_top_line: u64::try_from(viewport_start).unwrap_or(u64::MAX), + total_lines: u64::try_from(helper.lines.len()).unwrap_or(u64::MAX), + } + } + _ => VisibleSnapshotSource::Runtime { + cwd: buffer.cwd.clone(), + }, + } + }; + let buffer_cwd = match source { + VisibleSnapshotSource::Helper { + sequence, + size, + lines, + title, + cwd, + viewport_top_line, + total_lines, + } => { + return Ok(VisibleSnapshotResponse { + request_id, + buffer_id, + sequence, + size, + lines, + title: Some(title), + cwd, + viewport_top_line, + total_lines, + alternate_screen: false, + mouse_reporting: false, + focus_reporting: false, + bracketed_paste: false, + cursor: None, + }); + } + VisibleSnapshotSource::Runtime { cwd } => cwd, }; let runtime = self.buffer_runtime(buffer_id).await?; - let snapshot = runtime.capture_visible_snapshot(buffer.cwd.clone()).await?; + let snapshot = runtime.capture_visible_snapshot(buffer_cwd.clone()).await?; self.sync_buffer_runtime_status(buffer_id, &runtime).await?; Ok(VisibleSnapshotResponse { @@ -1706,6 +2486,35 @@ impl Runtime { start_line: u64, line_count: u32, ) -> Result { + let helper_slice = { + let state = self.state.lock().await; + match &state.buffer(buffer_id)?.kind { + BufferKind::Helper(helper) => { + let total_lines = u64::try_from(helper.lines.len()).unwrap_or(u64::MAX); + let start = usize::try_from(start_line) + .unwrap_or(usize::MAX) + .min(helper.lines.len()); + let end = start + .saturating_add(usize::try_from(line_count).unwrap_or(usize::MAX)) + .min(helper.lines.len()); + Some(( + u64::try_from(start).unwrap_or(u64::MAX), + total_lines, + helper.lines[start..end].to_vec(), + )) + } + BufferKind::Pty => None, + } + }; + if let Some((start_line, total_lines, lines)) = helper_slice { + return Ok(ScrollbackSliceResponse { + request_id, + buffer_id, + start_line, + total_lines, + lines, + }); + } let runtime = self.buffer_runtime(buffer_id).await?; let slice = runtime .capture_scrollback_slice(start_line, line_count) @@ -1731,7 +2540,7 @@ impl Runtime { false } else { buffer.last_snapshot_seq = update.sequence; - buffer.activity = update.activity; + buffer.activity = max_activity(buffer.activity, update.activity); if let Some(title) = update.title { match title { Some(title) => buffer.title = title, @@ -2026,6 +2835,25 @@ impl Runtime { } } +fn location_is_root_tab(state: &ServerState, location: &BufferLocation) -> Result { + let BufferLocationAttachment::Session { + session_id, + node_id, + } = location.attachment + else { + return Ok(false); + }; + let root_tabs_id = match state.root_tabs(session_id) { + Ok(root_tabs_id) => root_tabs_id, + Err(embers_core::MuxError::Conflict(_)) => return Ok(false), + Err(error) => return Err(error), + }; + let Node::Tabs(tabs) = state.node(root_tabs_id)? else { + return Ok(false); + }; + Ok(tabs.tabs.iter().any(|tab| tab.child == node_id)) +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ConnectionExit { Closed, @@ -2112,10 +2940,10 @@ async fn handle_connection( } }; - let span = request_span("handle_request", request.request_id()); - let _entered = span.enter(); + let request_id = request.request_id(); let (response, events, deferred_shutdown) = runtime .dispatch_request(connection_id, &outbound, request) + .instrument(request_span("handle_request", request_id)) .await; if outbound.send(ServerEnvelope::Response(response)).is_err() { @@ -2220,25 +3048,45 @@ fn focus_changed_event( }) } +fn layout_changed_events( + state: &ServerState, + session_id: embers_core::SessionId, +) -> Result> { + state.session(session_id)?; + let mut events = vec![ServerEvent::NodeChanged(NodeChangedEvent { session_id })]; + if let Some(focus_event) = focus_changed_event(state, session_id) { + events.push(ServerEvent::FocusChanged(focus_event)); + } + Ok(events) +} + +fn layout_ok_response( + state: &ServerState, + request_id: RequestId, + session_id: embers_core::SessionId, +) -> (ServerResponse, Vec) { + match layout_changed_events(state, session_id) { + Ok(events) => (ServerResponse::Ok(OkResponse { request_id }), events), + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + } +} + fn layout_snapshot_response( state: &ServerState, request_id: RequestId, session_id: embers_core::SessionId, ) -> (ServerResponse, Vec) { match session_snapshot(state, session_id) { - Ok(snapshot) => { - let mut events = vec![ServerEvent::NodeChanged(NodeChangedEvent { session_id })]; - if let Some(focus_event) = focus_changed_event(state, session_id) { - events.push(ServerEvent::FocusChanged(focus_event)); - } - ( + Ok(snapshot) => match layout_changed_events(state, session_id) { + Ok(events) => ( ServerResponse::SessionSnapshot(SessionSnapshotResponse { request_id, snapshot, }), events, - ) - } + ), + Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), + }, Err(error) => (mux_error_response(Some(request_id), error), Vec::new()), } } @@ -2291,7 +3139,11 @@ fn apply_runtime_status( if let Some(title) = &status.title { let _ = state.set_buffer_title(buffer_id, title.clone()); } - let _ = state.set_buffer_activity(buffer_id, status.activity); + if let Some(buffer) = state.buffers.get(&buffer_id) { + let current_activity = buffer.activity; + let _ = + state.set_buffer_activity(buffer_id, max_activity(current_activity, status.activity)); + } if status.running { let _ = state.mark_buffer_running(buffer_id, status.pid); } else { @@ -2299,6 +3151,19 @@ fn apply_runtime_status( } } +fn max_activity( + left: embers_core::ActivityState, + right: embers_core::ActivityState, +) -> embers_core::ActivityState { + use embers_core::ActivityState; + + match (left, right) { + (ActivityState::Bell, _) | (_, ActivityState::Bell) => ActivityState::Bell, + (ActivityState::Activity, _) | (_, ActivityState::Activity) => ActivityState::Activity, + _ => ActivityState::Idle, + } +} + fn buffer_pid_hint(state: &BufferState) -> Option { match state { BufferState::Running(running) => running.pid, @@ -2315,16 +3180,141 @@ mod tests { use std::path::PathBuf; use std::sync::Arc; - use embers_core::ActivityState; - use embers_protocol::{ServerEnvelope, ServerEvent}; + use embers_core::{ActivityState, FloatGeometry, MuxError, RequestId, SplitDirection}; + use embers_protocol::{ + BufferHistoryPlacement, BufferHistoryScope, InputRequest, NodeBreakDestination, + NodeJoinPlacement, NodeRequest, ServerEnvelope, ServerEvent, ServerResponse, + }; use tempfile::tempdir; use tokio::sync::mpsc; use super::{Runtime, ShutdownSignal, Subscription, wait_for_shutdown}; + use crate::model::HelperBufferScope; use crate::{BufferRuntimeUpdate, BufferState, ServerState}; use tokio::time::{Duration, timeout}; + fn create_leaf( + state: &mut ServerState, + session_id: embers_core::SessionId, + title: &str, + ) -> embers_core::NodeId { + let buffer_id = state.create_buffer(title, vec!["/bin/sh".to_owned()], None); + state + .create_buffer_view(session_id, buffer_id) + .expect("create leaf") + } + + fn floating_split_runtime() -> ( + Runtime, + embers_core::SessionId, + embers_core::FloatingId, + embers_core::NodeId, + embers_core::NodeId, + ) { + let tempdir = tempdir().expect("tempdir"); + let mut state = ServerState::new(); + let session_id = state.create_session("alpha"); + let root_leaf = create_leaf(&mut state, session_id, "root"); + state + .add_root_tab(session_id, "main", root_leaf) + .expect("attach root leaf"); + let split_left = create_leaf(&mut state, session_id, "left"); + let split_right = create_leaf(&mut state, session_id, "right"); + let split_root = state + .create_split_node( + session_id, + SplitDirection::Vertical, + vec![split_left, split_right], + ) + .expect("create floating split"); + let floating_id = state + .create_floating_window_with_options( + session_id, + split_root, + FloatGeometry::new(4, 4, 20, 10), + Some("popup".to_owned()), + false, + true, + ) + .expect("create floating"); + ( + Runtime::new( + state, + tempdir.path().join("server.sock"), + tempdir.path().join("workspace.json"), + tempdir.path().join("runtime"), + BTreeMap::new(), + ), + session_id, + floating_id, + split_left, + split_right, + ) + } + + fn hidden_floating_tabs_runtime() -> ( + Runtime, + embers_core::SessionId, + embers_core::FloatingId, + embers_core::NodeId, + ) { + let tempdir = tempdir().expect("tempdir"); + let mut state = ServerState::new(); + let session_id = state.create_session("alpha"); + let root_leaf = create_leaf(&mut state, session_id, "root"); + state + .add_root_tab(session_id, "main", root_leaf) + .expect("attach root leaf"); + let split_left = create_leaf(&mut state, session_id, "left"); + let split_right = create_leaf(&mut state, session_id, "right"); + let split_root = state + .create_split_node( + session_id, + SplitDirection::Vertical, + vec![split_left, split_right], + ) + .expect("create hidden split"); + let other_leaf = create_leaf(&mut state, session_id, "other"); + let hidden_tabs = state + .create_tabs_node( + session_id, + vec![ + crate::TabEntry::new("split", split_root), + crate::TabEntry::new("other", other_leaf), + ], + 0, + ) + .expect("create hidden tabs"); + let floating_id = state + .create_floating_window_with_options( + session_id, + hidden_tabs, + FloatGeometry::new(4, 4, 20, 10), + Some("popup".to_owned()), + false, + true, + ) + .expect("create floating"); + state + .floating + .get_mut(&floating_id) + .expect("floating exists") + .visible = false; + ( + Runtime::new( + state, + tempdir.path().join("server.sock"), + tempdir.path().join("workspace.json"), + tempdir.path().join("runtime"), + BTreeMap::new(), + ), + session_id, + floating_id, + split_right, + ) + } + #[tokio::test] async fn shutdown_signal_is_latched_for_new_receivers() { let signal = ShutdownSignal::new(); @@ -2548,4 +3538,225 @@ mod tests { assert!(matches!(buffer.state, BufferState::Interrupted(_))); assert_eq!(buffer.runtime_socket_path(), None); } + + #[tokio::test] + async fn open_history_for_detached_buffers_requires_attached_session_before_capture() { + let tempdir = tempdir().expect("tempdir"); + let mut state = ServerState::new(); + let buffer_id = state.create_buffer("detached", vec!["/bin/sh".to_owned()], None); + let runtime = Runtime::new( + state, + tempdir.path().join("server.sock"), + tempdir.path().join("workspace.json"), + tempdir.path().join("runtime"), + BTreeMap::new(), + ); + + let error = runtime + .open_history_buffer( + 1, + None, + buffer_id, + BufferHistoryScope::Visible, + BufferHistoryPlacement::Tab, + ) + .await + .expect_err("detached history requires an attached client session"); + + assert!(matches!( + error, + MuxError::Conflict(message) + if message == "history for detached buffers requires an attached client session" + )); + } + + #[tokio::test] + async fn resizing_helper_buffers_updates_visible_snapshot_viewport() { + let tempdir = tempdir().expect("tempdir"); + let mut state = ServerState::new(); + let source_buffer_id = state.create_buffer("source", vec!["/bin/sh".to_owned()], None); + let helper_buffer_id = state + .create_helper_buffer( + "source history", + source_buffer_id, + HelperBufferScope::Visible, + None, + vec![ + "line-1".to_owned(), + "line-2".to_owned(), + "line-3".to_owned(), + "line-4".to_owned(), + "line-5".to_owned(), + ], + ) + .expect("create helper buffer"); + let runtime = Arc::new(Runtime::new( + state, + tempdir.path().join("server.sock"), + tempdir.path().join("workspace.json"), + tempdir.path().join("runtime"), + BTreeMap::new(), + )); + + let (response, events) = runtime + .dispatch_input(InputRequest::Resize { + request_id: RequestId(1), + buffer_id: helper_buffer_id, + cols: 80, + rows: 3, + }) + .await; + + assert!(matches!( + response, + ServerResponse::Ok(response) if response.request_id == RequestId(1) + )); + assert!(matches!( + events.as_slice(), + [ServerEvent::RenderInvalidated(event)] if event.buffer_id == helper_buffer_id + )); + + let snapshot = runtime + .capture_visible_snapshot(RequestId(2), helper_buffer_id) + .await + .expect("capture visible snapshot succeeds"); + + assert_eq!(snapshot.size.rows, 3); + assert_eq!(snapshot.total_lines, 5); + assert_eq!(snapshot.viewport_top_line, 2); + assert_eq!( + snapshot.lines, + vec![ + "line-3".to_owned(), + "line-4".to_owned(), + "line-5".to_owned() + ] + ); + } + + #[tokio::test] + async fn join_buffer_at_node_emits_floating_changed_for_existing_floating() { + let tempdir = tempdir().expect("tempdir"); + let mut state = ServerState::new(); + let session_id = state.create_session("alpha"); + let root_buffer_id = state.create_buffer("root", vec!["/bin/sh".to_owned()], None); + let root_leaf = state + .create_buffer_view(session_id, root_buffer_id) + .expect("create root leaf"); + state + .add_root_tab(session_id, "main", root_leaf) + .expect("attach root leaf"); + + let popup_buffer_id = state.create_buffer("popup", vec!["/bin/sh".to_owned()], None); + let popup_leaf = state + .create_buffer_view(session_id, popup_buffer_id) + .expect("create popup leaf"); + let floating_id = state + .create_floating_window_with_options( + session_id, + popup_leaf, + FloatGeometry::new(4, 4, 20, 10), + Some("popup".to_owned()), + false, + true, + ) + .expect("create floating"); + + let detached_buffer_id = state.create_buffer("logs", vec!["/bin/sh".to_owned()], None); + let runtime = Runtime::new( + state, + tempdir.path().join("server.sock"), + tempdir.path().join("workspace.json"), + tempdir.path().join("runtime"), + BTreeMap::new(), + ); + + let (response, events) = runtime + .dispatch_node(NodeRequest::JoinBufferAtNode { + request_id: RequestId(1), + node_id: popup_leaf, + buffer_id: detached_buffer_id, + placement: NodeJoinPlacement::Right, + }) + .await; + + assert!(matches!(response, ServerResponse::Ok(_))); + assert!(events.iter().any(|event| { + matches!( + event, + ServerEvent::FloatingChanged(changed) + if changed.session_id == session_id + && changed.floating_id == Some(floating_id) + ) + })); + } + + #[tokio::test] + async fn swap_siblings_emits_floating_changed_for_existing_floating() { + let (runtime, session_id, floating_id, split_left, split_right) = floating_split_runtime(); + + let (response, events) = runtime + .dispatch_node(NodeRequest::SwapSiblings { + request_id: RequestId(1), + first_node_id: split_left, + second_node_id: split_right, + }) + .await; + + assert!(matches!(response, ServerResponse::Ok(_))); + assert!(events.iter().any(|event| { + matches!( + event, + ServerEvent::FloatingChanged(changed) + if changed.session_id == session_id + && changed.floating_id == Some(floating_id) + ) + })); + } + + #[tokio::test] + async fn move_node_after_emits_floating_changed_for_existing_floating() { + let (runtime, session_id, floating_id, split_left, split_right) = floating_split_runtime(); + + let (response, events) = runtime + .dispatch_node(NodeRequest::MoveNodeAfter { + request_id: RequestId(1), + node_id: split_left, + sibling_node_id: split_right, + }) + .await; + + assert!(matches!(response, ServerResponse::Ok(_))); + assert!(events.iter().any(|event| { + matches!( + event, + ServerEvent::FloatingChanged(changed) + if changed.session_id == session_id + && changed.floating_id == Some(floating_id) + ) + })); + } + + #[tokio::test] + async fn break_node_emits_floating_changed_when_node_stays_in_same_floating() { + let (runtime, session_id, floating_id, split_right) = hidden_floating_tabs_runtime(); + + let (response, events) = runtime + .dispatch_node(NodeRequest::BreakNode { + request_id: RequestId(1), + node_id: split_right, + destination: NodeBreakDestination::Tab, + }) + .await; + + assert!(matches!(response, ServerResponse::Ok(_))); + assert!(events.iter().any(|event| { + matches!( + event, + ServerEvent::FloatingChanged(changed) + if changed.session_id == session_id + && changed.floating_id == Some(floating_id) + ) + })); + } } diff --git a/crates/embers-server/src/state.rs b/crates/embers-server/src/state.rs index 416d58d..dac06ef 100644 --- a/crates/embers-server/src/state.rs +++ b/crates/embers-server/src/state.rs @@ -5,10 +5,12 @@ use embers_core::{ ActivityState, BufferId, FloatGeometry, FloatingId, IdAllocator, MuxError, NodeId, PtySize, Result, SessionId, SplitDirection, Timestamp, }; +use embers_protocol::NodeJoinPlacement; use crate::model::{ - Buffer, BufferAttachment, BufferState, BufferViewNode, BufferViewState, ExitedBuffer, - FloatingWindow, InterruptedBuffer, Node, RunningBuffer, Session, SplitNode, TabEntry, TabsNode, + Buffer, BufferAttachment, BufferKind, BufferState, BufferViewNode, BufferViewState, + ExitedBuffer, FloatingWindow, HelperBuffer, HelperBufferScope, InterruptedBuffer, Node, + RunningBuffer, Session, SplitNode, TabEntry, TabsNode, }; use crate::persist::{ CURRENT_FORMAT_VERSION, PersistedWorkspace, persisted_buffer, persisted_floating, @@ -16,6 +18,41 @@ use crate::persist::{ restored_session, }; +const DEFAULT_FLOATING_WIDTH_PADDING: u16 = 8; +const DEFAULT_FLOATING_HEIGHT_PADDING: u16 = 4; +const DEFAULT_FLOATING_MIN_WIDTH: u16 = 40; +const DEFAULT_FLOATING_MAX_WIDTH: u16 = 120; +const DEFAULT_FLOATING_MIN_HEIGHT: u16 = 10; +const DEFAULT_FLOATING_MAX_HEIGHT: u16 = 40; + +#[derive(Clone, Debug)] +enum ParentSlotSnapshot { + Split { + index: usize, + size: u16, + last_focused_descendant: Option, + }, + Tabs { + index: usize, + tab: TabEntry, + active: usize, + last_focused_descendant: Option, + }, +} + +#[derive(Clone, Debug)] +enum NodeOwnerSnapshot { + Parent { + parent_id: NodeId, + slot: ParentSlotSnapshot, + }, + Floating { + window: FloatingWindow, + index: usize, + focused_floating: Option, + }, +} + #[derive(Debug)] pub struct ServerState { pub sessions: BTreeMap, @@ -322,6 +359,7 @@ impl ServerState { floating: Vec::new(), focused_leaf: None, focused_floating: None, + zoomed_node: None, created_at: Timestamp::now(), }, ); @@ -350,6 +388,28 @@ impl ServerState { buffer_id } + pub fn create_helper_buffer( + &mut self, + title: impl Into, + source_buffer_id: BufferId, + scope: HelperBufferScope, + cwd: Option, + lines: Vec, + ) -> Result { + let source_pty_size = self.buffer(source_buffer_id)?.pty_size; + let buffer_id = self.buffer_ids.next(); + let mut buffer = Buffer::new(buffer_id, title, Vec::new(), cwd, BTreeMap::new()); + buffer.pty_size = source_pty_size; + buffer.last_snapshot_seq = 1; + buffer.kind = BufferKind::Helper(HelperBuffer { + source_buffer_id, + scope, + lines, + }); + self.buffers.insert(buffer_id, buffer); + Ok(buffer_id) + } + pub fn remove_buffer(&mut self, buffer_id: BufferId) -> Result { let buffer = self.buffer(buffer_id)?.clone(); if !matches!(buffer.attachment, BufferAttachment::Detached) { @@ -458,6 +518,19 @@ impl ServerState { &mut self, session_id: SessionId, buffer_id: BufferId, + ) -> Result { + let node_id = self.create_unattached_buffer_view(session_id, buffer_id)?; + if let Err(error) = self.attach_buffer(buffer_id, node_id) { + self.nodes.remove(&node_id); + return Err(error); + } + Ok(node_id) + } + + fn create_unattached_buffer_view( + &mut self, + session_id: SessionId, + buffer_id: BufferId, ) -> Result { self.ensure_session_exists(session_id)?; self.buffer(buffer_id)?; @@ -473,10 +546,6 @@ impl ServerState { view: BufferViewState::default(), }), ); - if let Err(error) = self.attach_buffer(buffer_id, node_id) { - self.nodes.remove(&node_id); - return Err(error); - } Ok(node_id) } @@ -563,7 +632,7 @@ impl ServerState { session_id, parent: None, tabs, - active: active.min(active.saturating_sub(0)), + active, last_focused_descendant: None, }), ); @@ -589,6 +658,43 @@ impl ServerState { self.create_floating_window_with_options(session_id, root_node, geometry, title, true, true) } + pub fn default_floating_geometry(&self, session_id: SessionId) -> Result { + let session = self.session(session_id)?; + let anchor_leaf = session + .focused_leaf + .or(self.resolve_visible_leaf(session.root_node)?) + .or(self.resolve_first_leaf(session.root_node)?); + let size = self + .visible_node_size(session.root_node)? + .or(anchor_leaf + .map(|leaf_id| match self.node(leaf_id)? { + Node::BufferView(node) => Ok(node.view.last_render_size), + _ => Err(MuxError::internal(format!( + "floating geometry anchor {leaf_id} is not a buffer view" + ))), + }) + .transpose()?) + .unwrap_or(PtySize::new(80, 24)); + let width = Self::default_floating_dimension( + size.cols, + DEFAULT_FLOATING_WIDTH_PADDING, + DEFAULT_FLOATING_MIN_WIDTH, + DEFAULT_FLOATING_MAX_WIDTH, + ); + let height = Self::default_floating_dimension( + size.rows, + DEFAULT_FLOATING_HEIGHT_PADDING, + DEFAULT_FLOATING_MIN_HEIGHT, + DEFAULT_FLOATING_MAX_HEIGHT, + ); + Ok(FloatGeometry::new( + size.cols.saturating_sub(width) / 2, + size.rows.saturating_sub(height) / 2, + width, + height, + )) + } + pub fn create_floating_window_with_options( &mut self, session_id: SessionId, @@ -632,8 +738,12 @@ impl ServerState { }, ); self.session_mut(session_id)?.floating.push(floating_id); - if focus { - self.focus_floating(floating_id)?; + if focus && let Err(error) = self.focus_floating(floating_id) { + self.floating.remove(&floating_id); + if let Some(session) = self.sessions.get_mut(&session_id) { + session.floating.retain(|id| *id != floating_id); + } + return Err(error); } Ok(floating_id) } @@ -660,20 +770,27 @@ impl ServerState { close_on_empty: bool, ) -> Result { let root_node = self.create_buffer_view(session_id, buffer_id)?; - self.create_floating_window_with_options( + match self.create_floating_window_with_options( session_id, root_node, geometry, title, focus, close_on_empty, - ) + ) { + Ok(floating_id) => Ok(floating_id), + Err(error) => { + self.discard_buffer_view(root_node); + Err(error) + } + } } pub fn close_floating(&mut self, floating_id: FloatingId) -> Result<()> { let floating = self.remove_floating_window(floating_id)?; let session_id = floating.session_id; self.remove_subtree_nodes(floating.root_node)?; + self.normalize_zoomed_node(session_id)?; self.heal_focus(session_id) } @@ -717,7 +834,7 @@ impl ServerState { Node::Tabs(tabs) => tabs.tabs.len(), _ => return Err(MuxError::invalid_input("node is not a tabs container")), }; - self.add_tab_sibling_at(tabs_id, append_index, title, child) + self.add_tab_sibling_at_with_focus(tabs_id, append_index, title, child, true) } pub fn add_tab_sibling_at( @@ -726,6 +843,17 @@ impl ServerState { index: usize, title: impl Into, child: NodeId, + ) -> Result { + self.add_tab_sibling_at_with_focus(tabs_id, index, title, child, true) + } + + fn add_tab_sibling_at_with_focus( + &mut self, + tabs_id: NodeId, + index: usize, + title: impl Into, + child: NodeId, + do_focus: bool, ) -> Result { let session_id = self.node_session_id(tabs_id)?; self.ensure_node_belongs_to(child, session_id)?; @@ -774,10 +902,12 @@ impl ServerState { index }; - if let Some(leaf) = self.resolve_focus_candidate(child)? { - self.focus_leaf(session_id, leaf)?; - } else { - self.heal_focus(session_id)?; + if do_focus { + if let Some(leaf) = self.resolve_focus_candidate(child)? { + self.focus_leaf(session_id, leaf)?; + } else { + self.heal_focus(session_id)?; + } } Ok(index) @@ -869,6 +999,17 @@ impl ServerState { direction: SplitDirection, sibling: NodeId, insert_before: bool, + ) -> Result { + self.wrap_node_in_split_with_focus(node_id, direction, sibling, insert_before, true) + } + + fn wrap_node_in_split_with_focus( + &mut self, + node_id: NodeId, + direction: SplitDirection, + sibling: NodeId, + insert_before: bool, + do_focus: bool, ) -> Result { let session_id = self.node_session_id(node_id)?; self.ensure_node_belongs_to(sibling, session_id)?; @@ -915,10 +1056,12 @@ impl ServerState { self.set_parent(node_id, Some(split_id))?; self.set_parent(sibling, Some(split_id))?; self.repoint_owner_reference(session_id, old_parent, node_id, split_id)?; - if let Some(leaf) = self.resolve_focus_candidate(sibling)? { - self.focus_leaf(session_id, leaf)?; - } else { - self.heal_focus(session_id)?; + if do_focus { + if let Some(leaf) = self.resolve_focus_candidate(sibling)? { + self.focus_leaf(session_id, leaf)?; + } else { + self.heal_focus(session_id)?; + } } Ok(split_id) } @@ -1212,6 +1355,313 @@ impl ServerState { self.focus_leaf(target_session, target_leaf) } + pub fn zoom_node(&mut self, node_id: NodeId) -> Result<()> { + let session_id = self.node_session_id(node_id)?; + self.ensure_node_visible_in_session(session_id, node_id)?; + self.refocus_zoomed_subtree(session_id, node_id)?; + self.session_mut(session_id)?.zoomed_node = Some(node_id); + Ok(()) + } + + pub fn unzoom_session(&mut self, session_id: SessionId) -> Result<()> { + self.session_mut(session_id)?.zoomed_node = None; + Ok(()) + } + + pub fn toggle_zoom_node(&mut self, node_id: NodeId) -> Result<()> { + let session_id = self.node_session_id(node_id)?; + if self.session(session_id)?.zoomed_node == Some(node_id) { + self.session_mut(session_id)?.zoomed_node = None; + } else { + self.ensure_node_visible_in_session(session_id, node_id)?; + self.refocus_zoomed_subtree(session_id, node_id)?; + self.session_mut(session_id)?.zoomed_node = Some(node_id); + } + Ok(()) + } + + pub fn swap_sibling_nodes( + &mut self, + first_node_id: NodeId, + second_node_id: NodeId, + ) -> Result<()> { + if first_node_id == second_node_id { + return Ok(()); + } + let parent_id = self.shared_parent(first_node_id, second_node_id)?; + match self.node_mut(parent_id)? { + Node::Split(split) => { + let first = split + .children + .iter() + .position(|child| *child == first_node_id) + .ok_or_else(|| { + MuxError::not_found(format!( + "node {first_node_id} is not a child of split {parent_id}" + )) + })?; + let second = split + .children + .iter() + .position(|child| *child == second_node_id) + .ok_or_else(|| { + MuxError::not_found(format!( + "node {second_node_id} is not a child of split {parent_id}" + )) + })?; + split.children.swap(first, second); + split.sizes.swap(first, second); + } + Node::Tabs(tabs) => { + let first = tabs + .tabs + .iter() + .position(|tab| tab.child == first_node_id) + .ok_or_else(|| { + MuxError::not_found(format!( + "node {first_node_id} is not a child of tabs {parent_id}" + )) + })?; + let second = tabs + .tabs + .iter() + .position(|tab| tab.child == second_node_id) + .ok_or_else(|| { + MuxError::not_found(format!( + "node {second_node_id} is not a child of tabs {parent_id}" + )) + })?; + tabs.tabs.swap(first, second); + match tabs.active { + active if active == first => tabs.active = second, + active if active == second => tabs.active = first, + _ => {} + } + } + Node::BufferView(_) => { + return Err(MuxError::invalid_input( + "buffer views do not have sibling children", + )); + } + } + Ok(()) + } + + pub fn move_node_before(&mut self, node_id: NodeId, sibling_node_id: NodeId) -> Result<()> { + self.reorder_sibling_node(node_id, sibling_node_id, true) + } + + pub fn move_node_after(&mut self, node_id: NodeId, sibling_node_id: NodeId) -> Result<()> { + self.reorder_sibling_node(node_id, sibling_node_id, false) + } + + pub fn break_node(&mut self, node_id: NodeId, to_floating: bool) -> Result<()> { + if to_floating + && self.node_parent(node_id)?.is_none() + && self.floating_id_by_root(node_id).is_some() + { + return Ok(()); + } + if !to_floating + && let Some(parent_id) = self.node_parent(node_id)? + && matches!(self.node(parent_id)?, Node::Tabs(_)) + { + return Ok(()); + } + let session_id = self.node_session_id(node_id)?; + let title = self.default_tab_title(node_id)?; + let do_focus = self.is_node_visible_in_session(session_id, node_id)?; + let owner_snapshot = self.capture_node_owner_snapshot(node_id)?; + let (old_parent, _) = self.detach_node_from_owner(node_id)?; + + let result = if to_floating { + let geometry = self.default_floating_geometry(session_id)?; + self.create_floating_window_with_options( + session_id, + node_id, + geometry, + Some(title), + true, + true, + ) + .map(|_| ()) + } else { + let tabs_id = match self.nearest_tabs_ancestor(old_parent)? { + Some(tabs_id) => tabs_id, + None => self.ensure_root_tabs_container(session_id)?, + }; + let insert_index = match self.node(tabs_id)? { + Node::Tabs(tabs) => tabs.tabs.len(), + _ => return Err(MuxError::invalid_input("node is not a tabs container")), + }; + self.add_tab_sibling_at_with_focus(tabs_id, insert_index, title, node_id, do_focus) + .map(|_| ()) + }; + if let Err(error) = result { + self.rollback_detached_node(node_id, owner_snapshot) + .map_err(|rollback_error| { + MuxError::internal(format!( + "failed to roll back break_node({node_id}) after {error}: {rollback_error}" + )) + })?; + return Err(error); + } + + if let Some(parent_id) = old_parent { + self.normalize_upwards(parent_id)?; + } + self.normalize_zoomed_node(session_id)?; + Ok(()) + } + + pub fn join_buffer_at_node( + &mut self, + node_id: NodeId, + buffer_id: BufferId, + placement: NodeJoinPlacement, + ) -> Result<()> { + let target_session = self.node_session_id(node_id)?; + let do_focus = self.is_node_visible_in_session(target_session, node_id)?; + let mut created_tabs_wrapper = None; + let source_view = match self.buffer(buffer_id)?.attachment { + BufferAttachment::Attached(source_view) => { + let source_session = self.node_session_id(source_view)?; + if source_session != target_session { + return Err(MuxError::conflict(format!( + "buffer {buffer_id} is attached in session {source_session} and cannot be joined into session {target_session}" + ))); + } + if source_view == node_id || self.node_is_descendant_of(source_view, node_id)? { + return Err(MuxError::conflict(format!( + "buffer {buffer_id} is already contained by node {node_id} via view {source_view}" + ))); + } + Some(source_view) + } + BufferAttachment::Detached => None, + }; + + let new_view = if source_view.is_some() { + self.create_unattached_buffer_view(target_session, buffer_id)? + } else { + self.create_buffer_view(target_session, buffer_id)? + }; + let result = match placement { + NodeJoinPlacement::Left => self + .wrap_node_in_split_with_focus( + node_id, + SplitDirection::Vertical, + new_view, + true, + do_focus, + ) + .map(|_| ()), + NodeJoinPlacement::Right => self + .wrap_node_in_split_with_focus( + node_id, + SplitDirection::Vertical, + new_view, + false, + do_focus, + ) + .map(|_| ()), + NodeJoinPlacement::Up => self + .wrap_node_in_split_with_focus( + node_id, + SplitDirection::Horizontal, + new_view, + true, + do_focus, + ) + .map(|_| ()), + NodeJoinPlacement::Down => self + .wrap_node_in_split_with_focus( + node_id, + SplitDirection::Horizontal, + new_view, + false, + do_focus, + ) + .map(|_| ()), + NodeJoinPlacement::TabBefore | NodeJoinPlacement::TabAfter => { + let title = self.buffer(buffer_id)?.title.clone(); + let tabs_id = if matches!(self.node(node_id)?, Node::Tabs(_)) { + node_id + } else if matches!( + self.node_parent(node_id)? + .map(|id| self.node(id)) + .transpose()?, + Some(Node::Tabs(_)) + ) { + self.node_parent(node_id)?.expect("checked parent exists") + } else { + let tabs_id = + self.wrap_node_in_tabs(node_id, self.default_tab_title(node_id)?)?; + created_tabs_wrapper = Some(tabs_id); + tabs_id + }; + let insert_index = { + let tabs = match self.node(tabs_id)? { + Node::Tabs(tabs) => tabs, + _ => return Err(MuxError::invalid_input("node is not a tabs container")), + }; + let len = tabs.tabs.len(); + if tabs_id == node_id { + let active = tabs.active.min(len); + match placement { + NodeJoinPlacement::TabBefore => active, + NodeJoinPlacement::TabAfter => active.saturating_add(1).min(len), + _ => unreachable!(), + } + } else { + let current = tabs + .tabs + .iter() + .position(|tab| tab.child == node_id) + .ok_or_else(|| { + MuxError::not_found(format!( + "node {node_id} is not a child of tabs {tabs_id}" + )) + })?; + match placement { + NodeJoinPlacement::TabBefore => current, + NodeJoinPlacement::TabAfter => current.saturating_add(1).min(len), + _ => unreachable!(), + } + } + }; + self.add_tab_sibling_at_with_focus(tabs_id, insert_index, title, new_view, do_focus) + .map(|_| ()) + } + }; + + if let Err(error) = result { + self.discard_buffer_view(new_view); + if let Some(tabs_id) = created_tabs_wrapper + && self.nodes.contains_key(&tabs_id) + { + let _ = self.normalize_upwards(tabs_id); + } + return Err(error); + } + if let Some(source_view) = source_view { + if let Err(error) = self.close_node(source_view) { + match self.close_node(new_view) { + Ok(()) => {} + Err(rollback_error) => { + return Err(MuxError::internal(format!( + "failed to close original view {source_view} after joining buffer {buffer_id}: {error}; rollback failed while closing new view {new_view}: {rollback_error}" + ))); + } + } + return Err(error); + } + self.buffer_mut(buffer_id)?.attachment = BufferAttachment::Attached(new_view); + } + self.normalize_zoomed_node(target_session)?; + Ok(()) + } + pub fn detach_buffer(&mut self, buffer_id: BufferId) -> Result<()> { match self.buffer(buffer_id)?.attachment { BufferAttachment::Attached(node_id) => self.close_node(node_id), @@ -1222,6 +1672,7 @@ impl ServerState { pub fn focus_leaf(&mut self, session_id: SessionId, leaf_id: NodeId) -> Result<()> { self.ensure_leaf_belongs_to(leaf_id, session_id)?; self.ensure_leaf_is_focusable(session_id, leaf_id)?; + let buffer_id = self.buffer_view_buffer_id(leaf_id)?; self.clear_session_focus(session_id)?; self.set_leaf_focus(leaf_id, true)?; @@ -1259,6 +1710,8 @@ impl ServerState { child = parent; } + self.set_buffer_activity(buffer_id, ActivityState::Idle)?; + Ok(()) } @@ -1317,6 +1770,7 @@ impl ServerState { self.set_parent(child, None)?; self.remove_subtree_nodes(child)?; self.normalize_upwards(tabs_id)?; + self.normalize_zoomed_node(session_id)?; self.heal_focus(session_id) } @@ -1339,6 +1793,7 @@ impl ServerState { ))); } + self.normalize_zoomed_node(session_id)?; self.heal_focus(session_id) } @@ -1371,6 +1826,7 @@ impl ServerState { } else { self.heal_focus(session_id)?; } + self.normalize_zoomed_node(session_id)?; Ok(()) } @@ -1438,9 +1894,35 @@ impl ServerState { ))); } } + + if let Some(zoomed_node) = session.zoomed_node { + if !self.nodes.contains_key(&zoomed_node) { + return Err(MuxError::conflict(format!( + "zoomed node {zoomed_node} is missing from session {}", + session.id + ))); + } + if self.node(zoomed_node)?.session_id() != session.id { + return Err(MuxError::conflict(format!( + "zoomed node {zoomed_node} belongs to the wrong session" + ))); + } + if !self.is_node_visible_in_session(session.id, zoomed_node)? { + return Err(MuxError::conflict(format!( + "zoomed node {zoomed_node} is not visible in session {}", + session.id + ))); + } + } } - if seen.len() != self.nodes.len() { + let unseen_invalid = self + .nodes + .keys() + .copied() + .filter(|node_id| !seen.contains(node_id)) + .find(|node_id| !self.is_detached_empty_tabs_node(*node_id)); + if unseen_invalid.is_some() { return Err(MuxError::conflict(format!( "orphaned node(s) detected: visited {} of {} node(s)", seen.len(), @@ -1494,6 +1976,7 @@ impl ServerState { }), ); self.session_mut(session_id)?.root_node = new_root; + self.session_mut(session_id)?.zoomed_node = None; self.heal_focus(session_id) } @@ -1543,6 +2026,22 @@ impl ServerState { Ok(()) } + fn refocus_zoomed_subtree(&mut self, session_id: SessionId, node_id: NodeId) -> Result<()> { + self.ensure_node_visible_in_session(session_id, node_id)?; + if let Some(focused_leaf) = self.session(session_id)?.focused_leaf + && self.node_is_descendant_of(focused_leaf, node_id)? + { + return Ok(()); + } + + if let Some(focus_candidate) = self.resolve_focus_candidate(node_id)? { + self.focus_leaf(session_id, focus_candidate)?; + } else { + self.clear_session_focus(session_id)?; + } + Ok(()) + } + fn resolve_floating_focus(&self, floating_id: FloatingId) -> Result> { let floating = self.floating_window(floating_id)?; if !floating.visible { @@ -1658,6 +2157,292 @@ impl ServerState { self.ensure_leaf(node_id) } + fn shared_parent(&self, first_node_id: NodeId, second_node_id: NodeId) -> Result { + if first_node_id == second_node_id { + return Err(MuxError::invalid_input( + "sibling operations require two distinct nodes", + )); + } + let first_parent = self.node_parent(first_node_id)?.ok_or_else(|| { + MuxError::invalid_input(format!("node {first_node_id} has no parent")) + })?; + let second_parent = self.node_parent(second_node_id)?.ok_or_else(|| { + MuxError::invalid_input(format!("node {second_node_id} has no parent")) + })?; + if first_parent != second_parent { + return Err(MuxError::conflict( + "node ergonomics are restricted to siblings with the same parent".to_owned(), + )); + } + Ok(first_parent) + } + + fn reorder_sibling_node( + &mut self, + node_id: NodeId, + sibling_node_id: NodeId, + before: bool, + ) -> Result<()> { + if node_id == sibling_node_id { + return Ok(()); + } + let parent_id = self.shared_parent(node_id, sibling_node_id)?; + match self.node_mut(parent_id)? { + Node::Split(split) => { + let from = split + .children + .iter() + .position(|child| *child == node_id) + .ok_or_else(|| { + MuxError::not_found(format!("node {node_id} is not in split {parent_id}")) + })?; + let target = split + .children + .iter() + .position(|child| *child == sibling_node_id) + .ok_or_else(|| { + MuxError::not_found(format!( + "node {sibling_node_id} is not in split {parent_id}" + )) + })?; + let child = split.children.remove(from); + let size = split.sizes.remove(from); + let mut insert_at = target; + if from < target { + insert_at = insert_at.saturating_sub(1); + } + if !before { + insert_at = insert_at.saturating_add(1); + } + split.children.insert(insert_at, child); + split.sizes.insert(insert_at, size); + } + Node::Tabs(tabs) => { + let from = tabs + .tabs + .iter() + .position(|tab| tab.child == node_id) + .ok_or_else(|| { + MuxError::not_found(format!("node {node_id} is not in tabs {parent_id}")) + })?; + let target = tabs + .tabs + .iter() + .position(|tab| tab.child == sibling_node_id) + .ok_or_else(|| { + MuxError::not_found(format!( + "node {sibling_node_id} is not in tabs {parent_id}" + )) + })?; + let tab = tabs.tabs.remove(from); + let mut insert_at = target; + if from < target { + insert_at = insert_at.saturating_sub(1); + } + if !before { + insert_at = insert_at.saturating_add(1); + } + tabs.tabs.insert(insert_at, tab); + if tabs.active == from { + tabs.active = insert_at; + } else if from < tabs.active && insert_at >= tabs.active { + tabs.active = tabs.active.saturating_sub(1); + } else if from > tabs.active && insert_at <= tabs.active { + tabs.active = tabs.active.saturating_add(1); + } + } + Node::BufferView(_) => { + return Err(MuxError::invalid_input( + "buffer views do not have sibling children", + )); + } + } + Ok(()) + } + + fn detach_node_from_owner( + &mut self, + node_id: NodeId, + ) -> Result<(Option, Option)> { + if self.is_session_root(node_id) { + return Err(MuxError::conflict( + "session root cannot be broken out of its owner".to_owned(), + )); + } + if let Some(parent_id) = self.node_parent(node_id)? { + self.remove_child(parent_id, node_id)?; + return Ok((Some(parent_id), None)); + } + if let Some(floating_id) = self.floating_id_by_root(node_id) { + let _ = self.remove_floating_window(floating_id)?; + return Ok((None, Some(floating_id))); + } + Err(MuxError::invalid_input(format!( + "node {node_id} has no owning container" + ))) + } + + fn capture_node_owner_snapshot(&self, node_id: NodeId) -> Result { + if let Some(parent_id) = self.node_parent(node_id)? { + let slot = match self.node(parent_id)? { + Node::Split(split) => { + let index = split + .children + .iter() + .position(|child| *child == node_id) + .ok_or_else(|| { + MuxError::not_found(format!( + "node {node_id} is not a child of parent {parent_id}" + )) + })?; + ParentSlotSnapshot::Split { + index, + size: split.sizes.get(index).copied().unwrap_or(1), + last_focused_descendant: split.last_focused_descendant, + } + } + Node::Tabs(tabs) => { + let index = tabs + .tabs + .iter() + .position(|tab| tab.child == node_id) + .ok_or_else(|| { + MuxError::not_found(format!( + "node {node_id} is not a child of parent {parent_id}" + )) + })?; + ParentSlotSnapshot::Tabs { + index, + tab: tabs.tabs[index].clone(), + active: tabs.active, + last_focused_descendant: tabs.last_focused_descendant, + } + } + Node::BufferView(_) => { + return Err(MuxError::invalid_input( + "buffer views do not own child nodes".to_owned(), + )); + } + }; + return Ok(NodeOwnerSnapshot::Parent { parent_id, slot }); + } + if let Some(floating_id) = self.floating_id_by_root(node_id) { + let window = self.floating_window(floating_id)?.clone(); + let session = self.session(window.session_id)?; + let index = session + .floating + .iter() + .position(|candidate| *candidate == floating_id) + .ok_or_else(|| { + MuxError::not_found(format!( + "floating window {floating_id} is not registered in session {}", + window.session_id + )) + })?; + return Ok(NodeOwnerSnapshot::Floating { + window, + index, + focused_floating: session.focused_floating, + }); + } + Err(MuxError::invalid_input(format!( + "node {node_id} has no owning container" + ))) + } + + fn rollback_detached_node(&mut self, node_id: NodeId, owner: NodeOwnerSnapshot) -> Result<()> { + if self.node_parent(node_id)?.is_some() || self.floating_id_by_root(node_id).is_some() { + let _ = self.detach_node_from_owner(node_id)?; + } + self.restore_node_owner(node_id, owner) + } + + fn restore_node_owner(&mut self, node_id: NodeId, owner: NodeOwnerSnapshot) -> Result<()> { + match owner { + NodeOwnerSnapshot::Parent { parent_id, slot } => { + match slot { + ParentSlotSnapshot::Split { + index, + size, + last_focused_descendant, + } => { + let split = match self.node_mut(parent_id)? { + Node::Split(split) => split, + _ => return Err(MuxError::invalid_input("node is not a split")), + }; + let insert_index = index.min(split.children.len()); + split.children.insert(insert_index, node_id); + split + .sizes + .insert(insert_index.min(split.sizes.len()), size); + split.last_focused_descendant = last_focused_descendant; + } + ParentSlotSnapshot::Tabs { + index, + tab, + active, + last_focused_descendant, + } => { + let tabs = match self.node_mut(parent_id)? { + Node::Tabs(tabs) => tabs, + _ => { + return Err(MuxError::invalid_input( + "node is not a tabs container", + )); + } + }; + let insert_index = index.min(tabs.tabs.len()); + tabs.tabs.insert(insert_index, tab); + tabs.active = active.min(tabs.tabs.len().saturating_sub(1)); + tabs.last_focused_descendant = last_focused_descendant; + } + } + self.set_parent(node_id, Some(parent_id)) + } + NodeOwnerSnapshot::Floating { + window, + index, + focused_floating, + } => { + let session_id = window.session_id; + let floating_id = window.id; + self.floating.insert(floating_id, window); + let session = self.session_mut(session_id)?; + let insert_index = index.min(session.floating.len()); + session.floating.insert(insert_index, floating_id); + session.focused_floating = focused_floating; + self.set_parent(node_id, None) + } + } + } + + fn nearest_tabs_ancestor(&self, mut node_id: Option) -> Result> { + while let Some(current) = node_id { + if matches!(self.node(current)?, Node::Tabs(_)) { + return Ok(Some(current)); + } + node_id = self.node_parent(current)?; + } + Ok(None) + } + + fn normalize_zoomed_node(&mut self, session_id: SessionId) -> Result<()> { + let keep = match self.session(session_id)?.zoomed_node { + Some(node_id) + if self + .nodes + .get(&node_id) + .is_some_and(|node| node.session_id() == session_id) + && self.is_node_visible_in_session(session_id, node_id)? => + { + Some(node_id) + } + _ => None, + }; + self.session_mut(session_id)?.zoomed_node = keep; + Ok(()) + } + fn is_session_root(&self, node_id: NodeId) -> bool { self.sessions .values() @@ -1671,7 +2456,7 @@ impl ServerState { .map(|floating| floating.id) } - fn floating_id_for_node(&self, node_id: NodeId) -> Result> { + pub fn floating_id_for_node(&self, node_id: NodeId) -> Result> { let root = self.top_root_for_node(node_id)?; Ok(self.floating_id_by_root(root)) } @@ -1721,6 +2506,15 @@ impl ServerState { Ok(false) } + fn ensure_node_visible_in_session(&self, session_id: SessionId, node_id: NodeId) -> Result<()> { + if self.is_node_visible_in_session(session_id, node_id)? { + return Ok(()); + } + Err(MuxError::invalid_input(format!( + "node {node_id} is not visible in session {session_id}" + ))) + } + fn subtree_contains(&self, root_id: NodeId, needle: NodeId) -> Result { if root_id == needle { return Ok(true); @@ -1953,7 +2747,7 @@ impl ServerState { } if let Node::BufferView(leaf) = self.node(node_id)? { - self.detach_buffer_raw(leaf.buffer_id)?; + self.detach_buffer_if_attached_to(leaf.buffer_id, leaf.id)?; } self.nodes.remove(&node_id); @@ -1965,21 +2759,96 @@ impl ServerState { Ok(()) } + fn detach_buffer_if_attached_to(&mut self, buffer_id: BufferId, node_id: NodeId) -> Result<()> { + if matches!( + self.buffer(buffer_id), + Ok(buffer) if buffer.attachment == BufferAttachment::Attached(node_id) + ) { + self.detach_buffer_raw(buffer_id)?; + } + Ok(()) + } + fn discard_buffer_view(&mut self, node_id: NodeId) { if self.node_parent(node_id).ok().flatten().is_some() && self.close_node(node_id).is_ok() { return; } - let buffer_id = match self.node(node_id) { - Ok(Node::BufferView(leaf)) => Some(leaf.buffer_id), + let buffer = match self.node(node_id) { + Ok(Node::BufferView(leaf)) => Some((leaf.buffer_id, leaf.id)), _ => None, }; - if let Some(buffer_id) = buffer_id { - let _ = self.detach_buffer_raw(buffer_id); + if let Some((buffer_id, leaf_id)) = buffer { + let _ = self.detach_buffer_if_attached_to(buffer_id, leaf_id); } self.nodes.remove(&node_id); } + fn default_floating_dimension(total: u16, padding: u16, min: u16, max: u16) -> u16 { + let available = total.saturating_sub(padding).max(1); + available.min(max).max(min.min(total.max(1))) + } + + fn visible_node_size(&self, node_id: NodeId) -> Result> { + match self.node(node_id)? { + Node::BufferView(node) => Ok(Some(node.view.last_render_size)), + Node::Split(split) => { + let child_sizes = split + .children + .iter() + .map(|child_id| self.visible_node_size(*child_id)) + .collect::>>()? + .into_iter() + .flatten() + .collect::>(); + if child_sizes.is_empty() { + return Ok(None); + } + let divider_count = + u16::try_from(child_sizes.len().saturating_sub(1)).unwrap_or(u16::MAX); + let (cols, rows) = match split.direction { + SplitDirection::Horizontal => ( + child_sizes.iter().map(|size| size.cols).max().unwrap_or(0), + child_sizes + .iter() + .fold(0_u16, |total, size| total.saturating_add(size.rows)) + .saturating_add(divider_count), + ), + SplitDirection::Vertical => ( + child_sizes + .iter() + .fold(0_u16, |total, size| total.saturating_add(size.cols)) + .saturating_add(divider_count), + child_sizes.iter().map(|size| size.rows).max().unwrap_or(0), + ), + }; + Ok(Some(PtySize { + cols, + rows, + pixel_width: 0, + pixel_height: 0, + })) + } + Node::Tabs(tabs) => { + let Some(active) = tabs.tabs.get(tabs.active) else { + return Ok(None); + }; + self.visible_node_size(active.child) + } + } + } + + fn node_is_descendant_of(&self, node_id: NodeId, ancestor_id: NodeId) -> Result { + let mut current = Some(node_id); + while let Some(candidate) = current { + if candidate == ancestor_id { + return Ok(true); + } + current = self.node_parent(candidate)?; + } + Ok(false) + } + fn ensure_leaf_is_focusable(&self, session_id: SessionId, leaf_id: NodeId) -> Result<()> { let root = self.top_root_for_node(leaf_id)?; if root == self.session(session_id)?.root_node { @@ -2020,7 +2889,7 @@ impl ServerState { session_id: SessionId, node_id: NodeId, expected_parent: Option, - is_session_root: bool, + _is_session_root: bool, seen: &mut BTreeSet, ) -> Result<()> { let node = self.node(node_id)?; @@ -2060,11 +2929,6 @@ impl ServerState { } } Node::Tabs(tabs) => { - if !is_session_root && tabs.tabs.is_empty() { - return Err(MuxError::conflict(format!( - "tabs node {node_id} must not be empty" - ))); - } if tabs.tabs.is_empty() { if tabs.active != 0 { return Err(MuxError::conflict(format!( @@ -2085,6 +2949,17 @@ impl ServerState { Ok(()) } + fn is_detached_empty_tabs_node(&self, node_id: NodeId) -> bool { + matches!( + self.nodes.get(&node_id), + Some(Node::Tabs(tabs)) + if tabs.parent.is_none() + && tabs.tabs.is_empty() + && self.floating_id_by_root(node_id).is_none() + && !self.sessions.values().any(|session| session.root_node == node_id) + ) + } + fn session_mut(&mut self, session_id: SessionId) -> Result<&mut Session> { self.sessions .get_mut(&session_id) diff --git a/crates/embers-server/src/terminal_backend.rs b/crates/embers-server/src/terminal_backend.rs index df25749..796569d 100644 --- a/crates/embers-server/src/terminal_backend.rs +++ b/crates/embers-server/src/terminal_backend.rs @@ -38,6 +38,11 @@ pub enum BackendDamage { Partial(Vec), } +/// Terminal emulation boundary used by the runtime keeper. +/// +/// Raw PTY bytes are routed through `RawByteRouter` and then ingested here. The backend owns +/// terminal parsing, alternate-screen state, scrollback, snapshots, cursor metadata, and render +/// damage tracking. pub trait TerminalBackend: Send { fn ingest_bytes(&mut self, bytes: &[u8]); fn resize(&mut self, size: PtySize); @@ -58,10 +63,18 @@ pub trait TerminalBackend: Send { pub struct RawByteRouter; impl RawByteRouter { + /// Route client-originated bytes before they reach the PTY. + /// + /// The current implementation is intentionally passthrough, but the method is the explicit + /// seam for future prefix/passthrough-aware interception. pub fn route_input(&self, bytes: Vec) -> Vec { bytes } + /// Route PTY output bytes before terminal emulation. + /// + /// Today this forwards output directly into the backend, making the raw-routing seam explicit + /// without introducing policy beyond passthrough. pub fn route_output(&mut self, backend: &mut dyn TerminalBackend, bytes: &[u8]) { backend.ingest_bytes(bytes); } @@ -346,8 +359,73 @@ impl TerminalBackend for AlacrittyTerminalBackend { #[cfg(test)] mod tests { - use super::{AlacrittyTerminalBackend, BackendDamage, TerminalBackend}; - use embers_core::{ActivityState, CursorShape, PtySize}; + use std::path::PathBuf; + + use super::{ + AlacrittyTerminalBackend, BackendDamage, BackendMetadata, BackendScrollbackSlice, + RawByteRouter, TerminalBackend, + }; + use embers_core::{ActivityState, CursorShape, PtySize, TerminalSnapshot}; + + #[derive(Default)] + struct StubBackend { + ingested: Vec, + } + + impl TerminalBackend for StubBackend { + fn ingest_bytes(&mut self, bytes: &[u8]) { + self.ingested.extend_from_slice(bytes); + } + + fn resize(&mut self, _size: PtySize) {} + + fn visible_snapshot( + &self, + sequence: u64, + size: PtySize, + cwd: Option, + ) -> embers_core::TerminalSnapshot { + let mut snapshot = embers_core::TerminalSnapshot::from_lines( + sequence, + size, + [String::from_utf8_lossy(&self.ingested).into_owned()], + ); + snapshot.cwd = cwd; + snapshot + } + + fn capture_scrollback(&self) -> Vec { + vec![String::from_utf8_lossy(&self.ingested).into_owned()] + } + + fn capture_scrollback_slice( + &self, + start_line: u64, + _line_count: u32, + ) -> BackendScrollbackSlice { + BackendScrollbackSlice { + start_line, + total_lines: 1, + lines: vec![String::from_utf8_lossy(&self.ingested).into_owned()], + } + } + + fn metadata(&self) -> BackendMetadata { + BackendMetadata::default() + } + + fn take_activity(&mut self) -> ActivityState { + ActivityState::Activity + } + + fn take_damage(&mut self) -> BackendDamage { + BackendDamage::None + } + } + + fn snapshot_lines(snapshot: TerminalSnapshot) -> Vec { + snapshot.lines.into_iter().map(|line| line.text).collect() + } #[test] fn visible_snapshot_extracts_plain_text_lines() { @@ -357,7 +435,7 @@ mod tests { backend.ingest_bytes(b"hello\r\nworld"); let snapshot = backend.visible_snapshot(3, PtySize::new(8, 3), None); - let lines: Vec<_> = snapshot.lines.into_iter().map(|line| line.text).collect(); + let lines = snapshot_lines(snapshot.clone()); assert_eq!(lines, vec!["hello", "world", ""]); assert_eq!(snapshot.total_lines, 3); assert_eq!(snapshot.viewport_top_line, 0); @@ -367,6 +445,50 @@ mod tests { )); } + #[test] + fn carriage_return_overwrites_cells_without_advancing_the_row() { + let mut backend = AlacrittyTerminalBackend::new(PtySize::new(8, 2)); + let _ = backend.take_damage(); + + backend.ingest_bytes(b"hello\rHEY"); + + let lines = snapshot_lines(backend.visible_snapshot(1, PtySize::new(8, 2), None)); + assert_eq!(lines, vec!["HEYlo", ""]); + } + + #[test] + fn automatic_wrap_moves_following_bytes_to_the_next_row() { + let mut backend = AlacrittyTerminalBackend::new(PtySize::new(4, 2)); + let _ = backend.take_damage(); + + backend.ingest_bytes(b"abcdX"); + + let lines = snapshot_lines(backend.visible_snapshot(1, PtySize::new(4, 2), None)); + assert_eq!(lines, vec!["abcd", "X"]); + } + + #[test] + fn erase_in_line_clears_trailing_cells_from_the_cursor() { + let mut backend = AlacrittyTerminalBackend::new(PtySize::new(6, 1)); + let _ = backend.take_damage(); + + backend.ingest_bytes(b"abcdef\rabc\x1b[K"); + + let lines = snapshot_lines(backend.visible_snapshot(1, PtySize::new(6, 1), None)); + assert_eq!(lines, vec!["abc"]); + } + + #[test] + fn clear_screen_resets_visible_cells_before_new_output() { + let mut backend = AlacrittyTerminalBackend::new(PtySize::new(6, 2)); + let _ = backend.take_damage(); + + backend.ingest_bytes(b"one\r\ntwo\x1b[2J\x1b[Hdone"); + + let lines = snapshot_lines(backend.visible_snapshot(1, PtySize::new(6, 2), None)); + assert_eq!(lines, vec!["done", ""]); + } + #[test] fn scrollback_capture_preserves_history_beyond_viewport() { let mut backend = AlacrittyTerminalBackend::new(PtySize::new(6, 2)); @@ -425,6 +547,26 @@ mod tests { assert!(metadata.bracketed_paste); } + #[test] + fn metadata_mode_flags_clear_when_disable_sequences_arrive() { + let mut backend = AlacrittyTerminalBackend::new(PtySize::new(10, 2)); + let _ = backend.take_damage(); + + backend.ingest_bytes(b"\x1b[?1049h\x1b[?1000h\x1b[?1004h\x1b[?2004h"); + let enabled = backend.metadata(); + assert!(enabled.alternate_screen); + assert!(enabled.mouse_reporting); + assert!(enabled.focus_reporting); + assert!(enabled.bracketed_paste); + + backend.ingest_bytes(b"\x1b[?1049l\x1b[?1000l\x1b[?1004l\x1b[?2004l"); + let disabled = backend.metadata(); + assert!(!disabled.alternate_screen); + assert!(!disabled.mouse_reporting); + assert!(!disabled.focus_reporting); + assert!(!disabled.bracketed_paste); + } + #[test] fn bell_activity_is_consumed_separately_from_metadata() { let mut backend = AlacrittyTerminalBackend::new(PtySize::new(10, 2)); @@ -440,4 +582,59 @@ mod tests { assert_eq!(metadata.title.as_deref(), Some("embers")); assert_eq!(backend.take_activity(), ActivityState::Activity); } + + #[test] + fn raw_byte_router_is_explicit_passthrough_for_input_and_output() { + let mut router = RawByteRouter; + let mut backend = StubBackend::default(); + let input = b"\x1b[200~paste\x1b[201~".to_vec(); + + assert_eq!(router.route_input(input.clone()), input); + + router.route_output(&mut backend, b"hello"); + router.route_output(&mut backend, b" world"); + + assert_eq!(backend.ingested, b"hello world"); + } + + #[test] + fn alternate_screen_visible_snapshot_tracks_active_screen_and_restores_primary_screen() { + let mut backend = AlacrittyTerminalBackend::new(PtySize::new(20, 4)); + let _ = backend.take_damage(); + + backend.ingest_bytes(b"main-one\r\nmain-two"); + backend.ingest_bytes(b"\x1b[?1049h\x1b[Halt-screen"); + + let alternate = backend.visible_snapshot(2, PtySize::new(20, 4), None); + let alternate_lines: Vec<_> = alternate + .lines + .iter() + .map(|line| line.text.as_str()) + .collect(); + assert!(alternate.modes.alternate_screen); + assert!( + alternate_lines + .iter() + .any(|line| line.contains("alt-screen")), + "alternate visible lines: {alternate_lines:?}" + ); + + backend.ingest_bytes(b"\x1b[?1049l"); + + let restored = backend.visible_snapshot(3, PtySize::new(20, 4), None); + let restored_lines: Vec<_> = restored + .lines + .iter() + .map(|line| line.text.as_str()) + .collect(); + assert!(!restored.modes.alternate_screen); + assert!( + restored_lines.iter().any(|line| line.contains("main-one")), + "restored visible lines: {restored_lines:?}" + ); + assert!( + restored_lines.iter().any(|line| line.contains("main-two")), + "restored visible lines: {restored_lines:?}" + ); + } } diff --git a/crates/embers-server/tests/buffer_lifecycle.rs b/crates/embers-server/tests/buffer_lifecycle.rs index 5da155f..ae8d5a3 100644 --- a/crates/embers-server/tests/buffer_lifecycle.rs +++ b/crates/embers-server/tests/buffer_lifecycle.rs @@ -106,6 +106,46 @@ fn closing_a_view_detaches_but_preserves_running_buffer() { )); } +#[test] +fn focusing_a_leaf_clears_recorded_activity() { + let mut state = ServerState::new(); + let session_id = state.create_session("main"); + let first_buffer = state.create_buffer("first", vec!["/bin/sh".to_owned()], None); + let first_view = state + .create_buffer_view(session_id, first_buffer) + .expect("create first view"); + state + .add_root_tab(session_id, "first", first_view) + .expect("attach first view"); + + let second_buffer = state.create_buffer("second", vec!["/bin/sh".to_owned()], None); + let second_view = state + .create_buffer_view(session_id, second_buffer) + .expect("create second view"); + state + .add_root_tab(session_id, "second", second_view) + .expect("attach second view"); + state + .focus_leaf(session_id, second_view) + .expect("focus second leaf"); + + state + .set_buffer_activity(first_buffer, ActivityState::Bell) + .expect("mark first buffer active"); + + state + .focus_leaf(session_id, first_view) + .expect("focus hidden first leaf"); + + assert_eq!( + state + .buffer(first_buffer) + .expect("first buffer exists") + .activity, + ActivityState::Idle + ); +} + #[test] fn resize_updates_buffer_size_for_attached_and_detached_buffers() { let mut state = ServerState::new(); @@ -135,3 +175,28 @@ fn resize_updates_buffer_size_for_attached_and_detached_buffers() { PtySize::new(90, 20) ); } + +#[test] +fn exited_detached_buffers_can_be_removed_cleanly() { + let mut state = ServerState::new(); + let buffer_id = state.create_buffer("shell", vec!["/bin/sh".to_owned()], None); + + state + .mark_buffer_running(buffer_id, Some(88)) + .expect("mark running"); + state + .mark_buffer_exited(buffer_id, Some(0)) + .expect("mark exited"); + + let removed = state + .remove_buffer(buffer_id) + .expect("remove detached exited buffer"); + assert!(matches!( + removed.state, + BufferState::Exited(ref exited) if exited.exit_code == Some(0) + )); + assert!(matches!( + state.buffer(buffer_id), + Err(MuxError::NotFound(_)) + )); +} diff --git a/crates/embers-server/tests/model_state.rs b/crates/embers-server/tests/model_state.rs index 64d1c31..50c4d9b 100644 --- a/crates/embers-server/tests/model_state.rs +++ b/crates/embers-server/tests/model_state.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use embers_core::{BufferId, FloatGeometry, NodeId, SessionId, SplitDirection}; +use embers_protocol::NodeJoinPlacement; use embers_server::{BufferAttachment, Node, ServerState}; use proptest::prelude::*; @@ -313,6 +314,367 @@ fn public_detach_buffer_closes_live_views() { state.validate().expect("state remains valid"); } +#[test] +fn zoom_toggle_tracks_session_zoomed_node_and_clears_on_close() { + let mut state = ServerState::new(); + let (session_id, _, leaf_id) = seed_single_leaf_session(&mut state, "alpha"); + + state.toggle_zoom_node(leaf_id).expect("zoom leaf"); + assert_eq!( + state.session(session_id).expect("session").zoomed_node, + Some(leaf_id) + ); + + state.close_node(leaf_id).expect("close zoomed leaf"); + assert_eq!( + state.session(session_id).expect("session").zoomed_node, + None + ); + state.validate().expect("state remains valid"); +} + +#[test] +fn zooming_moves_focus_into_the_zoomed_subtree() { + let mut state = ServerState::new(); + let (session_id, _, leaf_id) = seed_single_leaf_session(&mut state, "alpha"); + let detached = new_buffer(&mut state, "logs"); + + state + .join_buffer_at_node(leaf_id, detached, NodeJoinPlacement::Right) + .expect("join right"); + let detached_view = attached_view(&state, detached); + state + .focus_leaf(session_id, detached_view) + .expect("focus detached view"); + + state.zoom_node(leaf_id).expect("zoom leaf"); + + assert_eq!( + state.session(session_id).expect("session").zoomed_node, + Some(leaf_id) + ); + assert_eq!( + state.session(session_id).expect("session").focused_leaf, + Some(leaf_id) + ); + assert!(matches!( + state.node(leaf_id).expect("leaf"), + Node::BufferView(view) if view.view.focused + )); + assert!(matches!( + state.node(detached_view).expect("detached leaf"), + Node::BufferView(view) if !view.view.focused + )); + state.validate().expect("state remains valid"); +} + +#[test] +fn closing_zoomed_tab_clears_session_zoom() { + let mut state = ServerState::new(); + let (session_id, _, leaf_id) = seed_single_leaf_session(&mut state, "alpha"); + let (_, second_leaf) = new_leaf(&mut state, session_id, "second"); + state + .add_root_tab(session_id, "second", second_leaf) + .expect("add second root tab"); + let root_tabs = state.root_tabs(session_id).expect("root tabs"); + state.switch_tab(root_tabs, 0).expect("focus first tab"); + + state.toggle_zoom_node(leaf_id).expect("zoom leaf"); + state.close_tab(root_tabs, 0).expect("close zoomed tab"); + + assert_eq!( + state.session(session_id).expect("session").zoomed_node, + None + ); + assert_eq!( + state.session(session_id).expect("session").focused_leaf, + Some(second_leaf) + ); + assert_eq!( + state.session(session_id).expect("session").focused_floating, + None + ); + state.validate().expect("state remains valid"); +} + +#[test] +fn zooming_inactive_tab_is_rejected() { + let mut state = ServerState::new(); + let (session_id, _, first_leaf) = seed_single_leaf_session(&mut state, "alpha"); + let (_, second_leaf) = new_leaf(&mut state, session_id, "second"); + let root_tabs = state.root_tabs(session_id).expect("root tabs"); + state + .add_root_tab(session_id, "second", second_leaf) + .expect("add second root tab"); + + assert!(state.zoom_node(first_leaf).is_err()); + assert!(state.toggle_zoom_node(first_leaf).is_err()); + assert_eq!( + state.session(session_id).expect("session").zoomed_node, + None + ); + state.validate().expect("state remains valid"); + + state.switch_tab(root_tabs, 0).expect("focus first tab"); + state.zoom_node(first_leaf).expect("zoom visible tab"); + assert_eq!( + state.session(session_id).expect("session").zoomed_node, + Some(first_leaf) + ); +} + +#[test] +fn closing_zoomed_floating_clears_session_zoom() { + let mut state = ServerState::new(); + let (session_id, _, root_leaf) = seed_single_leaf_session(&mut state, "alpha"); + let popup_buffer = new_buffer(&mut state, "popup"); + let floating_id = state + .create_floating_from_buffer( + session_id, + popup_buffer, + FloatGeometry::new(5, 3, 40, 12), + Some("popup".to_owned()), + ) + .expect("create floating"); + let floating_root = state + .floating_window(floating_id) + .expect("floating") + .root_node; + + state + .toggle_zoom_node(floating_root) + .expect("zoom floating"); + state + .close_floating(floating_id) + .expect("close zoomed floating"); + + assert_eq!( + state.session(session_id).expect("session").zoomed_node, + None + ); + assert_eq!( + state.session(session_id).expect("session").focused_leaf, + Some(root_leaf) + ); + assert_eq!( + state.session(session_id).expect("session").focused_floating, + None + ); + state.validate().expect("state remains valid"); +} + +#[test] +fn swap_and_reorder_operate_only_on_siblings() { + let mut state = ServerState::new(); + let (session_id, _, leaf_id) = seed_single_leaf_session(&mut state, "alpha"); + let second = new_buffer(&mut state, "second"); + let third = new_buffer(&mut state, "third"); + let split_id = state + .split_leaf_with_new_buffer(leaf_id, SplitDirection::Vertical, second) + .expect("split root"); + let _second_leaf = attached_view(&state, second); + let third_leaf = state + .create_buffer_view(session_id, third) + .expect("third leaf"); + state + .wrap_node_in_split(split_id, SplitDirection::Vertical, third_leaf, false) + .expect("wrap split with third leaf"); + + let root = root_tab_child(&state, session_id, 0); + let split = match state.node(root).expect("root split") { + Node::Split(split) => split.clone(), + other => panic!("expected split, got {other:?}"), + }; + let first_child = split.children[0]; + let second_child = split.children[1]; + let non_sibling = match state.node(first_child).expect("nested split") { + Node::Split(split) => split.children[0], + other => panic!("expected nested split, got {other:?}"), + }; + + state + .swap_sibling_nodes(first_child, second_child) + .expect("swap siblings"); + let swapped_children = match state.node(root).expect("root split") { + Node::Split(split) => split.children.clone(), + other => panic!("expected split after swap, got {other:?}"), + }; + assert_eq!(swapped_children[0], second_child); + assert_eq!(swapped_children[1], first_child); + assert!( + state.swap_sibling_nodes(non_sibling, second_child).is_err(), + "non-siblings should not swap" + ); + assert!( + state.move_node_before(non_sibling, second_child).is_err(), + "non-siblings should not reorder" + ); + state + .move_node_before(first_child, second_child) + .expect("move sibling before"); + let restored_children = match state.node(root).expect("root split") { + Node::Split(split) => split.children.clone(), + other => panic!("expected split after reorder, got {other:?}"), + }; + assert_eq!(restored_children[0], first_child); + assert_eq!(restored_children[1], second_child); + state.validate().expect("state remains valid"); +} + +#[test] +fn break_node_to_floating_preserves_subtree_and_focuses_popup() { + let mut state = ServerState::new(); + let (session_id, _, leaf_id) = seed_single_leaf_session(&mut state, "alpha"); + let buffer_id = new_buffer(&mut state, "beta"); + state + .split_leaf_with_new_buffer(leaf_id, SplitDirection::Horizontal, buffer_id) + .expect("split root"); + let new_leaf = attached_view(&state, buffer_id); + + state.break_node(new_leaf, true).expect("break to floating"); + + let session = state.session(session_id).expect("session"); + assert_eq!(session.floating.len(), 1); + let floating = state + .floating_window(session.floating[0]) + .expect("floating exists"); + assert_eq!(floating.root_node, new_leaf); + assert_eq!(session.focused_floating, Some(floating.id)); + assert_eq!(session.focused_leaf, Some(new_leaf)); + match state.node(new_leaf).expect("new floating leaf exists") { + Node::BufferView(leaf) => assert!(leaf.view.focused), + other => panic!("expected floating root leaf, got {other:?}"), + } + state.validate().expect("state remains valid"); +} + +#[test] +fn breaking_existing_floating_to_floating_preserves_geometry_and_title() { + let mut state = ServerState::new(); + let (session_id, _, _) = seed_single_leaf_session(&mut state, "alpha"); + let (_, popup_leaf) = new_leaf(&mut state, session_id, "popup"); + let geometry = FloatGeometry::new(7, 8, 30, 12); + let floating_id = state + .create_floating_window_with_options( + session_id, + popup_leaf, + geometry, + Some("popup".to_owned()), + false, + true, + ) + .expect("create floating window"); + + state + .break_node(popup_leaf, true) + .expect("breaking an existing floating root to floating is a no-op"); + + let session = state.session(session_id).expect("session"); + assert_eq!(session.floating, vec![floating_id]); + let floating = state.floating_window(floating_id).expect("floating exists"); + assert_eq!(floating.root_node, popup_leaf); + assert_eq!(floating.geometry, geometry); + assert_eq!(floating.title.as_deref(), Some("popup")); + state.validate().expect("state remains valid"); +} + +#[test] +fn breaking_existing_tab_to_tab_preserves_order() { + let mut state = ServerState::new(); + let (session_id, _, first_leaf) = seed_single_leaf_session(&mut state, "alpha"); + let (_, second_leaf) = new_leaf(&mut state, session_id, "beta"); + state + .add_root_tab(session_id, "beta", second_leaf) + .expect("add second root tab"); + let root_tabs = state.root_tabs(session_id).expect("root tabs"); + let before = match state.node(root_tabs).expect("root tabs") { + Node::Tabs(tabs) => tabs.tabs.iter().map(|tab| tab.child).collect::>(), + other => panic!("expected root tabs, got {other:?}"), + }; + + state + .break_node(first_leaf, false) + .expect("breaking an existing tab to a tab is a no-op"); + + let after = match state.node(root_tabs).expect("root tabs") { + Node::Tabs(tabs) => tabs.tabs.iter().map(|tab| tab.child).collect::>(), + other => panic!("expected root tabs, got {other:?}"), + }; + assert_eq!(after, before); + state.validate().expect("state remains valid"); +} + +#[test] +fn join_buffer_at_node_can_insert_tabs_and_splits() { + let mut state = ServerState::new(); + let (session_id, _, leaf_id) = seed_single_leaf_session(&mut state, "alpha"); + let detached = new_buffer(&mut state, "tools"); + + state + .join_buffer_at_node(leaf_id, detached, NodeJoinPlacement::Right) + .expect("join right"); + let root = root_tab_child(&state, session_id, 0); + let detached_view = attached_view(&state, detached); + match state.node(root).expect("root") { + Node::Split(split) => assert_eq!(split.children, vec![leaf_id, detached_view]), + other => panic!("expected split root, got {other:?}"), + } + let target_root = root; + + let detached_again = state.create_buffer("logs", vec!["sh".to_owned()], None); + state + .join_buffer_at_node(root, detached_again, NodeJoinPlacement::TabAfter) + .expect("join after as tab"); + let root = session_root(&state, session_id); + let detached_again_view = attached_view(&state, detached_again); + let root_tabs = match state.node(root).expect("root") { + Node::Tabs(tabs) => tabs, + other => panic!("expected root tabs, got {other:?}"), + }; + let children = root_tabs + .tabs + .iter() + .map(|tab| tab.child) + .collect::>(); + assert_eq!(children, vec![target_root, detached_again_view]); + state.validate().expect("state remains valid"); +} + +#[test] +fn join_buffer_at_node_rejects_buffers_already_contained_by_target() { + let mut state = ServerState::new(); + let (session_id, buffer_id, leaf_id) = seed_single_leaf_session(&mut state, "alpha"); + + let error = state + .join_buffer_at_node(leaf_id, buffer_id, NodeJoinPlacement::Right) + .expect_err("joining a buffer into its own view should fail"); + + assert!(matches!(error, embers_core::MuxError::Conflict(_))); + assert_eq!(attached_view(&state, buffer_id), leaf_id); + assert_eq!(root_tab_child(&state, session_id, 0), leaf_id); + state.validate().expect("state remains valid"); +} + +#[test] +fn join_buffer_at_node_rejects_attached_buffers_from_other_sessions() { + let mut state = ServerState::new(); + let (target_session_id, _, target_leaf_id) = seed_single_leaf_session(&mut state, "alpha"); + let (source_session_id, source_buffer_id, source_leaf_id) = + seed_single_leaf_session(&mut state, "beta"); + let target_before = reachable_nodes(&state, target_session_id); + let source_before = reachable_nodes(&state, source_session_id); + + let error = state + .join_buffer_at_node(target_leaf_id, source_buffer_id, NodeJoinPlacement::Right) + .expect_err("cross-session rehoming should fail"); + + assert!(matches!(error, embers_core::MuxError::Conflict(_))); + assert_eq!(attached_view(&state, source_buffer_id), source_leaf_id); + assert_eq!(reachable_nodes(&state, target_session_id), target_before); + assert_eq!(reachable_nodes(&state, source_session_id), source_before); + state.validate().expect("state remains valid"); +} + #[test] fn focused_floating_transfers_back_to_root_when_closed() { let mut state = ServerState::new(); @@ -456,6 +818,98 @@ fn focus_leaf_rejects_hidden_floating_leaves_without_mutating_focus() { state.validate().expect("state remains valid"); } +#[test] +fn create_floating_window_rolls_back_when_focus_fails() { + let mut state = ServerState::new(); + let (session_id, _, root_leaf) = seed_single_leaf_session(&mut state, "alpha"); + let empty_tabs = state + .create_tabs_node(session_id, Vec::new(), 0) + .expect("create empty tabs root"); + + let error = state + .create_floating_window_with_options( + session_id, + empty_tabs, + FloatGeometry::new(4, 4, 20, 10), + Some("popup".to_owned()), + true, + true, + ) + .expect_err("empty floating root should not be focusable"); + + assert!(matches!(error, embers_core::MuxError::NotFound(_))); + assert_eq!( + state.session(session_id).expect("session").floating, + Vec::new() + ); + assert_eq!( + state.session(session_id).expect("session").focused_leaf, + Some(root_leaf) + ); + assert_eq!( + state.session(session_id).expect("session").focused_floating, + None + ); + assert!( + matches!(state.node(empty_tabs), Ok(Node::Tabs(_))), + "empty tabs node should remain a tabs container" + ); + assert_eq!( + state + .node_parent(empty_tabs) + .expect("empty tabs parent lookup"), + None + ); + assert_eq!( + state + .floating_id_for_node(empty_tabs) + .expect("floating lookup"), + None + ); + state.validate().expect("state should be valid"); +} + +#[test] +fn break_node_rolls_back_when_breaking_to_floating_cannot_focus() { + let mut state = ServerState::new(); + let (session_id, _, _) = seed_single_leaf_session(&mut state, "alpha"); + let empty_tabs = state + .create_tabs_node(session_id, Vec::new(), 0) + .expect("create empty tabs root"); + state + .add_root_tab(session_id, "scratch", empty_tabs) + .expect("attach empty tabs to root tabs"); + let focused_leaf_before = state.session(session_id).expect("session").focused_leaf; + + let error = state + .break_node(empty_tabs, true) + .expect_err("empty tabs cannot become a focused floating window"); + + assert!(matches!(error, embers_core::MuxError::NotFound(_))); + assert_eq!( + state.session(session_id).expect("session").floating, + Vec::new() + ); + assert_eq!( + state.session(session_id).expect("session").focused_leaf, + focused_leaf_before + ); + assert_eq!(root_tab_child(&state, session_id, 1), empty_tabs); + assert_eq!( + state + .node_parent(empty_tabs) + .expect("empty tabs parent lookup"), + state.root_tabs(session_id).ok() + ); + assert_eq!( + state + .floating_id_for_node(empty_tabs) + .expect("floating lookup"), + None + ); + state.validate().expect("state should be valid"); +} + #[test] fn add_tab_from_buffer_rolls_back_when_hidden_floating_cannot_focus() { let mut state = ServerState::new(); @@ -510,6 +964,126 @@ fn add_tab_from_buffer_rolls_back_when_hidden_floating_cannot_focus() { state.validate().expect("state remains valid"); } +#[test] +fn break_node_into_hidden_tabs_branch_preserves_focus() { + let mut state = ServerState::new(); + let (session_id, _, root_leaf) = seed_single_leaf_session(&mut state, "alpha"); + let (_, split_left) = new_leaf(&mut state, session_id, "split-left"); + let (_, split_right) = new_leaf(&mut state, session_id, "split-right"); + let split_root = state + .create_split_node( + session_id, + SplitDirection::Vertical, + vec![split_left, split_right], + ) + .expect("create hidden split"); + let (_, other_leaf) = new_leaf(&mut state, session_id, "other"); + let hidden_tabs = state + .create_tabs_node( + session_id, + vec![ + embers_server::TabEntry::new("split", split_root), + embers_server::TabEntry::new("other", other_leaf), + ], + 0, + ) + .expect("create hidden tabs"); + let floating_id = state + .create_floating_window_with_options( + session_id, + hidden_tabs, + FloatGeometry::new(4, 4, 20, 10), + Some("popup".to_owned()), + false, + true, + ) + .expect("create floating window"); + state + .floating + .get_mut(&floating_id) + .expect("floating exists") + .visible = false; + state + .focus_leaf(session_id, root_leaf) + .expect("refocus root"); + + state + .break_node(split_right, false) + .expect("breaking a hidden node into a hidden tabs branch should succeed"); + + let hidden_children = match state.node(hidden_tabs).expect("hidden tabs") { + Node::Tabs(tabs) => tabs.tabs.iter().map(|tab| tab.child).collect::>(), + other => panic!("expected hidden tabs, got {other:?}"), + }; + assert!(hidden_children.contains(&split_left)); + assert!(hidden_children.contains(&split_right)); + assert!(hidden_children.contains(&other_leaf)); + assert_eq!( + state.session(session_id).expect("session").focused_leaf, + Some(root_leaf) + ); + assert_eq!( + state + .floating_id_for_node(split_right) + .expect("floating lookup"), + Some(floating_id) + ); + state.validate().expect("state remains valid"); +} + +#[test] +fn join_buffer_at_node_into_hidden_floating_preserves_focus() { + let mut state = ServerState::new(); + let (session_id, _, root_leaf) = seed_single_leaf_session(&mut state, "alpha"); + let (popup_buffer, popup_leaf) = new_leaf(&mut state, session_id, "popup"); + let floating_id = state + .create_floating_window_with_options( + session_id, + popup_leaf, + FloatGeometry::new(4, 4, 20, 10), + Some("popup".to_owned()), + false, + true, + ) + .expect("create floating window"); + state + .floating + .get_mut(&floating_id) + .expect("floating exists") + .visible = false; + + let detached_buffer = new_buffer(&mut state, "detached"); + + state + .join_buffer_at_node(popup_leaf, detached_buffer, NodeJoinPlacement::Right) + .expect("hidden floating leaf should allow joining without stealing focus"); + + let detached_view = attached_view(&state, detached_buffer); + let floating_root = state + .floating_window(floating_id) + .expect("floating") + .root_node; + match state.node(floating_root).expect("floating root") { + Node::Split(split) => assert_eq!(split.children, vec![popup_leaf, detached_view]), + other => panic!("expected floating split root, got {other:?}"), + } + assert_eq!( + state.buffer(popup_buffer).expect("popup buffer").attachment, + BufferAttachment::Attached(popup_leaf) + ); + assert_eq!( + state.session(session_id).expect("session").focused_leaf, + Some(root_leaf) + ); + assert_eq!( + state + .floating_id_for_node(detached_view) + .expect("floating lookup"), + Some(floating_id) + ); + state.validate().expect("state remains valid"); +} + #[test] fn root_tabs_are_preserved_when_last_tab_closes() { let mut state = ServerState::new(); diff --git a/crates/embers-test-support/Cargo.toml b/crates/embers-test-support/Cargo.toml index b3961c0..d4adc24 100644 --- a/crates/embers-test-support/Cargo.toml +++ b/crates/embers-test-support/Cargo.toml @@ -6,15 +6,20 @@ license.workspace = true rust-version.workspace = true version.workspace = true +[lib] +doctest = false + [dependencies] assert_cmd.workspace = true embers-core = { path = "../embers-core" } embers-protocol = { path = "../embers-protocol" } embers-server = { path = "../embers-server" } +fastrand.workspace = true libc.workspace = true portable-pty.workspace = true tempfile.workspace = true tokio.workspace = true +tracing.workspace = true [[test]] name = "integration" diff --git a/crates/embers-test-support/src/lib.rs b/crates/embers-test-support/src/lib.rs index 0338336..83a0a81 100644 --- a/crates/embers-test-support/src/lib.rs +++ b/crates/embers-test-support/src/lib.rs @@ -6,6 +6,6 @@ mod test_lock; pub use cli::{cargo_bin, cargo_bin_path}; pub use protocol::TestConnection; -pub use pty::PtyHarness; +pub use pty::{PtyHarness, is_pty_available}; pub use server::TestServer; pub use test_lock::{InterprocessTestLock, acquire_test_lock}; diff --git a/crates/embers-test-support/src/pty.rs b/crates/embers-test-support/src/pty.rs index 6093b45..43528b7 100644 --- a/crates/embers-test-support/src/pty.rs +++ b/crates/embers-test-support/src/pty.rs @@ -1,16 +1,34 @@ use std::io::{Read, Write}; +use std::sync::OnceLock; use std::sync::mpsc::{self, Receiver}; use std::thread; use std::time::{Duration, Instant}; use embers_core::{MuxError, PtySize, Result}; use portable_pty::{ - CommandBuilder, MasterPty, NativePtySystem, PtySize as PortableSize, PtySystem, + CommandBuilder, MasterPty, NativePtySystem, PtyPair, PtySize as PortableSize, PtySystem, }; const OUTPUT_TAIL_CHARS: usize = 2000; +static PTY_AVAILABLE: OnceLock = OnceLock::new(); + +/// Checks if PTY devices are available on this system. +/// Returns true if we can successfully open a PTY pair. +pub fn is_pty_available() -> bool { + if let Some(available) = PTY_AVAILABLE.get() { + return *available; + } + match PtyHarness::openpty_with_retry(PtySize::new(80, 24)) { + Ok(_) => { + let _ = PTY_AVAILABLE.set(true); + true + } + Err(_) => false, + } +} pub struct PtyHarness { + #[allow(dead_code)] master: Box, child: Box, writer: Box, @@ -19,26 +37,54 @@ pub struct PtyHarness { } impl PtyHarness { - pub fn spawn(command: &str, args: &[&str], size: PtySize) -> Result { + /// Maximum number of retries for PTY allocation failures + const MAX_RETRIES: usize = 3; + + /// Initial delay between retries on PTY allocation failure + const RETRY_DELAY: Duration = Duration::from_millis(100); + + /// Maximum random jitter added to retry backoff so concurrent probes do not align exactly. + const RETRY_JITTER_MS: u64 = 25; + + fn openpty_with_retry(size: PtySize) -> Result { let pty_system = NativePtySystem::default(); - let pair = pty_system - .openpty(PortableSize { + let mut last_error = None; + + for attempt in 0..=Self::MAX_RETRIES { + match pty_system.openpty(PortableSize { rows: size.rows, cols: size.cols, pixel_width: size.pixel_width, pixel_height: size.pixel_height, - }) - .map_err(|error| MuxError::pty(error.to_string()))?; + }) { + Ok(pair) => return Ok(pair), + Err(error) => { + last_error = Some(error); + if attempt < Self::MAX_RETRIES { + let base_delay = Self::RETRY_DELAY * (attempt + 1) as u32; + let jitter = + Duration::from_millis(fastrand::u64(..Self::RETRY_JITTER_MS.max(1))); + thread::sleep(base_delay + jitter); + } + } + } + } + + Err(MuxError::pty(format!( + "failed to openpty after {} attempts: {}", + Self::MAX_RETRIES + 1, + // Safe: every failed attempt stores the last encountered PTY allocation error. + last_error.expect("openpty retry loop must capture an error before failing") + ))) + } + pub fn spawn(command: &str, args: &[&str], size: PtySize) -> Result { let mut command_builder = CommandBuilder::new(command); for arg in args { command_builder.arg(arg); } - let child = pair - .slave - .spawn_command(command_builder) - .map_err(|error| MuxError::pty(error.to_string()))?; + let pair = Self::openpty_with_retry(size)?; let mut reader = pair .master .try_clone_reader() @@ -47,22 +93,38 @@ impl PtyHarness { .master .take_writer() .map_err(|error| MuxError::pty(error.to_string()))?; + let mut child = pair + .slave + .spawn_command(command_builder) + .map_err(|error| MuxError::pty(error.to_string()))?; let (tx, rx) = mpsc::channel(); - let reader_join = thread::spawn(move || { - let mut buffer = [0_u8; 1024]; - loop { - match reader.read(&mut buffer) { - Ok(0) => break, - Ok(read) => { - if tx.send(buffer[..read].to_vec()).is_err() { - break; + let reader_join = thread::Builder::new() + .name("pty-reader".to_owned()) + .spawn(move || { + let mut buffer = [0_u8; 1024]; + loop { + match reader.read(&mut buffer) { + Ok(0) => break, + Ok(read) => { + if tx.send(buffer[..read].to_vec()).is_err() { + break; + } } + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, + Err(_) => break, } - Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, - Err(_) => break, } - } - }); + }) + .map_err(|error| { + let mut message = format!("failed to spawn PTY reader thread: {error}"); + if let Err(kill_error) = child.kill() { + message.push_str(&format!("; failed to kill child: {kill_error}")); + } + if let Err(wait_error) = child.wait() { + message.push_str(&format!("; failed to reap child: {wait_error}")); + } + MuxError::pty(message) + })?; Ok(Self { master: pair.master, @@ -141,7 +203,6 @@ impl PtyHarness { if let Some(join) = self.reader_join.take() { let _ = join.join(); } - let _ = &self.master; Ok(()) } } @@ -149,6 +210,7 @@ impl PtyHarness { impl Drop for PtyHarness { fn drop(&mut self) { let _ = self.child.kill(); + let _ = self.child.wait(); if let Some(join) = self.reader_join.take() { let _ = join.join(); } diff --git a/crates/embers-test-support/src/server.rs b/crates/embers-test-support/src/server.rs index 9935586..5d2a551 100644 --- a/crates/embers-test-support/src/server.rs +++ b/crates/embers-test-support/src/server.rs @@ -1,19 +1,24 @@ use std::path::Path; +use std::process::Command; +use std::time::Duration; use embers_core::{Result, init_test_tracing}; use embers_server::{Server, ServerConfig, ServerHandle}; use tempfile::TempDir; +const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); + #[derive(Debug)] pub struct TestServer { socket_path: std::path::PathBuf, - tempdir: TempDir, + _tempdir: TempDir, handle: Option, } impl TestServer { pub async fn start() -> Result { init_test_tracing(); + reap_stale_helper_processes(); let tempdir = tempfile::tempdir()?; let socket_path = tempdir.path().join("mux.sock"); @@ -23,7 +28,7 @@ impl TestServer { Ok(Self { socket_path, - tempdir, + _tempdir: tempdir, handle: Some(handle), }) } @@ -32,12 +37,161 @@ impl TestServer { &self.socket_path } + /// Shuts down the server and kills any orphaned embers helper processes that + /// were spawned for this socket during the test. pub async fn shutdown(mut self) -> Result<()> { - let _ = self.tempdir.path(); + let mut shutdown_error = None; if let Some(handle) = self.handle.take() { - handle.shutdown().await - } else { - Ok(()) + match tokio::time::timeout(SHUTDOWN_TIMEOUT, handle.shutdown()).await { + Ok(Ok(())) => {} + Ok(Err(e)) => { + tracing::warn!(error = %e, "TestServer shutdown returned error"); + shutdown_error = Some(e); + } + Err(_) => { + tracing::warn!("TestServer shutdown timed out after {:?}", SHUTDOWN_TIMEOUT); + shutdown_error = Some(embers_core::MuxError::timeout(format!( + "TestServer shutdown timed out after {:?}", + SHUTDOWN_TIMEOUT + ))); + } + } + } + self.kill_orphaned_processes(); + match shutdown_error { + Some(error) => Err(error), + None => Ok(()), + } + } + + /// Kill any orphaned embers helper processes that were spawned for this + /// server's socket but are no longer needed. + fn kill_orphaned_processes(&self) { + let socket_path_str = self.socket_path.to_string_lossy(); + let runtime_dir = self.socket_path.with_extension("runtimes"); + let runtime_dir_str = runtime_dir.to_string_lossy(); + let pid_path = self.socket_path.with_extension("pid"); + let orphaned = collect_orphaned_processes(&socket_path_str, &runtime_dir_str); + let mut handled = Vec::new(); + + if let Ok(pid_str) = std::fs::read_to_string(&pid_path) + && let Ok(pid) = pid_str.trim().parse::() + && orphaned + .iter() + .any(|(candidate_pid, _)| *candidate_pid == pid) + { + terminate_process(pid); + handled.push(pid); + } + + for (pid, kind) in orphaned { + if handled.contains(&pid) { + continue; + } + terminate_process(pid); + if kind == "server" { + tracing::debug!(pid, "killed orphaned __serve process"); + } else { + tracing::debug!(pid, "killed orphaned __runtime-keeper process"); + } } + + let _ = std::fs::remove_file(&pid_path); + let _ = std::fs::remove_dir_all(&runtime_dir); + } +} + +impl Drop for TestServer { + fn drop(&mut self) { + self.kill_orphaned_processes(); } } + +fn collect_orphaned_processes(socket_path: &str, runtime_dir: &str) -> Vec<(i32, &'static str)> { + let mut matches = Vec::new(); + if let Ok(output) = Command::new("ps").args(["-eo", "pid,args"]).output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let line = line.trim(); + let is_server = line.contains("__serve") && line.contains(socket_path); + let is_runtime_keeper = line.contains("__runtime-keeper") && line.contains(runtime_dir); + if (is_server || is_runtime_keeper) + && let Some(pid_str) = line.split_whitespace().next() + && let Ok(pid) = pid_str.parse::() + { + matches.push(( + pid, + if is_server { + "server" + } else { + "runtime_keeper" + }, + )); + } + } + } + matches +} + +fn terminate_process(pid: i32) { + let _ = Command::new("kill").arg(pid.to_string()).output(); + std::thread::sleep(Duration::from_millis(50)); + if process_exists(pid) { + let _ = Command::new("kill").arg("-9").arg(pid.to_string()).output(); + } +} + +fn process_exists(pid: i32) -> bool { + Command::new("kill") + .args(["-0", &pid.to_string()]) + .status() + .map(|status| status.success()) + .unwrap_or(false) +} + +fn reap_stale_helper_processes() { + if let Ok(output) = Command::new("ps").args(["-eo", "pid,args"]).output() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let Some((pid, socket_path, helper_kind)) = parse_helper_process(line) else { + continue; + }; + let Some(parent) = socket_path.parent() else { + continue; + }; + if parent.exists() { + continue; + } + let _ = Command::new("kill").arg("-9").arg(pid.to_string()).output(); + tracing::debug!( + pid, + helper = helper_kind, + socket = %socket_path.display(), + "killed stale helper process" + ); + } + } +} + +fn parse_helper_process(line: &str) -> Option<(i32, std::path::PathBuf, &'static str)> { + let line = line.trim(); + let is_server = line.contains("__serve"); + let is_runtime_keeper = line.contains("__runtime-keeper"); + if !is_server && !is_runtime_keeper { + return None; + } + + let mut fields = line.split_whitespace(); + let pid = fields.next()?.parse::().ok()?; + let args = fields.collect::>(); + let socket_path = args.windows(2).find_map(|window| { + (window[0] == "--socket").then_some(std::path::PathBuf::from(window[1])) + })?; + Some(( + pid, + socket_path, + if is_server { + "__serve" + } else { + "__runtime-keeper" + }, + )) +} diff --git a/crates/embers-test-support/tests/buffer_runtime.rs b/crates/embers-test-support/tests/buffer_runtime.rs index 6700541..621cc71 100644 --- a/crates/embers-test-support/tests/buffer_runtime.rs +++ b/crates/embers-test-support/tests/buffer_runtime.rs @@ -347,3 +347,145 @@ async fn scrollback_slice_returns_history_while_full_capture_stays_available() { server.shutdown().await.expect("shutdown server"); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn repeated_snapshot_reads_stay_stable_without_new_output() { + let _guard = acquire_test_lock().await.expect("acquire test lock"); + let server = TestServer::start().await.expect("start server"); + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + + let buffer = create_buffer( + &mut connection, + &[ + "/bin/sh", + "-lc", + "i=1; while [ $i -le 8 ]; do printf 'line-%02d\\n' \"$i\"; i=$((i+1)); done", + ], + ) + .await; + + wait_for_capture_contains(&mut connection, buffer.id, "line-08").await; + wait_for_exit(&mut connection, buffer.id).await; + + let first_full = capture_buffer(&mut connection, buffer.id).await; + let second_full = capture_buffer(&mut connection, buffer.id).await; + assert_eq!(first_full.sequence, second_full.sequence); + assert_eq!(first_full.size, second_full.size); + assert_eq!(first_full.lines, second_full.lines); + assert_eq!(first_full.title, second_full.title); + assert_eq!(first_full.cwd, second_full.cwd); + + let first_visible = connection + .capture_visible_buffer(buffer.id) + .await + .expect("first visible capture succeeds"); + let second_visible = connection + .capture_visible_buffer(buffer.id) + .await + .expect("second visible capture succeeds"); + assert_eq!(first_visible.sequence, second_visible.sequence); + assert_eq!(first_visible.size, second_visible.size); + assert_eq!(first_visible.lines, second_visible.lines); + assert_eq!(first_visible.title, second_visible.title); + assert_eq!(first_visible.cwd, second_visible.cwd); + assert_eq!( + first_visible.viewport_top_line, + second_visible.viewport_top_line + ); + assert_eq!(first_visible.total_lines, second_visible.total_lines); + assert_eq!( + first_visible.alternate_screen, + second_visible.alternate_screen + ); + assert_eq!( + first_visible.mouse_reporting, + second_visible.mouse_reporting + ); + assert_eq!( + first_visible.focus_reporting, + second_visible.focus_reporting + ); + assert_eq!( + first_visible.bracketed_paste, + second_visible.bracketed_paste + ); + assert_eq!(first_visible.cursor, second_visible.cursor); + + let first_slice = connection + .capture_scrollback_slice(buffer.id, 2, 3) + .await + .expect("first scrollback slice succeeds"); + let second_slice = connection + .capture_scrollback_slice(buffer.id, 2, 3) + .await + .expect("second scrollback slice succeeds"); + assert_eq!(first_slice.start_line, second_slice.start_line); + assert_eq!(first_slice.total_lines, second_slice.total_lines); + assert_eq!(first_slice.lines, second_slice.lines); + + server.shutdown().await.expect("shutdown server"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn detached_visible_capture_tracks_latest_size_and_output() { + let _guard = acquire_test_lock().await.expect("acquire test lock"); + let server = TestServer::start().await.expect("start server"); + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + + let buffer = create_buffer( + &mut connection, + &[ + "/bin/sh", + "-lc", + "printf '\\033]0;detached-preview\\007ready\\n'; while IFS= read -r line; do printf 'seen:%s\\n' \"$line\"; done", + ], + ) + .await; + assert_eq!(buffer.attachment_node_id, None); + + wait_for_capture_contains(&mut connection, buffer.id, "ready").await; + let initial_visible = connection + .capture_visible_buffer(buffer.id) + .await + .expect("initial visible capture succeeds"); + assert_eq!(initial_visible.size, PtySize::new(80, 24)); + assert_eq!(initial_visible.title.as_deref(), Some("detached-preview")); + assert!(initial_visible.lines.join("\n").contains("ready")); + + resize_buffer(&mut connection, buffer.id, 96, 18).await; + let resized_visible = connection + .capture_visible_buffer(buffer.id) + .await + .expect("resized visible capture succeeds"); + assert_eq!(resized_visible.size, PtySize::new(96, 18)); + assert!(resized_visible.total_lines >= 1); + + send_input(&mut connection, buffer.id, "after-resize\n").await; + wait_for_capture_contains(&mut connection, buffer.id, "seen:after-resize").await; + + let visible = connection + .capture_visible_buffer(buffer.id) + .await + .expect("final visible capture succeeds"); + assert_eq!(visible.size, PtySize::new(96, 18)); + assert_eq!(visible.title.as_deref(), Some("detached-preview")); + assert!(visible.lines.join("\n").contains("seen:after-resize")); + + let captured = capture_buffer(&mut connection, buffer.id).await; + assert_eq!(captured.size, PtySize::new(96, 18)); + assert!(captured.lines.join("\n").contains("ready")); + assert!(captured.lines.join("\n").contains("seen:after-resize")); + + let slice = connection + .capture_scrollback_slice(buffer.id, 0, 4) + .await + .expect("detached scrollback slice succeeds"); + assert!(slice.total_lines >= 2); + assert!(slice.lines.join("\n").contains("ready")); + + server.shutdown().await.expect("shutdown server"); +} diff --git a/crates/embers-test-support/tests/detach_move.rs b/crates/embers-test-support/tests/detach_move.rs index b67fea2..2f6fb96 100644 --- a/crates/embers-test-support/tests/detach_move.rs +++ b/crates/embers-test-support/tests/detach_move.rs @@ -1,5 +1,6 @@ use std::time::{Duration, Instant}; +use crate::support::integration_test_lock; use embers_core::{SplitDirection, new_request_id}; use embers_protocol::{ BufferRecord, BufferRequest, BuffersResponse, ClientMessage, InputRequest, NodeRequest, @@ -140,6 +141,25 @@ async fn send_input( assert!(matches!(response, ServerResponse::Ok(_))); } +async fn resize_buffer( + connection: &mut TestConnection, + buffer_id: embers_core::BufferId, + cols: u16, + rows: u16, +) { + let response = connection + .request(&ClientMessage::Input(InputRequest::Resize { + request_id: new_request_id(), + buffer_id, + cols, + rows, + })) + .await + .expect("resize request succeeds"); + + assert!(matches!(response, ServerResponse::Ok(_))); +} + async fn wait_for_capture_contains( connection: &mut TestConnection, buffer_id: embers_core::BufferId, @@ -161,6 +181,7 @@ async fn wait_for_capture_contains( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn detach_list_capture_and_reattach_buffer_via_socket() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await @@ -234,6 +255,7 @@ async fn detach_list_capture_and_reattach_buffer_via_socket() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn move_request_replaces_target_leaf_without_killing_buffer() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await @@ -301,3 +323,78 @@ async fn move_request_replaces_target_leaf_without_killing_buffer() { server.shutdown().await.expect("shutdown server"); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn detach_and_reattach_preserve_runtime_identity_size_and_capture() { + let _guard = integration_test_lock().lock().await; + let server = TestServer::start().await.expect("start server"); + let mut connection = TestConnection::connect(server.socket_path()) + .await + .expect("connect protocol client"); + + let session = create_session(&mut connection, "main").await; + let session_id = session.snapshot.session.id; + + let primary = create_echo_buffer(&mut connection, "primary").await; + let attached = add_root_tab(&mut connection, session_id, "primary", primary.id).await; + let original_leaf = attached + .session + .focused_leaf_id + .expect("primary tab focuses leaf"); + wait_for_capture_contains(&mut connection, primary.id, "ready").await; + + let before_detach = get_buffer(&mut connection, primary.id).await; + resize_buffer(&mut connection, primary.id, 120, 33).await; + let resized = get_buffer(&mut connection, primary.id).await; + assert_eq!(resized.pty_size, embers_core::PtySize::new(120, 33)); + + let detach = connection + .request(&ClientMessage::Buffer(BufferRequest::Detach { + request_id: new_request_id(), + buffer_id: primary.id, + })) + .await + .expect("detach request succeeds"); + assert!(matches!(detach, ServerResponse::Ok(_))); + + let detached = get_buffer(&mut connection, primary.id).await; + assert_eq!(detached.attachment_node_id, None); + assert_eq!(detached.pid, before_detach.pid); + assert_eq!(detached.pty_size, embers_core::PtySize::new(120, 33)); + assert_eq!(detached.state, before_detach.state); + + send_input(&mut connection, primary.id, "detached-still-live\n").await; + let detached_capture = + wait_for_capture_contains(&mut connection, primary.id, "seen:detached-still-live").await; + assert!(detached_capture.lines.join("\n").contains("ready")); + + let replacement = create_echo_buffer(&mut connection, "replacement").await; + let replacement_snapshot = + add_root_tab(&mut connection, session_id, "replacement", replacement.id).await; + let target_leaf = replacement_snapshot + .session + .focused_leaf_id + .expect("replacement tab focuses target leaf"); + assert_ne!(target_leaf, original_leaf); + + let moved = connection + .request(&ClientMessage::Node(NodeRequest::MoveBufferToNode { + request_id: new_request_id(), + buffer_id: primary.id, + target_leaf_node_id: target_leaf, + })) + .await + .expect("move request succeeds"); + assert!(matches!(moved, ServerResponse::SessionSnapshot(_))); + + let reattached = get_buffer(&mut connection, primary.id).await; + assert_eq!(reattached.attachment_node_id, Some(target_leaf)); + assert_eq!(reattached.state, before_detach.state); + assert_eq!(reattached.pid, before_detach.pid); + assert_eq!(reattached.pty_size, embers_core::PtySize::new(120, 33)); + + send_input(&mut connection, primary.id, "reattached\n").await; + wait_for_capture_contains(&mut connection, primary.id, "seen:reattached").await; + + server.shutdown().await.expect("shutdown server"); +} diff --git a/crates/embers-test-support/tests/floating_windows.rs b/crates/embers-test-support/tests/floating_windows.rs index 00d0863..e7df94c 100644 --- a/crates/embers-test-support/tests/floating_windows.rs +++ b/crates/embers-test-support/tests/floating_windows.rs @@ -1,3 +1,4 @@ +use crate::support::integration_test_lock; use embers_core::{FloatGeometry, new_request_id}; use embers_protocol::{ BufferRecord, BufferRequest, ClientMessage, FloatingRequest, NodeRequest, ServerResponse, @@ -99,6 +100,7 @@ async fn get_buffer( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn create_focus_move_and_close_floating_window_via_socket() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await @@ -203,6 +205,7 @@ async fn create_focus_move_and_close_floating_window_via_socket() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn closing_last_tab_in_floating_tabs_removes_popup() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await diff --git a/crates/embers-test-support/tests/integration.rs b/crates/embers-test-support/tests/integration.rs index c355916..c63ad90 100644 --- a/crates/embers-test-support/tests/integration.rs +++ b/crates/embers-test-support/tests/integration.rs @@ -7,3 +7,4 @@ mod pty_smoke; mod server_harness; mod session_root_tabs; mod split_layout; +mod support; diff --git a/crates/embers-test-support/tests/nested_tabs.rs b/crates/embers-test-support/tests/nested_tabs.rs index 10ceeb2..82c5aa1 100644 --- a/crates/embers-test-support/tests/nested_tabs.rs +++ b/crates/embers-test-support/tests/nested_tabs.rs @@ -1,3 +1,4 @@ +use crate::support::integration_test_lock; use embers_core::{SplitDirection, new_request_id}; use embers_protocol::{ BufferRecord, BufferRequest, ClientMessage, NodeRequest, ServerResponse, SessionRequest, @@ -108,6 +109,7 @@ fn split_record(snapshot: &SessionSnapshot, node_id: embers_core::NodeId) -> Spl #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn nested_tab_mutations_round_trip_through_socket() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await @@ -251,6 +253,7 @@ async fn nested_tab_mutations_round_trip_through_socket() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_tree_returns_nested_tab_structure() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await diff --git a/crates/embers-test-support/tests/protocol_server.rs b/crates/embers-test-support/tests/protocol_server.rs index 2bbf836..913dd86 100644 --- a/crates/embers-test-support/tests/protocol_server.rs +++ b/crates/embers-test-support/tests/protocol_server.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use crate::support::integration_test_lock; use embers_core::{ErrorCode, MuxError, RequestId, SessionId, new_request_id}; use embers_protocol::{ ClientMessage, FrameType, NodeRequest, PingRequest, RawFrame, ServerEnvelope, ServerEvent, @@ -55,6 +56,7 @@ fn encode_frame_bytes(frame: &RawFrame) -> Vec { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn subscriptions_fan_out_to_multiple_clients_with_session_filters() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut actor = TestConnection::connect(server.socket_path()) .await @@ -139,6 +141,7 @@ async fn subscriptions_fan_out_to_multiple_clients_with_session_filters() { #[tokio::test] async fn fragmented_request_frames_round_trip_and_preserve_correlation_id() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut stream = UnixStream::connect(server.socket_path()) .await @@ -177,6 +180,7 @@ async fn fragmented_request_frames_round_trip_and_preserve_correlation_id() { #[tokio::test] async fn malformed_payloads_return_protocol_violation_errors() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut stream = UnixStream::connect(server.socket_path()) .await @@ -208,6 +212,7 @@ async fn malformed_payloads_return_protocol_violation_errors() { #[tokio::test] async fn typed_errors_cover_invalid_ids_and_impossible_mutations() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await @@ -264,6 +269,7 @@ async fn typed_errors_cover_invalid_ids_and_impossible_mutations() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn disconnected_subscribers_are_cleaned_up_without_breaking_remaining_clients() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut actor = TestConnection::connect(server.socket_path()) .await diff --git a/crates/embers-test-support/tests/pty_smoke.rs b/crates/embers-test-support/tests/pty_smoke.rs index 7004a98..b0232bf 100644 --- a/crates/embers-test-support/tests/pty_smoke.rs +++ b/crates/embers-test-support/tests/pty_smoke.rs @@ -1,11 +1,13 @@ use std::time::Duration; +use crate::support::integration_test_lock; use embers_core::PtySize; use embers_test_support::PtyHarness; #[test] #[ignore = "exercises the PTY smoke harness in CI and later end-to-end runs"] fn pty_round_trips_input() { + let _guard = integration_test_lock().blocking_lock(); let mut harness = PtyHarness::spawn( "sh", &["-lc", "read line; printf '%s' \"$line\""], diff --git a/crates/embers-test-support/tests/server_harness.rs b/crates/embers-test-support/tests/server_harness.rs index e12cb79..a3ffa68 100644 --- a/crates/embers-test-support/tests/server_harness.rs +++ b/crates/embers-test-support/tests/server_harness.rs @@ -1,7 +1,9 @@ +use crate::support::integration_test_lock; use embers_test_support::{TestConnection, TestServer}; #[tokio::test] async fn harness_starts_server_and_pings_it() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await diff --git a/crates/embers-test-support/tests/session_root_tabs.rs b/crates/embers-test-support/tests/session_root_tabs.rs index 73b633a..4805889 100644 --- a/crates/embers-test-support/tests/session_root_tabs.rs +++ b/crates/embers-test-support/tests/session_root_tabs.rs @@ -1,3 +1,4 @@ +use crate::support::integration_test_lock; use embers_core::{ErrorCode, new_request_id}; use embers_protocol::{ BufferRecord, BufferRequest, ClientMessage, ServerResponse, SessionRequest, @@ -83,6 +84,7 @@ fn root_tabs(snapshot: &embers_protocol::SessionSnapshot) -> Option #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn create_list_get_and_close_sessions_via_socket() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await @@ -147,6 +149,7 @@ async fn create_list_get_and_close_sessions_via_socket() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn create_select_rename_and_close_root_tabs_via_socket() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await diff --git a/crates/embers-test-support/tests/split_layout.rs b/crates/embers-test-support/tests/split_layout.rs index 8268572..0674966 100644 --- a/crates/embers-test-support/tests/split_layout.rs +++ b/crates/embers-test-support/tests/split_layout.rs @@ -1,3 +1,4 @@ +use crate::support::integration_test_lock; use embers_core::{SplitDirection, new_request_id}; use embers_protocol::{ BufferRecord, BufferRequest, ClientMessage, NodeRequest, ServerResponse, SessionRequest, @@ -107,6 +108,7 @@ fn split_record(snapshot: &SessionSnapshot, node_id: embers_core::NodeId) -> Spl #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn split_and_resize_requests_build_nested_layouts_via_socket() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await @@ -188,6 +190,7 @@ async fn split_and_resize_requests_build_nested_layouts_via_socket() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn focus_and_close_requests_normalize_layout_and_detach_buffers() { + let _guard = integration_test_lock().lock().await; let server = TestServer::start().await.expect("start server"); let mut connection = TestConnection::connect(server.socket_path()) .await diff --git a/crates/embers-test-support/tests/support.rs b/crates/embers-test-support/tests/support.rs new file mode 100644 index 0000000..9f8fa1a --- /dev/null +++ b/crates/embers-test-support/tests/support.rs @@ -0,0 +1,23 @@ +use std::sync::OnceLock; + +use tokio::sync::{Mutex, MutexGuard}; + +pub struct IntegrationTestLock(Mutex<()>); + +impl IntegrationTestLock { + pub async fn lock(&self) -> MutexGuard<'_, ()> { + self.0.lock().await + } + + /// Uses `tokio::sync::Mutex::blocking_lock()` and will panic if called from an async context, + /// including a Tokio runtime thread or an `#[tokio::test]`. Use this only from synchronous + /// tests or non-async threads, and await `lock()` instead inside async tests. + pub fn blocking_lock(&self) -> MutexGuard<'_, ()> { + self.0.blocking_lock() + } +} + +pub fn integration_test_lock() -> &'static IntegrationTestLock { + static LOCK: OnceLock = OnceLock::new(); + LOCK.get_or_init(|| IntegrationTestLock(Mutex::new(()))) +} diff --git a/docs/activity-bell-policy.md b/docs/activity-bell-policy.md new file mode 100644 index 0000000..c0528f8 --- /dev/null +++ b/docs/activity-bell-policy.md @@ -0,0 +1,27 @@ +# Activity and bell policy + +Phase 6 locks down how background terminal updates are reflected in buffer metadata. + +## Buffer states + +- `Idle` - no recent background activity is recorded for the buffer. +- `Activity` - PTY output advanced the buffer without a bell. +- `Bell` - PTY output for the buffer included a bell character. + +## What counts as activity + +Any PTY output that advances a buffer snapshot marks that buffer as `Activity`. This is stored on the durable `Buffer` record, not on the current view, so hidden and detached buffers keep their activity state even while they are off-screen. + +## What counts as a bell + +If the terminal backend observes a bell during an output update, the durable `Buffer` record's stored state is set to `Bell` instead of plain `Activity`. `Bell` wins over ordinary activity for that buffer until the state is cleared by focus or replaced by a later update, which lets client and automation layers distinguish attention-worthy output from normal background chatter. + +## Hidden and detached buffers + +Hidden tabs, inactive panes, and detached buffers continue to ingest PTY output, update captures, and overwrite their stored activity state as new output arrives. Clients that attach to the buffer recover that state from the server snapshot, and automation can observe bell updates from the normal render-invalidated path. + +## Reset on focus + +When a buffer becomes the focused leaf, the server clears its stored status back to `Idle`. This acknowledges the previously hidden background signal without destroying terminal state or capture history. + +Reveal without focus does not clear stored status. That reset only applies to the activity/bell state that had accumulated before focus, so if the focused program produces more output afterward, later runtime updates can mark the buffer active again. diff --git a/docs/config-api-book/404.html b/docs/config-api-book/404.html index b105977..569e4c3 100644 --- a/docs/config-api-book/404.html +++ b/docs/config-api-book/404.html @@ -36,7 +36,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/action.html b/docs/config-api-book/action.html index eeecb4e..929312e 100644 --- a/docs/config-api-book/action.html +++ b/docs/config-api-book/action.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; @@ -178,6 +178,29 @@

Action

Namespace: global

+

fn break_current_node

+ +
fn break_current_node(_: ActionApi, destination: "tab" | "floating") -> Action
+
+
+ +
+ +
+Break the current node into a new tab or floating window. +`destination` accepts `tab` or `floating`. +Example: `action.break_current_node("floating")`. +
+ +
+ +
+ + +
+

fn cancel_search

fn cancel_search(_: ActionApi) -> Action
@@ -344,6 +367,28 @@

fn close_view

+
+ +

fn commit_search

+ +
fn commit_search(_: ActionApi) -> Action
+
+
+ +
+ +
+Finalize the active search, keep the current match and cursor position, and leave search +mode with the committed result in place. +
+ +
+ +
+ +

fn copy_selection

@@ -730,6 +775,29 @@

fn insert_tab_before_current

+
+ +

fn join_buffer_here

+ +
fn join_buffer_here(_: ActionApi, buffer_id: int, placement: "tab-after" | "tab-before" | "left" | "right" | "up" | "down") -> Action
+
+
+ +
+ +
+Attach a buffer at the current node. +`placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. +Example: `action.join_buffer_here(12, "tab-after")`. +
+ +
+ +
+ +

fn kill_buffer

@@ -851,6 +919,94 @@

fn move_buffer_to_node

+
+ +

fn move_current_node_after

+ +
fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move the current node after a sibling. +
+ +
+ +
+ + +
+ +

fn move_current_node_before

+ +
fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move the current node before a sibling. +Use this when the current node is the one being repositioned. +Example: `action.move_current_node_before(42)`. +
+ +
+ +
+ + +
+ +

fn move_node_after

+ +
fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move a node after a sibling. +Use this when you need to move a specific node id instead of the current node. +Example: `action.move_node_after(10, 42)`. +
+ +
+ +
+ + +
+ +

fn move_node_before

+ +
fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move a node before a sibling. +
+ +
+ +
+ +

fn next_current_tabs

@@ -918,7 +1074,7 @@

fn noop

fn notify

-
fn notify(_: ActionApi, level: String, message: String) -> Action
+
fn notify(_: ActionApi, level: "info" | "warn" | "error", message: String) -> Action
+
+ +

fn open_buffer_history

+ +
fn open_buffer_history(_: ActionApi, buffer_id: int, scope: "visible" | "full", placement: "floating" | "tab") -> Action
+
+
+ +
+ +
+Open the history of a buffer in a new view. +`scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. +Example: `action.open_buffer_history(12, "visible", "floating")`. +
+ +
+ +
+ +

fn open_floating

@@ -1466,7 +1645,7 @@

fn send_keys_current

fn split_with

-
fn split_with(_: ActionApi, direction: String, tree: TreeSpec) -> Action
+
fn split_with(_: ActionApi, direction: "h" | "horizontal" | "v" | "vertical", tree: TreeSpec) -> Action
+
+ +

fn swap_current_node

+ +
fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Swap the current node with a sibling. +
+ +
+ +
+ +

fn toggle_mode

@@ -1504,6 +1704,51 @@

fn toggle_mode

+
+ +

fn toggle_zoom_node

+ +
fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action
+
+
+ +
+ +
+Toggle zoom for the specified node id. +There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the +focused node and `toggle_zoom_node` when you already know the target id. +
+ +
+ +
+ + +
+ +

fn unzoom_current_session

+ +
fn unzoom_current_session(_: ActionApi) -> Action
+
+
+ +
+ +
+Clear the current session's active zoom state. +This removes the current session zoom rather than unwinding a stack of prior zooms. +
+ +
+ +
+ +

fn yank_selection

@@ -1525,6 +1770,28 @@

fn yank_selection

+
+ +

fn zoom_current_node

+ +
fn zoom_current_node(_: ActionApi) -> Action
+
+
+ +
+ +
+Zoom the session's currently focused node. +There is intentionally no separate `zoom_node(node_id)` helper in this API surface. +
+ +
+ +
+ + diff --git a/docs/config-api-book/book-a0b12cfe.js b/docs/config-api-book/book-a0b12cfe.js index 62d7c4c..ae89846 100644 --- a/docs/config-api-book/book-a0b12cfe.js +++ b/docs/config-api-book/book-a0b12cfe.js @@ -651,7 +651,8 @@ aria-label="Show hidden lines">'; if (e.altKey || e.ctrlKey || e.metaKey) { return; } - if (window.search && window.search.hasFocus()) { + if (window.__EMBERS_CONFIG_API_SEARCH__ + && window.__EMBERS_CONFIG_API_SEARCH__.hasFocus()) { return; } const html = document.querySelector('html'); diff --git a/docs/config-api-book/buffer-ref.html b/docs/config-api-book/buffer-ref.html index 407ef6c..000c1d9 100644 --- a/docs/config-api-book/buffer-ref.html +++ b/docs/config-api-book/buffer-ref.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/context.html b/docs/config-api-book/context.html index 1ee9a8f..be20280 100644 --- a/docs/config-api-book/context.html +++ b/docs/config-api-book/context.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/defs/registration.rhai b/docs/config-api-book/defs/registration.rhai index 752c140..44b90d2 100644 --- a/docs/config-api-book/defs/registration.rhai +++ b/docs/config-api-book/defs/registration.rhai @@ -9,14 +9,39 @@ fn bar(_: UiApi, left: array, center: array, right: array) -> BarSpec; /// /// `segment(_: UiApi, text: String) -> BarSegment` produces plain text with default /// [`StyleSpec`] values and no click target. +/// +/// See the overloaded `segment(_: UiApi, text: String, options: Map) -> BarSegment` doc for +/// the full `options: Map` styling keys. fn segment(_: UiApi, text: string) -> BarSegment; /// Create a [`BarSegment`] from a [`UiApi`] receiver, text, and an `options: Map`. /// +/// See the main `segment(_: UiApi, text: String) -> BarSegment` doc for the shared behavior. +/// /// `segment(_: UiApi, text: String, options: Map) -> BarSegment` supports `fg`, `bg`, -/// `bold`, `italic`, `underline`, `dim`, and `target` keys to override styling and attach an -/// optional interaction target. `dim` is a boolean that renders the text with reduced -/// intensity for a muted appearance. +/// `bold`, `italic`, `underline`, `dim`, `blink`, and `target` keys to override styling and +/// attach an optional interaction target. +/// +/// `fg` is the foreground color and accepts a standard CSS color name, a hex code such as +/// `#ff0000`, or an RGB/RGBA string such as `rgb(255,0,0)` or `rgba(255,0,0,0.5)`. +/// `bg` is the background color and accepts the same color formats. +/// `bold`, `italic`, `underline`, `dim`, and `blink` are boolean `true`/`false` flags. +/// `dim` renders the text with reduced intensity for a muted appearance. +/// `blink` enables blinking text for that segment, but many modern terminal emulators ignore +/// or disable blink by default, so blinking text may not appear consistently. +/// `target` is an optional interaction target, usually either a string identifier such as +/// `"myTarget"` or a structured object such as `#{ type: "callback", id: "save" }`, +/// depending on the consumer API. +/// +/// Examples: +/// - `#{ fg: "#ff0000", bg: "rgba(0,0,0,0.5)", bold: true }` +/// - `#{ target: "myTarget" }` +/// - `#{ target: #{ type: "callback", id: "save" } }` +/// +/// Accessibility note: blinking text can be distracting and may trigger seizures for some +/// users. Use `blink` sparingly, prefer non-animated emphasis such as `dim` or color/weight +/// changes, follow WCAG guidance to avoid flashing content, and respect reduced-motion +/// preferences before enabling `blink`. fn segment(_: UiApi, text: string, options: map) -> BarSegment; /// Read an environment variable, if it is set. @@ -82,6 +107,11 @@ fn tabs(_: TreeApi, tabs: array) -> TreeSpec; /// Build a tabs container with an explicit active tab. fn tabs_with_active(_: TreeApi, tabs: array, active: int) -> TreeSpec; +/// Break the current node into a new tab or floating window. +/// `destination` accepts `tab` or `floating`. +/// Example: `action.break_current_node("floating")`. +fn break_current_node(_: ActionApi, destination: string) -> Action; + /// Cancel the active search. fn cancel_search(_: ActionApi) -> Action; @@ -106,6 +136,10 @@ fn close_node(_: ActionApi, node_id: int) -> Action; /// Close the currently focused view. fn close_view(_: ActionApi) -> Action; +/// Finalize the active search, keep the current match and cursor position, and leave search +/// mode with the committed result in place. +fn commit_search(_: ActionApi) -> Action; + /// Copy the current selection into the clipboard. fn copy_selection(_: ActionApi) -> Action; @@ -166,6 +200,11 @@ fn insert_tab_before(_: ActionApi, tabs_node_id: int, title: string, tree: TreeS /// Insert a tab before the current tab. fn insert_tab_before_current(_: ActionApi, title: string, tree: TreeSpec) -> Action; +/// Attach a buffer at the current node. +/// `placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. +/// Example: `action.join_buffer_here(12, "tab-after")`. +fn join_buffer_here(_: ActionApi, buffer_id: int, placement: string) -> Action; + /// Kill the currently focused buffer. fn kill_buffer(_: ActionApi) -> Action; @@ -192,6 +231,22 @@ fn move_buffer_to_floating(_: ActionApi, buffer_id: int, options: map) -> Action /// Move a buffer into a specific node. fn move_buffer_to_node(_: ActionApi, buffer_id: int, node_id: int) -> Action; +/// Move the current node after a sibling. +fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action; + +/// Move the current node before a sibling. +/// Use this when the current node is the one being repositioned. +/// Example: `action.move_current_node_before(42)`. +fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action; + +/// Move a node after a sibling. +/// Use this when you need to move a specific node id instead of the current node. +/// Example: `action.move_node_after(10, 42)`. +fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action; + +/// Move a node before a sibling. +fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action; + /// Select the next tab in the currently focused tabs node. fn next_current_tabs(_: ActionApi) -> Action; @@ -204,6 +259,11 @@ fn noop(_: ActionApi) -> Action; /// Emit a client notification. fn notify(_: ActionApi, level: string, message: string) -> Action; +/// Open the history of a buffer in a new view. +/// `scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. +/// Example: `action.open_buffer_history(12, "visible", "floating")`. +fn open_buffer_history(_: ActionApi, buffer_id: int, scope: string, placement: string) -> Action; + /// Open a floating view around the provided tree. fn open_floating(_: ActionApi, tree: TreeSpec, options: map) -> Action; @@ -297,12 +357,28 @@ fn send_keys_current(_: ActionApi, notation: string) -> Action; /// Split the current node and attach the provided tree as the new sibling. fn split_with(_: ActionApi, direction: string, tree: TreeSpec) -> Action; +/// Swap the current node with a sibling. +fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action; + /// Toggle a named input mode. fn toggle_mode(_: ActionApi, mode: string) -> Action; +/// Toggle zoom for the specified node id. +/// There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the +/// focused node and `toggle_zoom_node` when you already know the target id. +fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action; + +/// Clear the current session's active zoom state. +/// This removes the current session zoom rather than unwinding a stack of prior zooms. +fn unzoom_current_session(_: ActionApi) -> Action; + /// Copy the current selection into the clipboard. fn yank_selection(_: ActionApi) -> Action; +/// Zoom the session's currently focused node. +/// There is intentionally no separate `zoom_node(node_id)` helper in this API surface. +fn zoom_current_node(_: ActionApi) -> Action; + /// Toggle focus-on-click behavior. /// /// # rhai-autodocs:index:22 diff --git a/docs/config-api-book/defs/runtime.rhai b/docs/config-api-book/defs/runtime.rhai index ca8da20..902b976 100644 --- a/docs/config-api-book/defs/runtime.rhai +++ b/docs/config-api-book/defs/runtime.rhai @@ -14,14 +14,39 @@ fn bar(_: UiApi, left: array, center: array, right: array) -> BarSpec; /// /// `segment(_: UiApi, text: String) -> BarSegment` produces plain text with default /// [`StyleSpec`] values and no click target. +/// +/// See the overloaded `segment(_: UiApi, text: String, options: Map) -> BarSegment` doc for +/// the full `options: Map` styling keys. fn segment(_: UiApi, text: string) -> BarSegment; /// Create a [`BarSegment`] from a [`UiApi`] receiver, text, and an `options: Map`. /// +/// See the main `segment(_: UiApi, text: String) -> BarSegment` doc for the shared behavior. +/// /// `segment(_: UiApi, text: String, options: Map) -> BarSegment` supports `fg`, `bg`, -/// `bold`, `italic`, `underline`, `dim`, and `target` keys to override styling and attach an -/// optional interaction target. `dim` is a boolean that renders the text with reduced -/// intensity for a muted appearance. +/// `bold`, `italic`, `underline`, `dim`, `blink`, and `target` keys to override styling and +/// attach an optional interaction target. +/// +/// `fg` is the foreground color and accepts a standard CSS color name, a hex code such as +/// `#ff0000`, or an RGB/RGBA string such as `rgb(255,0,0)` or `rgba(255,0,0,0.5)`. +/// `bg` is the background color and accepts the same color formats. +/// `bold`, `italic`, `underline`, `dim`, and `blink` are boolean `true`/`false` flags. +/// `dim` renders the text with reduced intensity for a muted appearance. +/// `blink` enables blinking text for that segment, but many modern terminal emulators ignore +/// or disable blink by default, so blinking text may not appear consistently. +/// `target` is an optional interaction target, usually either a string identifier such as +/// `"myTarget"` or a structured object such as `#{ type: "callback", id: "save" }`, +/// depending on the consumer API. +/// +/// Examples: +/// - `#{ fg: "#ff0000", bg: "rgba(0,0,0,0.5)", bold: true }` +/// - `#{ target: "myTarget" }` +/// - `#{ target: #{ type: "callback", id: "save" } }` +/// +/// Accessibility note: blinking text can be distracting and may trigger seizures for some +/// users. Use `blink` sparingly, prefer non-animated emphasis such as `dim` or color/weight +/// changes, follow WCAG guidance to avoid flashing content, and respect reduced-motion +/// preferences before enabling `blink`. fn segment(_: UiApi, text: string, options: map) -> BarSegment; /// Read an environment variable, if it is set. @@ -131,6 +156,11 @@ fn tabs(_: TreeApi, tabs: array) -> TreeSpec; /// Build a tabs container with an explicit active tab. fn tabs_with_active(_: TreeApi, tabs: array, active: int) -> TreeSpec; +/// Break the current node into a new tab or floating window. +/// `destination` accepts `tab` or `floating`. +/// Example: `action.break_current_node("floating")`. +fn break_current_node(_: ActionApi, destination: string) -> Action; + /// Cancel the active search. fn cancel_search(_: ActionApi) -> Action; @@ -155,6 +185,10 @@ fn close_node(_: ActionApi, node_id: int) -> Action; /// Close the currently focused view. fn close_view(_: ActionApi) -> Action; +/// Finalize the active search, keep the current match and cursor position, and leave search +/// mode with the committed result in place. +fn commit_search(_: ActionApi) -> Action; + /// Copy the current selection into the clipboard. fn copy_selection(_: ActionApi) -> Action; @@ -215,6 +249,11 @@ fn insert_tab_before(_: ActionApi, tabs_node_id: int, title: string, tree: TreeS /// Insert a tab before the current tab. fn insert_tab_before_current(_: ActionApi, title: string, tree: TreeSpec) -> Action; +/// Attach a buffer at the current node. +/// `placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. +/// Example: `action.join_buffer_here(12, "tab-after")`. +fn join_buffer_here(_: ActionApi, buffer_id: int, placement: string) -> Action; + /// Kill the currently focused buffer. fn kill_buffer(_: ActionApi) -> Action; @@ -241,6 +280,22 @@ fn move_buffer_to_floating(_: ActionApi, buffer_id: int, options: map) -> Action /// Move a buffer into a specific node. fn move_buffer_to_node(_: ActionApi, buffer_id: int, node_id: int) -> Action; +/// Move the current node after a sibling. +fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action; + +/// Move the current node before a sibling. +/// Use this when the current node is the one being repositioned. +/// Example: `action.move_current_node_before(42)`. +fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action; + +/// Move a node after a sibling. +/// Use this when you need to move a specific node id instead of the current node. +/// Example: `action.move_node_after(10, 42)`. +fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action; + +/// Move a node before a sibling. +fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action; + /// Select the next tab in the currently focused tabs node. fn next_current_tabs(_: ActionApi) -> Action; @@ -253,6 +308,11 @@ fn noop(_: ActionApi) -> Action; /// Emit a client notification. fn notify(_: ActionApi, level: string, message: string) -> Action; +/// Open the history of a buffer in a new view. +/// `scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. +/// Example: `action.open_buffer_history(12, "visible", "floating")`. +fn open_buffer_history(_: ActionApi, buffer_id: int, scope: string, placement: string) -> Action; + /// Open a floating view around the provided tree. fn open_floating(_: ActionApi, tree: TreeSpec, options: map) -> Action; @@ -346,12 +406,28 @@ fn send_keys_current(_: ActionApi, notation: string) -> Action; /// Split the current node and attach the provided tree as the new sibling. fn split_with(_: ActionApi, direction: string, tree: TreeSpec) -> Action; +/// Swap the current node with a sibling. +fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action; + /// Toggle a named input mode. fn toggle_mode(_: ActionApi, mode: string) -> Action; +/// Toggle zoom for the specified node id. +/// There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the +/// focused node and `toggle_zoom_node` when you already know the target id. +fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action; + +/// Clear the current session's active zoom state. +/// This removes the current session zoom rather than unwinding a stack of prior zooms. +fn unzoom_current_session(_: ActionApi) -> Action; + /// Copy the current selection into the clipboard. fn yank_selection(_: ActionApi) -> Action; +/// Zoom the session's currently focused node. +/// There is intentionally no separate `zoom_node(node_id)` helper in this API surface. +fn zoom_current_node(_: ActionApi) -> Action; + /// Return the active tab index. fn active_index(bar: TabBarContext) -> int; diff --git a/docs/config-api-book/event-info.html b/docs/config-api-book/event-info.html index 0f3cd3d..917344b 100644 --- a/docs/config-api-book/event-info.html +++ b/docs/config-api-book/event-info.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/example.html b/docs/config-api-book/example.html index 36c442a..9867a07 100644 --- a/docs/config-api-book/example.html +++ b/docs/config-api-book/example.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/floating-ref.html b/docs/config-api-book/floating-ref.html index 5fb8fd0..297414a 100644 --- a/docs/config-api-book/floating-ref.html +++ b/docs/config-api-book/floating-ref.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/index.html b/docs/config-api-book/index.html index 241c2b3..f7abc39 100644 --- a/docs/config-api-book/index.html +++ b/docs/config-api-book/index.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/mouse.html b/docs/config-api-book/mouse.html index 22fc3e5..8c6b816 100644 --- a/docs/config-api-book/mouse.html +++ b/docs/config-api-book/mouse.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/mux.html b/docs/config-api-book/mux.html index 3cb17e0..02a574b 100644 --- a/docs/config-api-book/mux.html +++ b/docs/config-api-book/mux.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/node-ref.html b/docs/config-api-book/node-ref.html index f9c5ef0..57fd86e 100644 --- a/docs/config-api-book/node-ref.html +++ b/docs/config-api-book/node-ref.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/print.html b/docs/config-api-book/print.html index a445bd0..8dd2a5c 100644 --- a/docs/config-api-book/print.html +++ b/docs/config-api-book/print.html @@ -36,7 +36,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; @@ -412,6 +412,29 @@

Actio

Namespace: global

+

fn break_current_node

+ +
fn break_current_node(_: ActionApi, destination: "tab" | "floating") -> Action
+
+
+ +
+ +
+Break the current node into a new tab or floating window. +`destination` accepts `tab` or `floating`. +Example: `action.break_current_node("floating")`. +
+ +
+ +
+ + +
+

fn cancel_search

fn cancel_search(_: ActionApi) -> Action
@@ -578,6 +601,28 @@

fn close_view

+
+ +

fn commit_search

+ +
fn commit_search(_: ActionApi) -> Action
+
+
+ +
+ +
+Finalize the active search, keep the current match and cursor position, and leave search +mode with the committed result in place. +
+ +
+ +
+ +

fn copy_selection

@@ -964,6 +1009,29 @@

fn insert_tab_before_current

+
+ +

fn join_buffer_here

+ +
fn join_buffer_here(_: ActionApi, buffer_id: int, placement: "tab-after" | "tab-before" | "left" | "right" | "up" | "down") -> Action
+
+
+ +
+ +
+Attach a buffer at the current node. +`placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. +Example: `action.join_buffer_here(12, "tab-after")`. +
+ +
+ +
+ +

fn kill_buffer

@@ -1085,6 +1153,94 @@

fn move_buffer_to_node

+
+ +

fn move_current_node_after

+ +
fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move the current node after a sibling. +
+ +
+ +
+ + +
+ +

fn move_current_node_before

+ +
fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move the current node before a sibling. +Use this when the current node is the one being repositioned. +Example: `action.move_current_node_before(42)`. +
+ +
+ +
+ + +
+ +

fn move_node_after

+ +
fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move a node after a sibling. +Use this when you need to move a specific node id instead of the current node. +Example: `action.move_node_after(10, 42)`. +
+ +
+ +
+ + +
+ +

fn move_node_before

+ +
fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move a node before a sibling. +
+ +
+ +
+ +

fn next_current_tabs

@@ -1152,7 +1308,7 @@

fn noop

fn notify

-
fn notify(_: ActionApi, level: String, message: String) -> Action
+
fn notify(_: ActionApi, level: "info" | "warn" | "error", message: String) -> Action
+
+ +

fn open_buffer_history

+ +
fn open_buffer_history(_: ActionApi, buffer_id: int, scope: "visible" | "full", placement: "floating" | "tab") -> Action
+
+
+ +
+ +
+Open the history of a buffer in a new view. +`scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. +Example: `action.open_buffer_history(12, "visible", "floating")`. +
+ +
+ +
+ +

fn open_floating

@@ -1700,7 +1879,7 @@

fn send_keys_current

fn split_with

-
fn split_with(_: ActionApi, direction: String, tree: TreeSpec) -> Action
+
fn split_with(_: ActionApi, direction: "h" | "horizontal" | "v" | "vertical", tree: TreeSpec) -> Action
+
+ +

fn swap_current_node

+ +
fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Swap the current node with a sibling. +
+ +
+ +
+ +

fn toggle_mode

@@ -1738,6 +1938,51 @@

fn toggle_mode

+
+ +

fn toggle_zoom_node

+ +
fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action
+
+
+ +
+ +
+Toggle zoom for the specified node id. +There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the +focused node and `toggle_zoom_node` when you already know the target id. +
+ +
+ +
+ + +
+ +

fn unzoom_current_session

+ +
fn unzoom_current_session(_: ActionApi) -> Action
+
+
+ +
+ +
+Clear the current session's active zoom state. +This removes the current session zoom rather than unwinding a stack of prior zooms. +
+ +
+ +
+ +

fn yank_selection

@@ -1759,6 +2004,28 @@

fn yank_selection

+
+ +

fn zoom_current_node

+ +
fn zoom_current_node(_: ActionApi) -> Action
+
+
+ +
+ +
+Zoom the session's currently focused node. +There is intentionally no separate `zoom_node(node_id)` helper in this API surface. +
+ +
+ +
+ +

Tree (Registration)

Namespace: global

@@ -2135,6 +2402,8 @@

fn segment

Create a [`BarSegment`] from a [`UiApi`] receiver and text using default styling.

segment(_: UiApi, text: String) -> BarSegment produces plain text with default [StyleSpec] values and no click target.

+

See the overloaded segment(_: UiApi, text: String, options: Map) -> BarSegment doc for +the full options: Map styling keys.

@@ -2282,6 +2551,29 @@

Action

Namespace: global

+

fn break_current_node

+ +
fn break_current_node(_: ActionApi, destination: "tab" | "floating") -> Action
+
+
+ +
+ +
+Break the current node into a new tab or floating window. +`destination` accepts `tab` or `floating`. +Example: `action.break_current_node("floating")`. +
+ +
+ +
+ + +
+

fn cancel_search

fn cancel_search(_: ActionApi) -> Action
@@ -2448,6 +2740,28 @@

fn close_view

+
+ +

fn commit_search

+ +
fn commit_search(_: ActionApi) -> Action
+
+
+ +
+ +
+Finalize the active search, keep the current match and cursor position, and leave search +mode with the committed result in place. +
+ +
+ +
+ +

fn copy_selection

@@ -2834,6 +3148,29 @@

fn insert_tab_before_current

+
+ +

fn join_buffer_here

+ +
fn join_buffer_here(_: ActionApi, buffer_id: int, placement: "tab-after" | "tab-before" | "left" | "right" | "up" | "down") -> Action
+
+
+ +
+ +
+Attach a buffer at the current node. +`placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. +Example: `action.join_buffer_here(12, "tab-after")`. +
+ +
+ +
+ +

fn kill_buffer

@@ -2955,6 +3292,94 @@

fn move_buffer_to_node

+
+ +

fn move_current_node_after

+ +
fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move the current node after a sibling. +
+ +
+ +
+ + +
+ +

fn move_current_node_before

+ +
fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move the current node before a sibling. +Use this when the current node is the one being repositioned. +Example: `action.move_current_node_before(42)`. +
+ +
+ +
+ + +
+ +

fn move_node_after

+ +
fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move a node after a sibling. +Use this when you need to move a specific node id instead of the current node. +Example: `action.move_node_after(10, 42)`. +
+ +
+ +
+ + +
+ +

fn move_node_before

+ +
fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move a node before a sibling. +
+ +
+ +
+ +

fn next_current_tabs

@@ -3022,7 +3447,7 @@

fn noop

fn notify

-
fn notify(_: ActionApi, level: String, message: String) -> Action
+
fn notify(_: ActionApi, level: "info" | "warn" | "error", message: String) -> Action
+
+ +

fn open_buffer_history

+ +
fn open_buffer_history(_: ActionApi, buffer_id: int, scope: "visible" | "full", placement: "floating" | "tab") -> Action
+
+
+ +
+ +
+Open the history of a buffer in a new view. +`scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. +Example: `action.open_buffer_history(12, "visible", "floating")`. +
+ +
+ +
+ +

fn open_floating

@@ -3570,7 +4018,7 @@

fn send_keys_current

fn split_with

-
fn split_with(_: ActionApi, direction: String, tree: TreeSpec) -> Action
+
fn split_with(_: ActionApi, direction: "h" | "horizontal" | "v" | "vertical", tree: TreeSpec) -> Action
+
+ +

fn swap_current_node

+ +
fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Swap the current node with a sibling. +
+ +
+ +
+ +

fn toggle_mode

@@ -3608,6 +4077,51 @@

fn toggle_mode

+
+ +

fn toggle_zoom_node

+ +
fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action
+
+
+ +
+ +
+Toggle zoom for the specified node id. +There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the +focused node and `toggle_zoom_node` when you already know the target id. +
+ +
+ +
+ + +
+ +

fn unzoom_current_session

+ +
fn unzoom_current_session(_: ActionApi) -> Action
+
+
+ +
+ +
+Clear the current session's active zoom state. +This removes the current session zoom rather than unwinding a stack of prior zooms. +
+ +
+ +
+ +

fn yank_selection

@@ -3629,6 +4143,28 @@

fn yank_selection

+
+ +

fn zoom_current_node

+ +
fn zoom_current_node(_: ActionApi) -> Action
+
+
+ +
+ +
+Zoom the session's currently focused node. +There is intentionally no separate `zoom_node(node_id)` helper in this API surface. +
+ +
+ +
+ +

Tree

Namespace: global

@@ -5875,6 +6411,8 @@

fn segment

Create a [`BarSegment`] from a [`UiApi`] receiver and text using default styling.

segment(_: UiApi, text: String) -> BarSegment produces plain text with default [StyleSpec] values and no click target.

+

See the overloaded segment(_: UiApi, text: String, options: Map) -> BarSegment doc for +the full options: Map styling keys.

diff --git a/docs/config-api-book/registration-action.html b/docs/config-api-book/registration-action.html index 9964117..a63ecb6 100644 --- a/docs/config-api-book/registration-action.html +++ b/docs/config-api-book/registration-action.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; @@ -178,6 +178,29 @@

Actio

Namespace: global

+

fn break_current_node

+ +
fn break_current_node(_: ActionApi, destination: "tab" | "floating") -> Action
+
+
+ +
+ +
+Break the current node into a new tab or floating window. +`destination` accepts `tab` or `floating`. +Example: `action.break_current_node("floating")`. +
+ +
+ +
+ + +
+

fn cancel_search

fn cancel_search(_: ActionApi) -> Action
@@ -344,6 +367,28 @@

fn close_view

+
+ +

fn commit_search

+ +
fn commit_search(_: ActionApi) -> Action
+
+
+ +
+ +
+Finalize the active search, keep the current match and cursor position, and leave search +mode with the committed result in place. +
+ +
+ +
+ +

fn copy_selection

@@ -730,6 +775,29 @@

fn insert_tab_before_current

+
+ +

fn join_buffer_here

+ +
fn join_buffer_here(_: ActionApi, buffer_id: int, placement: "tab-after" | "tab-before" | "left" | "right" | "up" | "down") -> Action
+
+
+ +
+ +
+Attach a buffer at the current node. +`placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. +Example: `action.join_buffer_here(12, "tab-after")`. +
+ +
+ +
+ +

fn kill_buffer

@@ -851,6 +919,94 @@

fn move_buffer_to_node

+
+ +

fn move_current_node_after

+ +
fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move the current node after a sibling. +
+ +
+ +
+ + +
+ +

fn move_current_node_before

+ +
fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move the current node before a sibling. +Use this when the current node is the one being repositioned. +Example: `action.move_current_node_before(42)`. +
+ +
+ +
+ + +
+ +

fn move_node_after

+ +
fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move a node after a sibling. +Use this when you need to move a specific node id instead of the current node. +Example: `action.move_node_after(10, 42)`. +
+ +
+ +
+ + +
+ +

fn move_node_before

+ +
fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Move a node before a sibling. +
+ +
+ +
+ +

fn next_current_tabs

@@ -918,7 +1074,7 @@

fn noop

fn notify

-
fn notify(_: ActionApi, level: String, message: String) -> Action
+
fn notify(_: ActionApi, level: "info" | "warn" | "error", message: String) -> Action
+
+ +

fn open_buffer_history

+ +
fn open_buffer_history(_: ActionApi, buffer_id: int, scope: "visible" | "full", placement: "floating" | "tab") -> Action
+
+
+ +
+ +
+Open the history of a buffer in a new view. +`scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. +Example: `action.open_buffer_history(12, "visible", "floating")`. +
+ +
+ +
+ +

fn open_floating

@@ -1466,7 +1645,7 @@

fn send_keys_current

fn split_with

-
fn split_with(_: ActionApi, direction: String, tree: TreeSpec) -> Action
+
fn split_with(_: ActionApi, direction: "h" | "horizontal" | "v" | "vertical", tree: TreeSpec) -> Action
+
+ +

fn swap_current_node

+ +
fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action
+
+
+ +
+ +
+Swap the current node with a sibling. +
+ +
+ +
+ +

fn toggle_mode

@@ -1504,6 +1704,51 @@

fn toggle_mode

+
+ +

fn toggle_zoom_node

+ +
fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action
+
+
+ +
+ +
+Toggle zoom for the specified node id. +There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the +focused node and `toggle_zoom_node` when you already know the target id. +
+ +
+ +
+ + +
+ +

fn unzoom_current_session

+ +
fn unzoom_current_session(_: ActionApi) -> Action
+
+
+ +
+ +
+Clear the current session's active zoom state. +This removes the current session zoom rather than unwinding a stack of prior zooms. +
+ +
+ +
+ +

fn yank_selection

@@ -1525,6 +1770,28 @@

fn yank_selection

+
+ +

fn zoom_current_node

+ +
fn zoom_current_node(_: ActionApi) -> Action
+
+
+ +
+ +
+Zoom the session's currently focused node. +There is intentionally no separate `zoom_node(node_id)` helper in this API surface. +
+ +
+ +
+ + diff --git a/docs/config-api-book/registration-globals.html b/docs/config-api-book/registration-globals.html index 04c2080..15d1f7c 100644 --- a/docs/config-api-book/registration-globals.html +++ b/docs/config-api-book/registration-globals.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/registration-system.html b/docs/config-api-book/registration-system.html index c439a4c..eb8256f 100644 --- a/docs/config-api-book/registration-system.html +++ b/docs/config-api-book/registration-system.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/registration-tree.html b/docs/config-api-book/registration-tree.html index b7d4726..2002096 100644 --- a/docs/config-api-book/registration-tree.html +++ b/docs/config-api-book/registration-tree.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/registration-ui.html b/docs/config-api-book/registration-ui.html index d2e0b04..ac24c3f 100644 --- a/docs/config-api-book/registration-ui.html +++ b/docs/config-api-book/registration-ui.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; @@ -214,6 +214,8 @@

fn segment

Create a [`BarSegment`] from a [`UiApi`] receiver and text using default styling.

segment(_: UiApi, text: String) -> BarSegment produces plain text with default [StyleSpec] values and no click target.

+

See the overloaded segment(_: UiApi, text: String, options: Map) -> BarSegment doc for +the full options: Map styling keys.

diff --git a/docs/config-api-book/runtime-theme.html b/docs/config-api-book/runtime-theme.html index dc17ddc..c09c6f8 100644 --- a/docs/config-api-book/runtime-theme.html +++ b/docs/config-api-book/runtime-theme.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/searcher-c2a407aa.js b/docs/config-api-book/searcher-c2a407aa.js index db84c4f..7d94be6 100644 --- a/docs/config-api-book/searcher-c2a407aa.js +++ b/docs/config-api-book/searcher-c2a407aa.js @@ -2,7 +2,8 @@ /* global Mark, elasticlunr, path_to_root */ -window.search = window.search || {}; +const EMBERS_DOC_SEARCH = + window.__EMBERS_CONFIG_API_SEARCH__ || (window.__EMBERS_CONFIG_API_SEARCH__ = {}); (function search() { // Search functionality // @@ -302,7 +303,7 @@ window.search = window.search || {}; config.hasFocus = hasFocus; } - initSearchInteractions(window.search); + initSearchInteractions(EMBERS_DOC_SEARCH); function unfocusSearchbar() { // hacky, but just focusing a div only works once @@ -426,7 +427,7 @@ window.search = window.search || {}; const script = document.createElement('script'); script.src = url; script.id = id; - script.onload = () => init(window.search); + script.onload = () => init(EMBERS_DOC_SEARCH); script.onerror = error => { console.error(`Failed to load \`${url}\`: ${error}`); }; @@ -437,7 +438,7 @@ window.search = window.search || {}; if (yes) { loadSearchScript( window.path_to_searchindex_js || - path_to_root + 'searchindex-256e957a.js', + path_to_root + 'searchindex-60b5c002.js', 'mdbook-search-index'); search_wrap.classList.remove('hidden'); searchicon.setAttribute('aria-expanded', 'true'); @@ -552,4 +553,4 @@ window.search = window.search || {}; // Exported functions search.hasFocus = hasFocus; -})(window.search); +})(EMBERS_DOC_SEARCH); diff --git a/docs/config-api-book/searchindex-256e957a.js b/docs/config-api-book/searchindex-256e957a.js deleted file mode 100644 index 70994c4..0000000 --- a/docs/config-api-book/searchindex-256e957a.js +++ /dev/null @@ -1 +0,0 @@ -window.search = Object.assign(window.search, JSON.parse('{"doc_urls":["index.html#embers-config-api","index.html#pages","index.html#definitions","index.html#example","example.html#example","registration-globals.html#registration-globals","registration-action.html#action-registration","registration-tree.html#tree-registration","registration-system.html#system-registration","registration-ui.html#ui-registration","mouse.html#mouse","theme.html#theme","tabbar.html#tabbar","action.html#action","tree.html#tree","context.html#context","mux.html#mux","event-info.html#eventinfo","session-ref.html#sessionref","buffer-ref.html#bufferref","node-ref.html#noderef","floating-ref.html#floatingref","tab-bar-context.html#tabbarcontext","tab-info.html#tabinfo","system-runtime.html#system","ui.html#ui","runtime-theme.html#runtime-theme"],"index":{"documentStore":{"docInfo":{"0":{"body":41,"breadcrumbs":4,"title":3},"1":{"body":37,"breadcrumbs":2,"title":1},"10":{"body":55,"breadcrumbs":6,"title":1},"11":{"body":15,"breadcrumbs":3,"title":1},"12":{"body":16,"breadcrumbs":3,"title":1},"13":{"body":918,"breadcrumbs":65,"title":1},"14":{"body":202,"breadcrumbs":14,"title":1},"15":{"body":165,"breadcrumbs":14,"title":1},"16":{"body":136,"breadcrumbs":12,"title":1},"17":{"body":91,"breadcrumbs":9,"title":1},"18":{"body":48,"breadcrumbs":6,"title":1},"19":{"body":230,"breadcrumbs":20,"title":1},"2":{"body":2,"breadcrumbs":2,"title":1},"20":{"body":178,"breadcrumbs":17,"title":1},"21":{"body":78,"breadcrumbs":9,"title":1},"22":{"body":75,"breadcrumbs":8,"title":1},"23":{"body":70,"breadcrumbs":8,"title":1},"24":{"body":41,"breadcrumbs":5,"title":1},"25":{"body":61,"breadcrumbs":4,"title":1},"26":{"body":19,"breadcrumbs":6,"title":2},"3":{"body":1,"breadcrumbs":2,"title":1},"4":{"body":50,"breadcrumbs":2,"title":1},"5":{"body":136,"breadcrumbs":16,"title":2},"6":{"body":918,"breadcrumbs":130,"title":2},"7":{"body":202,"breadcrumbs":28,"title":2},"8":{"body":41,"breadcrumbs":10,"title":2},"9":{"body":61,"breadcrumbs":8,"title":2}},"docs":{"0":{"body":"This reference is generated from the Rust-backed Rhai exports used by Embers. There are two execution phases: registration time: the top-level config file where you declare modes, bindings, named actions, and visual settings runtime: named actions, event handlers, and tab bar formatters that run against live client state Definition files live in defs/.","breadcrumbs":"Overview » Embers Config API","id":"0","title":"Embers Config API"},"1":{"body":"action buffer-ref context event-info floating-ref mouse mux node-ref registration-action registration-globals registration-system registration-tree registration-ui runtime-theme session-ref system-runtime tab-bar-context tab-info tabbar theme tree ui","breadcrumbs":"Overview » Pages","id":"1","title":"Pages"},"10":{"body":"Namespace: global fn set_click_focus fn set_click_focus(mouse: MouseApi, value: bool) Description Toggle focus-on-click behavior. fn set_click_forward fn set_click_forward(mouse: MouseApi, value: bool) Description Toggle forwarding mouse clicks into the focused buffer. fn set_wheel_forward fn set_wheel_forward(mouse: MouseApi, value: bool) Description Toggle wheel event forwarding into the focused buffer. fn set_wheel_scroll fn set_wheel_scroll(mouse: MouseApi, value: bool) Description Toggle client-side wheel scrolling.","breadcrumbs":"Mouse » Mouse » Mouse » Mouse » Mouse » Mouse","id":"10","title":"Mouse"},"11":{"body":"Namespace: global fn set_palette fn set_palette(theme: ThemeApi, palette: Map) Description Add named colors to the theme palette.","breadcrumbs":"Theme » Theme » Theme","id":"11","title":"Theme"},"12":{"body":"Namespace: global fn set_formatter fn set_formatter(tabbar: TabbarApi, callback: FnPtr) Description Register the function used to format the tab bar.","breadcrumbs":"Tabbar » Tabbar » Tabbar","id":"12","title":"Tabbar"},"13":{"body":"Namespace: global fn cancel_search fn cancel_search(_: ActionApi) -> Action Description Cancel the active search. fn cancel_selection fn cancel_selection(_: ActionApi) -> Action Description Cancel the current selection. fn chain fn chain(_: ActionApi, actions: Array) -> Action Description Chain multiple actions into one composite action. fn clear_pending_keys fn clear_pending_keys(_: ActionApi) -> Action Description Clear any partially-entered key sequence. fn close_floating fn close_floating(_: ActionApi) -> Action Description Close the currently focused floating window. fn close_floating_id fn close_floating_id(_: ActionApi, floating_id: int) -> Action Description Close a floating window by id. fn close_node fn close_node(_: ActionApi, node_id: int) -> Action Description Close a view by node id. fn close_view fn close_view(_: ActionApi) -> Action Description Close the currently focused view. fn copy_selection fn copy_selection(_: ActionApi) -> Action Description Copy the current selection into the clipboard. fn detach_buffer fn detach_buffer(_: ActionApi) -> Action Description Detach the currently focused buffer. fn detach_buffer_id fn detach_buffer_id(_: ActionApi, buffer_id: int) -> Action Description Detach a buffer by id. fn enter_mode fn enter_mode(_: ActionApi, mode: String) -> Action Description Enter a specific input mode by name. fn enter_search_mode fn enter_search_mode(_: ActionApi) -> Action Description Enter incremental search mode. fn enter_select_block fn enter_select_block(_: ActionApi) -> Action Description Enter block selection mode. fn enter_select_char fn enter_select_char(_: ActionApi) -> Action Description Enter character selection mode. fn enter_select_line fn enter_select_line(_: ActionApi) -> Action Description Enter line selection mode. fn focus_buffer fn focus_buffer(_: ActionApi, buffer_id: int) -> Action Description Focus a specific buffer by id. fn focus_down fn focus_down(_: ActionApi) -> Action Description Focus the view below the current node. fn focus_left fn focus_left(_: ActionApi) -> Action Description Example Focus the view to the left of the current node. action.focus_left() fn focus_right fn focus_right(_: ActionApi) -> Action Description Focus the view to the right of the current node. fn focus_up fn focus_up(_: ActionApi) -> Action Description Focus the view above the current node. fn follow_output fn follow_output(_: ActionApi) -> Action Description Re-enable following live output. fn insert_tab_after fn insert_tab_after(_: ActionApi, tabs_node_id: int, title: String, tree: TreeSpec) -> Action Description Insert a tab after a specific tabs node. fn insert_tab_after_current fn insert_tab_after_current(_: ActionApi, title: String, tree: TreeSpec) -> Action Description Insert a tab after the current tab in the focused tabs node. fn insert_tab_before fn insert_tab_before(_: ActionApi, tabs_node_id: int, title: String, tree: TreeSpec) -> Action Description Insert a tab before a specific tabs node. fn insert_tab_before_current fn insert_tab_before_current(_: ActionApi, title: String, tree: TreeSpec) -> Action Description Insert a tab before the current tab. fn kill_buffer fn kill_buffer(_: ActionApi) -> Action Description Kill the currently focused buffer. fn kill_buffer_id fn kill_buffer_id(_: ActionApi, buffer_id: int) -> Action Description Kill a buffer by id. fn leave_mode fn leave_mode(_: ActionApi) -> Action Description Leave the active input mode. fn move_buffer_to_floating fn move_buffer_to_floating(_: ActionApi, buffer_id: int, options: Map) -> Action Description Options Move a buffer into a new floating window. x (i16): horizontal offset from the anchor (default: 0) y (i16): vertical offset from the anchor (default: 0) width (FloatingSize): window width, as a percentage (e.g., 50%) or pixel value (default: 50%) height (FloatingSize): window height, as a percentage or pixel value (default: 50%) anchor (FloatingAnchor): anchor point for positioning, e.g., “top_left”, “center” (default: center) title (Option): window title (default: none) focus (bool): whether to focus the window after creation (default: true) close_on_empty (bool): whether to close the window when its buffer empties (default: true) fn move_buffer_to_node fn move_buffer_to_node(_: ActionApi, buffer_id: int, node_id: int) -> Action Description Move a buffer into a specific node. fn next_current_tabs fn next_current_tabs(_: ActionApi) -> Action Description Select the next tab in the currently focused tabs node. fn next_tab fn next_tab(_: ActionApi, tabs_node_id: int) -> Action Description Select the next tab in a specific tabs node. fn noop fn noop(_: ActionApi) -> Action Description Build a no-op action. fn notify fn notify(_: ActionApi, level: String, message: String) -> Action Description Emit a client notification. fn open_floating fn open_floating(_: ActionApi, tree: TreeSpec, options: Map) -> Action Description Open a floating view around the provided tree. fn prev_current_tabs fn prev_current_tabs(_: ActionApi) -> Action Description Select the previous tab in the currently focused tabs node. fn prev_tab fn prev_tab(_: ActionApi, tabs_node_id: int) -> Action Description Select the previous tab in a specific tabs node. fn replace_current_with fn replace_current_with(_: ActionApi, tree: TreeSpec) -> Action Description Replace the focused node with a new tree. fn replace_node fn replace_node(_: ActionApi, node_id: int, tree: TreeSpec) -> Action Description Replace a specific node by id with a new tree. fn reveal_buffer fn reveal_buffer(_: ActionApi, buffer_id: int) -> Action Description Reveal a specific buffer by id. fn run_named_action fn run_named_action(_: ActionApi, name: String) -> Action Description Run another named action by name. fn scroll_line_down fn scroll_line_down(_: ActionApi) -> Action Description Scroll one line downward in local scrollback. fn scroll_line_up fn scroll_line_up(_: ActionApi) -> Action Description Scroll one line upward in local scrollback. fn scroll_page_down fn scroll_page_down(_: ActionApi) -> Action Description Scroll one page downward in local scrollback. fn scroll_page_up fn scroll_page_up(_: ActionApi) -> Action Description Scroll one page upward in local scrollback. fn scroll_to_bottom fn scroll_to_bottom(_: ActionApi) -> Action Description Scroll to the bottom of local scrollback. fn scroll_to_top fn scroll_to_top(_: ActionApi) -> Action Description Scroll to the top of local scrollback. fn search_next fn search_next(_: ActionApi) -> Action Description Jump to the next search match. fn search_prev fn search_prev(_: ActionApi) -> Action Description Jump to the previous search match. fn select_current_tabs fn select_current_tabs(_: ActionApi, index: int) -> Action Description Select a tab by index in the currently focused tabs node. fn select_move_down fn select_move_down(_: ActionApi) -> Action Description Move the active selection down. fn select_move_left fn select_move_left(_: ActionApi) -> Action Description Move the active selection left. fn select_move_right fn select_move_right(_: ActionApi) -> Action Description Move the active selection right. fn select_move_up fn select_move_up(_: ActionApi) -> Action Description Move the active selection up. fn select_tab fn select_tab(_: ActionApi, tabs_node_id: int, index: int) -> Action Description Select a tab by index in a specific tabs node. fn send_bytes fn send_bytes(_: ActionApi, buffer_id: int, bytes: String) -> Action\\nfn send_bytes(_: ActionApi, buffer_id: int, bytes: Array) -> Action Description Send a string of bytes to a specific buffer. fn send_bytes_current fn send_bytes_current(_: ActionApi, bytes: String) -> Action\\nfn send_bytes_current(_: ActionApi, bytes: Array) -> Action Description Send a string of bytes to the focused buffer. fn send_keys fn send_keys(_: ActionApi, buffer_id: int, notation: String) -> Action Description Send a key notation sequence to a specific buffer. fn send_keys_current fn send_keys_current(_: ActionApi, notation: String) -> Action Description Send a key notation sequence to the focused buffer. fn split_with fn split_with(_: ActionApi, direction: String, tree: TreeSpec) -> Action Description Split the current node and attach the provided tree as the new sibling. fn toggle_mode fn toggle_mode(_: ActionApi, mode: String) -> Action Description Toggle a named input mode. fn yank_selection fn yank_selection(_: ActionApi) -> Action Description Copy the current selection into the clipboard.","breadcrumbs":"Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action","id":"13","title":"Action"},"14":{"body":"Namespace: global fn buffer_attach fn buffer_attach(_: TreeApi, buffer_id: int) -> TreeSpec Description Attach an existing buffer by id. fn buffer_current fn buffer_current(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused buffer. fn buffer_empty fn buffer_empty(_: TreeApi) -> TreeSpec Description Build an empty buffer tree node. fn buffer_spawn fn buffer_spawn(_: TreeApi, command: Array) -> TreeSpec\\nfn buffer_spawn(_: TreeApi, command: Array, options: Map) -> TreeSpec Description Example Spawn a new buffer from a command array. Supported options keys are title ( string), cwd ( string), and env\\n( map). Unknown keys are rejected. tree.buffer_spawn([\\"/bin/zsh\\"], #{ title: \\"shell\\" }) fn current_buffer fn current_buffer(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused buffer. fn current_node fn current_node(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused node. fn split fn split(_: TreeApi, direction: String, children: Array) -> TreeSpec\\nfn split(_: TreeApi, direction: String, children: Array, sizes: Array) -> TreeSpec Description Build a split with an explicit direction string. fn split_h fn split_h(_: TreeApi, children: Array) -> TreeSpec Description Build a horizontal split. fn split_v fn split_v(_: TreeApi, children: Array) -> TreeSpec Description Build a vertical split. fn tab fn tab(_: TreeApi, title: String, tree: TreeSpec) -> TabSpec Description Build a single tab specification. fn tabs fn tabs(_: TreeApi, tabs: Array) -> TreeSpec Description Build a tabs container with the first tab active. fn tabs_with_active fn tabs_with_active(_: TreeApi, tabs: Array, active: int) -> TreeSpec Description Build a tabs container with an explicit active tab.","breadcrumbs":"Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree","id":"14","title":"Tree"},"15":{"body":"Namespace: global fn current_buffer fn current_buffer(context: Context) -> ? Description Example Return the currently focused buffer, if any. ReturnType: BufferRef | () let buffer = ctx.current_buffer();\\nif buffer != () { print(buffer.title());\\n} fn current_floating fn current_floating(context: Context) -> ? Description Return the currently focused floating window, if any. ReturnType: FloatingRef | () fn current_mode fn current_mode(context: Context) -> String Description Return the active input mode name. fn current_node fn current_node(context: Context) -> ? Description Return the currently focused node, if any. ReturnType: NodeRef | () fn current_session fn current_session(context: Context) -> ? Description Return the current session reference, if any. ReturnType: SessionRef | () fn detached_buffers fn detached_buffers(context: Context) -> Array Description Return detached buffers in the current model snapshot. fn event fn event(context: Context) -> ? Description Return the current event payload, if any. ReturnType: EventInfo | () fn find_buffer fn find_buffer(context: Context, buffer_id: int) -> ? Description Find a buffer by numeric id. Returns `()` when it does not exist. ReturnType: BufferRef | () fn find_floating fn find_floating(context: Context, floating_id: int) -> ? Description Find a floating window by numeric id. Returns `()` when it does not exist. ReturnType: FloatingRef | () fn find_node fn find_node(context: Context, node_id: int) -> ? Description Find a node by numeric id. Returns `()` when it does not exist. ReturnType: NodeRef | () fn sessions fn sessions(context: Context) -> Array Description Return every visible session. fn visible_buffers fn visible_buffers(context: Context) -> Array Description Return visible buffers in the current model snapshot.","breadcrumbs":"Context » Context » Context » Context » Context » Context » Context » Context » Context » Context » Context » Context » Context » Context","id":"15","title":"Context"},"16":{"body":"Namespace: global fn current_buffer fn current_buffer(mux: MuxApi) -> ? Description Return the currently focused buffer, if any. ReturnType: BufferRef | () fn current_floating fn current_floating(mux: MuxApi) -> ? Description Return the currently focused floating window, if any. ReturnType: FloatingRef | () fn current_node fn current_node(mux: MuxApi) -> ? Description Return the currently focused node, if any. ReturnType: NodeRef | () fn current_session fn current_session(mux: MuxApi) -> ? Description Return the current session reference, if any. ReturnType: SessionRef | () fn detached_buffers fn detached_buffers(mux: MuxApi) -> Array Description Return detached buffers in the current model snapshot. fn find_buffer fn find_buffer(mux: MuxApi, buffer_id: int) -> ? Description Find a buffer by numeric id. Returns `()` when it does not exist. ReturnType: BufferRef | () fn find_floating fn find_floating(mux: MuxApi, floating_id: int) -> ? Description Find a floating window by numeric id. Returns `()` when it does not exist. ReturnType: FloatingRef | () fn find_node fn find_node(mux: MuxApi, node_id: int) -> ? Description Find a node by numeric id. Returns `()` when it does not exist. ReturnType: NodeRef | () fn sessions fn sessions(mux: MuxApi) -> Array Description Return every visible session. fn visible_buffers fn visible_buffers(mux: MuxApi) -> Array Description Return visible buffers in the current model snapshot.","breadcrumbs":"Mux » Mux » Mux » Mux » Mux » Mux » Mux » Mux » Mux » Mux » Mux » Mux","id":"16","title":"Mux"},"17":{"body":"Namespace: global fn buffer_id fn buffer_id(event: EventInfo) -> ? Description Return the buffer id attached to an event, or `()`. ReturnType: int | () fn client_id fn client_id(event: EventInfo) -> ? Description Return the client id attached to an event, or `()`. ReturnType: int | () fn floating_id fn floating_id(event: EventInfo) -> ? Description Return the floating id attached to an event, or `()`. ReturnType: int | () fn name fn name(event: EventInfo) -> String Description Return the event name. fn node_id fn node_id(event: EventInfo) -> ? Description Return the node id attached to an event, or `()`. ReturnType: int | () fn previous_session_id fn previous_session_id(event: EventInfo) -> ? Description Return the previous session id attached to an event, or `()`. ReturnType: int | () fn session_id fn session_id(event: EventInfo) -> ? Description Return the session id attached to an event, or `()`. ReturnType: int | ()","breadcrumbs":"EventInfo » EventInfo » EventInfo » EventInfo » EventInfo » EventInfo » EventInfo » EventInfo » EventInfo","id":"17","title":"EventInfo"},"18":{"body":"Namespace: global fn floating fn floating(session: SessionRef) -> Array Description Return floating window ids attached to the session. fn id fn id(session: SessionRef) -> int Description Return the numeric session id. fn name fn name(session: SessionRef) -> String Description Return the session name. fn root_node fn root_node(session: SessionRef) -> int Description Return the root tabs node for the session.","breadcrumbs":"SessionRef » SessionRef » SessionRef » SessionRef » SessionRef » SessionRef","id":"18","title":"SessionRef"},"19":{"body":"Namespace: global fn activity fn activity(buffer: BufferRef) -> String Description Return the current activity state name. fn command fn command(buffer: BufferRef) -> Array Description Return the original command vector. fn cwd fn cwd(buffer: BufferRef) -> ? Description Return the working directory, if any. ReturnType: string | () fn env_hint fn env_hint(buffer: BufferRef, key: String) -> ? Description Look up a single environment hint captured on the buffer. ReturnType: string | () fn exit_code fn exit_code(buffer: BufferRef) -> ? Description Return the process exit code, if any. ReturnType: int | () fn history_text fn history_text(buffer: BufferRef) -> String Description Example Return the full captured history text for the buffer. let buffer = ctx.current_buffer();\\nif buffer != () { let history = buffer.history_text();\\n} fn id fn id(buffer: BufferRef) -> int Description Return the numeric buffer id. fn is_attached fn is_attached(buffer: BufferRef) -> bool Description Return whether the buffer is currently attached to a node. fn is_detached fn is_detached(buffer: BufferRef) -> bool Description Return whether the buffer has been detached. fn is_running fn is_running(buffer: BufferRef) -> bool Description Return whether the buffer process is still running. fn is_visible fn is_visible(buffer: BufferRef) -> bool Description Return whether the buffer is visible in the current presentation. fn node_id fn node_id(buffer: BufferRef) -> ? Description Return the attached node id, if any. ReturnType: int | () fn pid fn pid(buffer: BufferRef) -> ? Description Return the process id, if any. ReturnType: int | () fn process_name fn process_name(buffer: BufferRef) -> ? Description Return the detected process name, if any. ReturnType: string | () fn session_id fn session_id(buffer: BufferRef) -> ? Description Return the attached session id, if any. ReturnType: int | () fn snapshot_text fn snapshot_text(buffer: BufferRef, limit: int) -> String Description Return a text snapshot limited to the requested line count. fn title fn title(buffer: BufferRef) -> String Description Return the buffer title. fn tty_path fn tty_path(buffer: BufferRef) -> ? Description Return the controlling TTY path, if any. ReturnType: string | ()","breadcrumbs":"BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef","id":"19","title":"BufferRef"},"2":{"body":"registration.rhai runtime.rhai","breadcrumbs":"Overview » Definitions","id":"2","title":"Definitions"},"20":{"body":"Namespace: global fn active_tab_index fn active_tab_index(node: NodeRef) -> ? Description Return the active tab index, if any. ReturnType: int | () fn buffer fn buffer(node: NodeRef) -> ? Description Return the attached buffer id, if any. ReturnType: int | () fn children fn children(node: NodeRef) -> Array Description Return child node ids. fn geometry fn geometry(node: NodeRef) -> ? Description Return the geometry map, if any. ReturnType: Map | () fn id fn id(node: NodeRef) -> int Description Return the node id. fn is_floating_root fn is_floating_root(node: NodeRef) -> bool Description Return whether the node is the root of a floating window. fn is_focused fn is_focused(node: NodeRef) -> bool Description Return whether the node is focused. fn is_root fn is_root(node: NodeRef) -> bool Description Return whether the node is the session root. fn is_visible fn is_visible(node: NodeRef) -> bool Description Return whether the node is visible in the current presentation. fn kind fn kind(node: NodeRef) -> String Description Return the node kind such as `buffer_view`, `split`, or `tabs`. fn parent fn parent(node: NodeRef) -> ? Description Return the parent node id, if any. ReturnType: int | () fn session_id fn session_id(node: NodeRef) -> int Description Return the owning session id. fn split_direction fn split_direction(node: NodeRef) -> ? Description Return the split direction, if any. ReturnType: string | () fn split_weights fn split_weights(node: NodeRef) -> ? Description Return split weights, if any. ReturnType: Array | () fn tab_titles fn tab_titles(node: NodeRef) -> Array Description Return tab titles on a tabs node.","breadcrumbs":"NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef","id":"20","title":"NodeRef"},"21":{"body":"Namespace: global fn geometry fn geometry(floating: FloatingRef) -> Map Description Return the floating geometry map. fn id fn id(floating: FloatingRef) -> int Description Return the floating id. fn is_focused fn is_focused(floating: FloatingRef) -> bool Description Return whether the floating is focused. fn is_visible fn is_visible(floating: FloatingRef) -> bool Description Return whether the floating is visible. fn root_node fn root_node(floating: FloatingRef) -> int Description Return the root node id. fn session_id fn session_id(floating: FloatingRef) -> int Description Return the owning session id. fn title fn title(floating: FloatingRef) -> ? Description Return the floating title, if any. ReturnType: string | ()","breadcrumbs":"FloatingRef » FloatingRef » FloatingRef » FloatingRef » FloatingRef » FloatingRef » FloatingRef » FloatingRef » FloatingRef","id":"21","title":"FloatingRef"},"22":{"body":"Namespace: global fn active_index fn active_index(bar: TabBarContext) -> int Description Return the active tab index. fn is_root fn is_root(bar: TabBarContext) -> bool Description Return whether the formatted tabs are the root tabs. fn mode fn mode(bar: TabBarContext) -> String Description Return the formatter mode name. fn node_id fn node_id(bar: TabBarContext) -> int Description Return the tabs node id currently being formatted. fn tabs fn tabs(bar: TabBarContext) -> Array Description Return tab metadata used by the formatter. fn viewport_width fn viewport_width(bar: TabBarContext) -> int Description Return the formatter viewport width in cells.","breadcrumbs":"TabBarContext » TabBarContext » TabBarContext » TabBarContext » TabBarContext » TabBarContext » TabBarContext » TabBarContext","id":"22","title":"TabBarContext"},"23":{"body":"Namespace: global fn buffer_count fn buffer_count(tab: TabInfo) -> int Description Return how many buffers are attached to the tab. fn has_activity fn has_activity(tab: TabInfo) -> bool Description Return whether the tab has activity. fn has_bell fn has_bell(tab: TabInfo) -> bool Description Return whether the tab has a bell marker. fn index fn index(tab: TabInfo) -> int Description Return the zero-based tab index. fn is_active fn is_active(tab: TabInfo) -> bool Description Return whether the tab is active. fn title fn title(tab: TabInfo) -> String Description Return the tab title.","breadcrumbs":"TabInfo » TabInfo » TabInfo » TabInfo » TabInfo » TabInfo » TabInfo » TabInfo","id":"23","title":"TabInfo"},"24":{"body":"Namespace: global fn env fn env(_: SystemApi, name: String) -> ? Description Read an environment variable, if it is set. ReturnType: string | () fn now fn now(_: SystemApi) -> int Description Return the current Unix timestamp in seconds. fn which fn which(_: SystemApi, name: String) -> ? Description Resolve an executable from `PATH`, if it is found. ReturnType: string | ()","breadcrumbs":"System » System » System » System » System","id":"24","title":"System"},"25":{"body":"Namespace: global fn bar fn bar(_: UiApi, left: Array, center: Array, right: Array) -> BarSpec Description Build a full bar specification from left, center, and right segments. fn segment fn segment(_: UiApi, text: String) -> BarSegment\\nfn segment(_: UiApi, text: String, options: Map) -> BarSegment Description Create a [`BarSegment`] from a [`UiApi`] receiver and text using default styling. segment(_: UiApi, text: String) -> BarSegment produces plain text with default\\n[ StyleSpec] values and no click target.","breadcrumbs":"UI » UI » UI » UI","id":"25","title":"UI"},"26":{"body":"Namespace: global fn color fn color(theme: ThemeRuntimeApi, name: String) -> ? Description Read a named color from the active runtime palette, if it exists. ReturnType: RgbColor | ()","breadcrumbs":"Runtime Theme » Runtime Theme » Runtime Theme","id":"26","title":"Runtime Theme"},"3":{"body":"example.md","breadcrumbs":"Overview » Example","id":"3","title":"Example"},"4":{"body":"This is a trimmed example based on the repository fixture config. It shows the two main phases together. set_leader(\\"\\"); fn shell_tree(ctx) { tree.buffer_spawn( [\\"/bin/zsh\\"], #{ title: \\"shell\\", cwd: if ctx.current_buffer() == () { () } else { ctx.current_buffer().cwd() } } )\\n} fn split_below(ctx) { action.split_with(\\"horizontal\\", shell_tree(ctx))\\n} fn format_tabs(ctx) { let active = ctx.tabs()[ctx.active_index()]; ui.bar([ ui.segment(\\" \\" + active.title() + \\" \\", #{ fg: theme.color(\\"active_fg\\"), bg: theme.color(\\"active_bg\\") }) ], [], [])\\n} define_action(\\"split-below\\", split_below);\\nbind(\\"normal\\", \\"\\\\\\"\\", \\"split-below\\");\\ntheme.set_palette(#{ active_fg: \\"#303446\\", active_bg: \\"#c6d0f5\\"\\n});\\ntabbar.set_formatter(format_tabs);\\nmouse.set_click_focus(true);","breadcrumbs":"Example » Example","id":"4","title":"Example"},"5":{"body":"Namespace: global fn bind fn bind(mode: String, notation: String, action: Action)\\nfn bind(mode: String, notation: String, action_name: String)\\nfn bind(mode: String, notation: String, actions: Array) Description Example Bind a key notation to an [`Action`], a string action name, or an array of actions. Use the Action overload for inline builders such as action.focus_left(), the string\\noverload for a named action registered with define_action, or an array to chain multiple\\nactions in sequence. bind(\\"normal\\", \\"ws\\", \\"workspace-split\\"); fn define_action fn define_action(name: String, callback: FnPtr) Description Register a function pointer as a named action callable from bindings. fn define_mode fn define_mode(mode_name: String)\\nfn define_mode(mode_name: String, options: Map) Description Define a custom input mode with hooks and fallback options. Supported options are fallback, on_enter, and on_leave. fn on fn on(event_name: String, callback: FnPtr) Description Attach a callback to an emitted event such as `buffer_bell`. fn set_leader fn set_leader(notation: String) Description Example Set the leader sequence used in binding notations. set_leader(\\"\\"); fn unbind fn unbind(mode: String, notation: String) Description Remove a previously bound key sequence.","breadcrumbs":"Registration Globals » Registration Globals » Registration Globals » Registration Globals » Registration Globals » Registration Globals » Registration Globals » Registration Globals","id":"5","title":"Registration Globals"},"6":{"body":"Namespace: global fn cancel_search fn cancel_search(_: ActionApi) -> Action Description Cancel the active search. fn cancel_selection fn cancel_selection(_: ActionApi) -> Action Description Cancel the current selection. fn chain fn chain(_: ActionApi, actions: Array) -> Action Description Chain multiple actions into one composite action. fn clear_pending_keys fn clear_pending_keys(_: ActionApi) -> Action Description Clear any partially-entered key sequence. fn close_floating fn close_floating(_: ActionApi) -> Action Description Close the currently focused floating window. fn close_floating_id fn close_floating_id(_: ActionApi, floating_id: int) -> Action Description Close a floating window by id. fn close_node fn close_node(_: ActionApi, node_id: int) -> Action Description Close a view by node id. fn close_view fn close_view(_: ActionApi) -> Action Description Close the currently focused view. fn copy_selection fn copy_selection(_: ActionApi) -> Action Description Copy the current selection into the clipboard. fn detach_buffer fn detach_buffer(_: ActionApi) -> Action Description Detach the currently focused buffer. fn detach_buffer_id fn detach_buffer_id(_: ActionApi, buffer_id: int) -> Action Description Detach a buffer by id. fn enter_mode fn enter_mode(_: ActionApi, mode: String) -> Action Description Enter a specific input mode by name. fn enter_search_mode fn enter_search_mode(_: ActionApi) -> Action Description Enter incremental search mode. fn enter_select_block fn enter_select_block(_: ActionApi) -> Action Description Enter block selection mode. fn enter_select_char fn enter_select_char(_: ActionApi) -> Action Description Enter character selection mode. fn enter_select_line fn enter_select_line(_: ActionApi) -> Action Description Enter line selection mode. fn focus_buffer fn focus_buffer(_: ActionApi, buffer_id: int) -> Action Description Focus a specific buffer by id. fn focus_down fn focus_down(_: ActionApi) -> Action Description Focus the view below the current node. fn focus_left fn focus_left(_: ActionApi) -> Action Description Example Focus the view to the left of the current node. action.focus_left() fn focus_right fn focus_right(_: ActionApi) -> Action Description Focus the view to the right of the current node. fn focus_up fn focus_up(_: ActionApi) -> Action Description Focus the view above the current node. fn follow_output fn follow_output(_: ActionApi) -> Action Description Re-enable following live output. fn insert_tab_after fn insert_tab_after(_: ActionApi, tabs_node_id: int, title: String, tree: TreeSpec) -> Action Description Insert a tab after a specific tabs node. fn insert_tab_after_current fn insert_tab_after_current(_: ActionApi, title: String, tree: TreeSpec) -> Action Description Insert a tab after the current tab in the focused tabs node. fn insert_tab_before fn insert_tab_before(_: ActionApi, tabs_node_id: int, title: String, tree: TreeSpec) -> Action Description Insert a tab before a specific tabs node. fn insert_tab_before_current fn insert_tab_before_current(_: ActionApi, title: String, tree: TreeSpec) -> Action Description Insert a tab before the current tab. fn kill_buffer fn kill_buffer(_: ActionApi) -> Action Description Kill the currently focused buffer. fn kill_buffer_id fn kill_buffer_id(_: ActionApi, buffer_id: int) -> Action Description Kill a buffer by id. fn leave_mode fn leave_mode(_: ActionApi) -> Action Description Leave the active input mode. fn move_buffer_to_floating fn move_buffer_to_floating(_: ActionApi, buffer_id: int, options: Map) -> Action Description Options Move a buffer into a new floating window. x (i16): horizontal offset from the anchor (default: 0) y (i16): vertical offset from the anchor (default: 0) width (FloatingSize): window width, as a percentage (e.g., 50%) or pixel value (default: 50%) height (FloatingSize): window height, as a percentage or pixel value (default: 50%) anchor (FloatingAnchor): anchor point for positioning, e.g., “top_left”, “center” (default: center) title (Option): window title (default: none) focus (bool): whether to focus the window after creation (default: true) close_on_empty (bool): whether to close the window when its buffer empties (default: true) fn move_buffer_to_node fn move_buffer_to_node(_: ActionApi, buffer_id: int, node_id: int) -> Action Description Move a buffer into a specific node. fn next_current_tabs fn next_current_tabs(_: ActionApi) -> Action Description Select the next tab in the currently focused tabs node. fn next_tab fn next_tab(_: ActionApi, tabs_node_id: int) -> Action Description Select the next tab in a specific tabs node. fn noop fn noop(_: ActionApi) -> Action Description Build a no-op action. fn notify fn notify(_: ActionApi, level: String, message: String) -> Action Description Emit a client notification. fn open_floating fn open_floating(_: ActionApi, tree: TreeSpec, options: Map) -> Action Description Open a floating view around the provided tree. fn prev_current_tabs fn prev_current_tabs(_: ActionApi) -> Action Description Select the previous tab in the currently focused tabs node. fn prev_tab fn prev_tab(_: ActionApi, tabs_node_id: int) -> Action Description Select the previous tab in a specific tabs node. fn replace_current_with fn replace_current_with(_: ActionApi, tree: TreeSpec) -> Action Description Replace the focused node with a new tree. fn replace_node fn replace_node(_: ActionApi, node_id: int, tree: TreeSpec) -> Action Description Replace a specific node by id with a new tree. fn reveal_buffer fn reveal_buffer(_: ActionApi, buffer_id: int) -> Action Description Reveal a specific buffer by id. fn run_named_action fn run_named_action(_: ActionApi, name: String) -> Action Description Run another named action by name. fn scroll_line_down fn scroll_line_down(_: ActionApi) -> Action Description Scroll one line downward in local scrollback. fn scroll_line_up fn scroll_line_up(_: ActionApi) -> Action Description Scroll one line upward in local scrollback. fn scroll_page_down fn scroll_page_down(_: ActionApi) -> Action Description Scroll one page downward in local scrollback. fn scroll_page_up fn scroll_page_up(_: ActionApi) -> Action Description Scroll one page upward in local scrollback. fn scroll_to_bottom fn scroll_to_bottom(_: ActionApi) -> Action Description Scroll to the bottom of local scrollback. fn scroll_to_top fn scroll_to_top(_: ActionApi) -> Action Description Scroll to the top of local scrollback. fn search_next fn search_next(_: ActionApi) -> Action Description Jump to the next search match. fn search_prev fn search_prev(_: ActionApi) -> Action Description Jump to the previous search match. fn select_current_tabs fn select_current_tabs(_: ActionApi, index: int) -> Action Description Select a tab by index in the currently focused tabs node. fn select_move_down fn select_move_down(_: ActionApi) -> Action Description Move the active selection down. fn select_move_left fn select_move_left(_: ActionApi) -> Action Description Move the active selection left. fn select_move_right fn select_move_right(_: ActionApi) -> Action Description Move the active selection right. fn select_move_up fn select_move_up(_: ActionApi) -> Action Description Move the active selection up. fn select_tab fn select_tab(_: ActionApi, tabs_node_id: int, index: int) -> Action Description Select a tab by index in a specific tabs node. fn send_bytes fn send_bytes(_: ActionApi, buffer_id: int, bytes: String) -> Action\\nfn send_bytes(_: ActionApi, buffer_id: int, bytes: Array) -> Action Description Send a string of bytes to a specific buffer. fn send_bytes_current fn send_bytes_current(_: ActionApi, bytes: String) -> Action\\nfn send_bytes_current(_: ActionApi, bytes: Array) -> Action Description Send a string of bytes to the focused buffer. fn send_keys fn send_keys(_: ActionApi, buffer_id: int, notation: String) -> Action Description Send a key notation sequence to a specific buffer. fn send_keys_current fn send_keys_current(_: ActionApi, notation: String) -> Action Description Send a key notation sequence to the focused buffer. fn split_with fn split_with(_: ActionApi, direction: String, tree: TreeSpec) -> Action Description Split the current node and attach the provided tree as the new sibling. fn toggle_mode fn toggle_mode(_: ActionApi, mode: String) -> Action Description Toggle a named input mode. fn yank_selection fn yank_selection(_: ActionApi) -> Action Description Copy the current selection into the clipboard.","breadcrumbs":"Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration)","id":"6","title":"Action (Registration)"},"7":{"body":"Namespace: global fn buffer_attach fn buffer_attach(_: TreeApi, buffer_id: int) -> TreeSpec Description Attach an existing buffer by id. fn buffer_current fn buffer_current(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused buffer. fn buffer_empty fn buffer_empty(_: TreeApi) -> TreeSpec Description Build an empty buffer tree node. fn buffer_spawn fn buffer_spawn(_: TreeApi, command: Array) -> TreeSpec\\nfn buffer_spawn(_: TreeApi, command: Array, options: Map) -> TreeSpec Description Example Spawn a new buffer from a command array. Supported options keys are title ( string), cwd ( string), and env\\n( map). Unknown keys are rejected. tree.buffer_spawn([\\"/bin/zsh\\"], #{ title: \\"shell\\" }) fn current_buffer fn current_buffer(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused buffer. fn current_node fn current_node(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused node. fn split fn split(_: TreeApi, direction: String, children: Array) -> TreeSpec\\nfn split(_: TreeApi, direction: String, children: Array, sizes: Array) -> TreeSpec Description Build a split with an explicit direction string. fn split_h fn split_h(_: TreeApi, children: Array) -> TreeSpec Description Build a horizontal split. fn split_v fn split_v(_: TreeApi, children: Array) -> TreeSpec Description Build a vertical split. fn tab fn tab(_: TreeApi, title: String, tree: TreeSpec) -> TabSpec Description Build a single tab specification. fn tabs fn tabs(_: TreeApi, tabs: Array) -> TreeSpec Description Build a tabs container with the first tab active. fn tabs_with_active fn tabs_with_active(_: TreeApi, tabs: Array, active: int) -> TreeSpec Description Build a tabs container with an explicit active tab.","breadcrumbs":"Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration)","id":"7","title":"Tree (Registration)"},"8":{"body":"Namespace: global fn env fn env(_: SystemApi, name: String) -> ? Description Read an environment variable, if it is set. ReturnType: string | () fn now fn now(_: SystemApi) -> int Description Return the current Unix timestamp in seconds. fn which fn which(_: SystemApi, name: String) -> ? Description Resolve an executable from `PATH`, if it is found. ReturnType: string | ()","breadcrumbs":"System (Registration) » System (Registration) » System (Registration) » System (Registration) » System (Registration)","id":"8","title":"System (Registration)"},"9":{"body":"Namespace: global fn bar fn bar(_: UiApi, left: Array, center: Array, right: Array) -> BarSpec Description Build a full bar specification from left, center, and right segments. fn segment fn segment(_: UiApi, text: String) -> BarSegment\\nfn segment(_: UiApi, text: String, options: Map) -> BarSegment Description Create a [`BarSegment`] from a [`UiApi`] receiver and text using default styling. segment(_: UiApi, text: String) -> BarSegment produces plain text with default\\n[ StyleSpec] values and no click target.","breadcrumbs":"UI (Registration) » UI (Registration) » UI (Registration) » UI (Registration)","id":"9","title":"UI (Registration)"}},"length":27,"save":true},"fields":["title","body","breadcrumbs"],"index":{"body":{"root":{"0":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"3":{"0":{"3":{"4":{"4":{"6":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"5":{"0":{"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}},"a":{"b":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{".":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"\\"":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}}},"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":8.06225774829855},"6":{"tf":8.06225774829855}}}}},"df":5,"docs":{"0":{"tf":1.4142135623730951},"1":{"tf":1.4142135623730951},"13":{"tf":8.426149773176359},"5":{"tf":3.1622776601683795},"6":{"tf":8.426149773176359}}}},"v":{"df":11,"docs":{"13":{"tf":2.449489742783178},"14":{"tf":1.7320508075688772},"15":{"tf":1.0},"19":{"tf":1.4142135623730951},"20":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.4142135623730951},"26":{"tf":1.0},"4":{"tf":1.0},"6":{"tf":2.449489742783178},"7":{"tf":1.7320508075688772}},"e":{".":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"_":{"b":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"22":{"tf":1.0}}}}},"df":0,"docs":{}}},"t":{"a":{"b":{"_":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"y":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"d":{"d":{"df":1,"docs":{"11":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{},"g":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}}},"df":0,"docs":{}},"n":{"c":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}}},"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"0":{"tf":1.0}}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}}},"r":{"a":{"df":0,"docs":{},"y":{"df":13,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":3.1622776601683795},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.7320508075688772},"22":{"tf":1.0},"25":{"tf":1.7320508075688772},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772},"7":{"tf":3.1622776601683795},"9":{"tf":1.7320508075688772}}}},"df":0,"docs":{}}},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":10,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"17":{"tf":2.449489742783178},"18":{"tf":1.0},"19":{"tf":1.7320508075688772},"20":{"tf":1.0},"23":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}},"df":5,"docs":{"0":{"tf":1.0},"1":{"tf":1.0},"12":{"tf":1.0},"25":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":2,"docs":{"25":{"tf":2.0},"9":{"tf":2.0}}}},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}}}},"s":{"df":0,"docs":{},"e":{"df":2,"docs":{"23":{"tf":1.0},"4":{"tf":1.0}}}}},"df":0,"docs":{},"e":{"df":1,"docs":{"22":{"tf":1.0}},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"h":{"a":{"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"23":{"tf":1.0}}},"o":{"df":0,"docs":{},"w":{"df":3,"docs":{"13":{"tf":1.0},"4":{"tf":1.4142135623730951},"6":{"tf":1.0}}}}}},"g":{"df":1,"docs":{"4":{"tf":1.0}}},"i":{"df":0,"docs":{},"n":{"/":{"df":0,"docs":{},"z":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"d":{"(":{"\\"":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.7320508075688772}}},"df":0,"docs":{}}}},"df":2,"docs":{"0":{"tf":1.0},"5":{"tf":2.0}}},"df":0,"docs":{}}},"l":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":8,"docs":{"10":{"tf":2.0},"13":{"tf":1.4142135623730951},"19":{"tf":2.0},"20":{"tf":2.0},"21":{"tf":1.4142135623730951},"22":{"tf":1.0},"23":{"tf":1.7320508075688772},"6":{"tf":1.4142135623730951}}}},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"n":{"d":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},".":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"y":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}}}},"_":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"5":{"tf":1.0}}}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":1,"docs":{"23":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"y":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":7,"docs":{"13":{"tf":3.0},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":3.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"s":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"df":0,"docs":{}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":1,"docs":{"20":{"tf":1.0}}}}}}},"df":12,"docs":{"1":{"tf":1.0},"10":{"tf":1.4142135623730951},"13":{"tf":3.605551275463989},"14":{"tf":2.23606797749979},"15":{"tf":2.449489742783178},"16":{"tf":2.0},"17":{"tf":1.0},"19":{"tf":3.1622776601683795},"20":{"tf":1.4142135623730951},"23":{"tf":1.0},"6":{"tf":3.605551275463989},"7":{"tf":2.23606797749979}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":4.358898943540674}}}}}}}}},"i":{"df":0,"docs":{},"l":{"d":{"df":6,"docs":{"13":{"tf":1.0},"14":{"tf":3.1622776601683795},"25":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":3.1622776601683795},"9":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"5":{"tf":1.0}}}}},"df":0,"docs":{}}}},"y":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}}},"c":{"6":{"d":{"0":{"df":0,"docs":{},"f":{"5":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"a":{"b":{"df":0,"docs":{},"l":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.7320508075688772}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"n":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.4142135623730951}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"22":{"tf":1.0}}}},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"25":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}}}}},"h":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":3,"docs":{"13":{"tf":1.4142135623730951},"5":{"tf":1.0},"6":{"tf":1.4142135623730951}}}},"r":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"l":{"d":{"df":1,"docs":{"20":{"tf":1.0}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":3,"docs":{"14":{"tf":2.0},"20":{"tf":1.0},"7":{"tf":2.0}}}}}},"df":0,"docs":{}}}},"l":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"_":{"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"y":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"df":0,"docs":{}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"i":{"c":{"df":0,"docs":{},"k":{"df":3,"docs":{"10":{"tf":1.4142135623730951},"25":{"tf":1.0},"9":{"tf":1.0}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":1,"docs":{"17":{"tf":1.0}}},"df":0,"docs":{}}},"df":5,"docs":{"0":{"tf":1.0},"10":{"tf":1.0},"13":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":1.0}}}}},"p":{"b":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"df":0,"docs":{}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}}}}},"o":{"d":{"df":0,"docs":{},"e":{"df":1,"docs":{"19":{"tf":1.0}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":1,"docs":{"26":{"tf":1.0}}}}}}},"df":2,"docs":{"11":{"tf":1.0},"26":{"tf":1.4142135623730951}}}}},"m":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"n":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":3,"docs":{"14":{"tf":1.7320508075688772},"19":{"tf":1.4142135623730951},"7":{"tf":1.7320508075688772}}},"df":0,"docs":{}}},"df":0,"docs":{}},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":2,"docs":{"0":{"tf":1.4142135623730951},"4":{"tf":1.0}}}}},"t":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":2,"docs":{"1":{"tf":1.4142135623730951},"15":{"tf":3.605551275463989}}}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"y":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"t":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"t":{"df":0,"docs":{},"x":{".":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.0},"19":{"tf":1.0},"4":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{")":{".":{"c":{"df":0,"docs":{},"w":{"d":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"df":0,"docs":{},"s":{"(":{")":{"[":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{".":{"a":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":4,"docs":{"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}}}}},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"15":{"tf":1.0}},"e":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"n":{"df":0,"docs":{},"o":{"d":{"df":4,"docs":{"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}}},"df":11,"docs":{"13":{"tf":4.123105625617661},"14":{"tf":1.7320508075688772},"15":{"tf":2.6457513110645907},"16":{"tf":2.449489742783178},"19":{"tf":1.7320508075688772},"20":{"tf":1.0},"22":{"tf":1.0},"24":{"tf":1.0},"6":{"tf":4.123105625617661},"7":{"tf":1.7320508075688772},"8":{"tf":1.0}}}}}}},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}}}}},"w":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.0}}}}}}}},"df":0,"docs":{}},"df":4,"docs":{"14":{"tf":1.0},"19":{"tf":1.0},"4":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}}},"d":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"l":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"f":{"a":{"df":0,"docs":{},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":2.8284271247461903},"25":{"tf":1.4142135623730951},"6":{"tf":2.8284271247461903},"9":{"tf":1.4142135623730951}}}}}},"df":1,"docs":{"0":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.4142135623730951}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"\\"":{"df":0,"docs":{},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}},"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"0":{"tf":1.0},"2":{"tf":1.0}}}}}}},"s":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":22,"docs":{"10":{"tf":2.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":7.937253933193772},"14":{"tf":3.4641016151377544},"15":{"tf":3.4641016151377544},"16":{"tf":3.1622776601683795},"17":{"tf":2.6457513110645907},"18":{"tf":2.0},"19":{"tf":4.242640687119285},"20":{"tf":3.872983346207417},"21":{"tf":2.6457513110645907},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"24":{"tf":1.7320508075688772},"25":{"tf":1.4142135623730951},"26":{"tf":1.0},"5":{"tf":2.449489742783178},"6":{"tf":7.937253933193772},"7":{"tf":3.4641016151377544},"8":{"tf":1.7320508075688772},"9":{"tf":1.4142135623730951}}}}}}},"df":0,"docs":{}},"t":{"a":{"c":{"df":0,"docs":{},"h":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":5,"docs":{"13":{"tf":1.4142135623730951},"15":{"tf":1.0},"16":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.4142135623730951}},"e":{"d":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}},"df":0,"docs":{}}}},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":5,"docs":{"13":{"tf":1.0},"14":{"tf":1.7320508075688772},"20":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.7320508075688772}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{".":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{},"m":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":4,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0}}}}}},"n":{"a":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"k":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"c":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}},"v":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":4,"docs":{"14":{"tf":1.0},"24":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0}},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":3,"docs":{"19":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}}}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{}},"df":6,"docs":{"0":{"tf":1.0},"1":{"tf":1.0},"10":{"tf":1.0},"15":{"tf":1.4142135623730951},"17":{"tf":2.6457513110645907},"5":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":2,"docs":{"15":{"tf":1.0},"17":{"tf":2.8284271247461903}}}}}}}}}},"x":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":9,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"15":{"tf":1.0},"19":{"tf":1.0},"3":{"tf":1.0},"4":{"tf":1.4142135623730951},"5":{"tf":1.4142135623730951},"6":{"tf":1.0},"7":{"tf":1.0}},"e":{".":{"df":0,"docs":{},"m":{"d":{"df":1,"docs":{"3":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":3,"docs":{"0":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":5,"docs":{"14":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"26":{"tf":1.0},"7":{"tf":1.0}}}},"t":{"_":{"c":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}}}},"f":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":1,"docs":{"0":{"tf":1.4142135623730951}}}},"n":{"d":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":2,"docs":{"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772}}},"df":0,"docs":{}},"r":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"x":{"df":0,"docs":{},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":9,"docs":{"1":{"tf":1.0},"13":{"tf":2.0},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"17":{"tf":1.0},"18":{"tf":1.4142135623730951},"20":{"tf":1.0},"21":{"tf":2.23606797749979},"6":{"tf":2.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":5,"docs":{"13":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"a":{"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"21":{"tf":2.8284271247461903}}}}},"s":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}}},"df":0,"docs":{}}},"n":{"df":23,"docs":{"10":{"tf":2.8284271247461903},"11":{"tf":1.4142135623730951},"12":{"tf":1.4142135623730951},"13":{"tf":11.313708498984761},"14":{"tf":5.0990195135927845},"15":{"tf":4.898979485566356},"16":{"tf":4.47213595499958},"17":{"tf":3.7416573867739413},"18":{"tf":2.8284271247461903},"19":{"tf":6.0},"20":{"tf":5.477225575051661},"21":{"tf":3.7416573867739413},"22":{"tf":3.4641016151377544},"23":{"tf":3.4641016151377544},"24":{"tf":2.449489742783178},"25":{"tf":2.23606797749979},"26":{"tf":1.4142135623730951},"4":{"tf":1.7320508075688772},"5":{"tf":3.872983346207417},"6":{"tf":11.313708498984761},"7":{"tf":5.0990195135927845},"8":{"tf":2.449489742783178},"9":{"tf":2.23606797749979}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.4142135623730951}}}}}},"o":{"c":{"df":0,"docs":{},"u":{"df":3,"docs":{"10":{"tf":1.0},"13":{"tf":2.6457513110645907},"6":{"tf":2.6457513110645907}},"s":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":9,"docs":{"10":{"tf":1.4142135623730951},"13":{"tf":3.3166247903554},"14":{"tf":1.7320508075688772},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"20":{"tf":1.0},"21":{"tf":1.0},"6":{"tf":3.3166247903554},"7":{"tf":1.7320508075688772}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"_":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":0,"docs":{},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"12":{"tf":1.0},"22":{"tf":1.4142135623730951}},"t":{"df":2,"docs":{"0":{"tf":1.0},"22":{"tf":1.7320508075688772}}}}},"df":0,"docs":{}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":1,"docs":{"10":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}}}},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":3,"docs":{"19":{"tf":1.0},"25":{"tf":1.0},"9":{"tf":1.0}}}},"n":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}}},"o":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":2,"docs":{"20":{"tf":1.4142135623730951},"21":{"tf":1.4142135623730951}}},"y":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"l":{"df":0,"docs":{},"o":{"b":{"a":{"df":0,"docs":{},"l":{"df":23,"docs":{"1":{"tf":1.0},"10":{"tf":1.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":1.0},"25":{"tf":1.0},"26":{"tf":1.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"h":{"a":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}}}},"df":0,"docs":{}},"s":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"23":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"y":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"23":{"tf":1.0}},"l":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"19":{"tf":1.4142135623730951}}},"y":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"k":{"df":1,"docs":{"5":{"tf":1.0}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0}}}}}}}}}},"i":{"1":{"6":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":1,"docs":{"21":{"tf":1.0}}}},"df":0,"docs":{}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":12,"docs":{"13":{"tf":2.6457513110645907},"14":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":2.449489742783178},"18":{"tf":1.7320508075688772},"19":{"tf":2.23606797749979},"20":{"tf":2.449489742783178},"21":{"tf":2.0},"22":{"tf":1.0},"6":{"tf":2.6457513110645907},"7":{"tf":1.0}}},"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":5,"docs":{"13":{"tf":2.0},"20":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.4142135623730951},"6":{"tf":2.0}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":1,"docs":{"1":{"tf":1.4142135623730951}}}},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"5":{"tf":1.0}}}}},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.7320508075688772},"15":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.7320508075688772}}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"_":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}}},"t":{"df":15,"docs":{"13":{"tf":4.47213595499958},"14":{"tf":1.4142135623730951},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":2.449489742783178},"18":{"tf":1.4142135623730951},"19":{"tf":2.449489742783178},"20":{"tf":2.23606797749979},"21":{"tf":1.7320508075688772},"22":{"tf":1.7320508075688772},"23":{"tf":1.4142135623730951},"24":{"tf":1.0},"6":{"tf":4.47213595499958},"7":{"tf":1.4142135623730951},"8":{"tf":1.0}}}},"s":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"23":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"_":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":2,"docs":{"20":{"tf":1.0},"21":{"tf":1.0}},"e":{"d":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"df":0,"docs":{}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":2,"docs":{"20":{"tf":1.0},"22":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"n":{"df":1,"docs":{"19":{"tf":1.0}},"n":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":3,"docs":{"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0}},"i":{"b":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"j":{"df":0,"docs":{},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"y":{"df":6,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":1.4142135623730951},"19":{"tf":1.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.7320508075688772},"7":{"tf":1.4142135623730951}}}},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"n":{"d":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.4142135623730951}}},"df":0,"docs":{}}}},"l":{"df":0,"docs":{},"e":{"a":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{">":{"df":0,"docs":{},"w":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}}}},"df":0,"docs":{},"v":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"25":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":3,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.4142135623730951}}}}},"n":{"df":0,"docs":{},"e":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"19":{"tf":1.0},"6":{"tf":1.7320508075688772}}}},"v":{"df":0,"docs":{},"e":{"df":3,"docs":{"0":{"tf":1.4142135623730951},"13":{"tf":1.0},"6":{"tf":1.0}}}}},"o":{"c":{"a":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}},"df":0,"docs":{}},"df":0,"docs":{},"o":{"df":0,"docs":{},"k":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"m":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"4":{"tf":1.0}}}},"n":{"df":0,"docs":{},"i":{"df":1,"docs":{"23":{"tf":1.0}}}},"p":{"<":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}},"df":10,"docs":{"11":{"tf":1.0},"13":{"tf":1.4142135623730951},"14":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.4142135623730951},"25":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.4142135623730951},"7":{"tf":1.0},"9":{"tf":1.0}}},"r":{"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"23":{"tf":1.0}}}}}},"t":{"c":{"df":0,"docs":{},"h":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"a":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"t":{"a":{"d":{"a":{"df":0,"docs":{},"t":{"a":{"df":1,"docs":{"22":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"o":{"d":{"df":0,"docs":{},"e":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":6,"docs":{"0":{"tf":1.0},"13":{"tf":3.0},"15":{"tf":1.0},"22":{"tf":1.4142135623730951},"5":{"tf":1.0},"6":{"tf":3.0}},"l":{"df":2,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":2,"docs":{"1":{"tf":1.0},"10":{"tf":1.4142135623730951}},"e":{".":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"u":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"10":{"tf":2.0}}}}},"df":0,"docs":{}}}},"v":{"df":0,"docs":{},"e":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}}}}},"x":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"16":{"tf":3.1622776601683795}}}}},"df":2,"docs":{"1":{"tf":1.0},"16":{"tf":1.0}}}}},"n":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":13,"docs":{"0":{"tf":1.4142135623730951},"11":{"tf":1.0},"13":{"tf":2.23606797749979},"15":{"tf":1.0},"17":{"tf":1.4142135623730951},"18":{"tf":1.4142135623730951},"19":{"tf":1.4142135623730951},"22":{"tf":1.0},"24":{"tf":1.4142135623730951},"26":{"tf":1.4142135623730951},"5":{"tf":1.7320508075688772},"6":{"tf":2.23606797749979},"8":{"tf":1.4142135623730951}},"s":{"df":0,"docs":{},"p":{"a":{"c":{"df":22,"docs":{"10":{"tf":1.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":1.0},"25":{"tf":1.0},"26":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":4,"docs":{"13":{"tf":2.0},"14":{"tf":1.0},"6":{"tf":2.0},"7":{"tf":1.0}}},"x":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}}}},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":7,"docs":{"13":{"tf":1.7320508075688772},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"19":{"tf":1.0},"22":{"tf":1.0},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}}},"df":13,"docs":{"1":{"tf":1.0},"13":{"tf":4.242640687119285},"14":{"tf":1.4142135623730951},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.4142135623730951},"20":{"tf":3.0},"21":{"tf":1.0},"22":{"tf":1.0},"6":{"tf":4.242640687119285},"7":{"tf":1.4142135623730951}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"20":{"tf":4.0}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"o":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"t":{"a":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":2.0},"5":{"tf":2.449489742783178},"6":{"tf":2.0}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"y":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"w":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":4,"docs":{"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"18":{"tf":1.0},"19":{"tf":1.0}}}}}}},"o":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}},"n":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"_":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.0}}}}},"l":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"v":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}},"p":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"<":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":7,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":1.4142135623730951},"25":{"tf":1.0},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772},"7":{"tf":1.4142135623730951},"9":{"tf":1.0}}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"t":{"df":0,"docs":{},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"d":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"20":{"tf":1.0},"21":{"tf":1.0}}}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":3,"docs":{"1":{"tf":1.0},"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":2,"docs":{"11":{"tf":1.4142135623730951},"26":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.4142135623730951}}}}},"t":{"df":0,"docs":{},"i":{"a":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"t":{"df":0,"docs":{},"h":{"df":3,"docs":{"19":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}},"y":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"d":{"df":1,"docs":{"15":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"a":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{}}}}},"df":0,"docs":{}}},"h":{"a":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":2,"docs":{"0":{"tf":1.0},"4":{"tf":1.0}}}}},"df":0,"docs":{}},"i":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}},"df":0,"docs":{},"x":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"l":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}},"df":0,"docs":{}},"o":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"5":{"tf":1.0}}}}}}},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"19":{"tf":1.0},"20":{"tf":1.0}}}}}},"v":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"17":{"tf":1.0},"6":{"tf":1.7320508075688772}},"s":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":1,"docs":{"17":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}}},"df":1,"docs":{"5":{"tf":1.0}}}}}}}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{".":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"15":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"o":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":1,"docs":{"19":{"tf":2.0}}}}}},"d":{"df":0,"docs":{},"u":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}}}}},"r":{"df":0,"docs":{},"e":{"a":{"d":{"df":3,"docs":{"24":{"tf":1.0},"26":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"f":{"df":1,"docs":{"1":{"tf":2.0}},"e":{"df":0,"docs":{},"r":{"df":5,"docs":{"0":{"tf":1.0},"14":{"tf":1.7320508075688772},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.7320508075688772}}}}},"g":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.4142135623730951}},"r":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{".":{"df":0,"docs":{},"r":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"2":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":7,"docs":{"0":{"tf":1.0},"1":{"tf":2.23606797749979},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}}}}}},"j":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}},"df":0,"docs":{}}},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":1,"docs":{"5":{"tf":1.0}}}}},"p":{"df":0,"docs":{},"l":{"a":{"c":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}}}},"q":{"df":0,"docs":{},"u":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"s":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"v":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}}}},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"n":{"df":11,"docs":{"15":{"tf":3.4641016151377544},"16":{"tf":3.1622776601683795},"17":{"tf":2.6457513110645907},"18":{"tf":2.0},"19":{"tf":4.123105625617661},"20":{"tf":3.872983346207417},"21":{"tf":2.6457513110645907},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"24":{"tf":1.0},"8":{"tf":1.0}},"t":{"df":0,"docs":{},"y":{"df":0,"docs":{},"p":{"df":9,"docs":{"15":{"tf":2.8284271247461903},"16":{"tf":2.6457513110645907},"17":{"tf":2.449489742783178},"19":{"tf":2.8284271247461903},"20":{"tf":2.449489742783178},"21":{"tf":1.0},"24":{"tf":1.4142135623730951},"26":{"tf":1.0},"8":{"tf":1.4142135623730951}}}}}}}}},"v":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"l":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"g":{"b":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"26":{"tf":1.0}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"25":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"18":{"tf":1.0},"21":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":4,"docs":{"18":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0},"22":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"d":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":4,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.0}},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"m":{"df":3,"docs":{"0":{"tf":1.0},"1":{"tf":1.4142135623730951},"26":{"tf":1.4142135623730951}},"e":{".":{"df":0,"docs":{},"r":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"2":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}},"s":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"t":{"df":0,"docs":{},"o":{"_":{"b":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":3,"docs":{"10":{"tf":1.0},"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}}}},"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"p":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}},"df":0,"docs":{}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{},"g":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"25":{"tf":1.7320508075688772},"9":{"tf":1.7320508075688772}}},"df":0,"docs":{}},"df":2,"docs":{"25":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}}}}},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":4.0},"6":{"tf":4.0}}}},"df":0,"docs":{}}},"n":{"d":{"_":{"b":{"df":0,"docs":{},"y":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"y":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}},"df":0,"docs":{}},"q":{"df":0,"docs":{},"u":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"c":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}}}}},"s":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":4,"docs":{"17":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0}}},"df":0,"docs":{}}},"df":8,"docs":{"1":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":1.4142135623730951},"18":{"tf":2.0},"19":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.0},"16":{"tf":1.0},"18":{"tf":2.23606797749979}}}}},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"t":{"_":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":1,"docs":{"10":{"tf":1.0}},"s":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"w":{"a":{"df":0,"docs":{},"r":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":1,"docs":{"10":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"12":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"12":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"l":{"df":0,"docs":{},"e":{"a":{"d":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"\\"":{"<":{"c":{"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"p":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"11":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":1,"docs":{"11":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"w":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"w":{"a":{"df":0,"docs":{},"r":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":1,"docs":{"10":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"s":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":1,"docs":{"10":{"tf":1.0}},"l":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":4,"docs":{"0":{"tf":1.0},"24":{"tf":1.0},"5":{"tf":1.0},"8":{"tf":1.0}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":3,"docs":{"14":{"tf":1.0},"4":{"tf":1.0},"7":{"tf":1.0}}}}},"o":{"df":0,"docs":{},"w":{"df":1,"docs":{"4":{"tf":1.0}}}}},"i":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"d":{"df":0,"docs":{},"e":{"df":1,"docs":{"10":{"tf":1.0}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"df":0,"docs":{},"l":{"df":3,"docs":{"14":{"tf":1.0},"19":{"tf":1.0},"7":{"tf":1.0}}}}},"z":{"df":0,"docs":{},"e":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"n":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":1.0}}}}}}}},"df":0,"docs":{}},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"i":{"df":0,"docs":{},"f":{"df":6,"docs":{"13":{"tf":3.4641016151377544},"14":{"tf":1.0},"25":{"tf":1.0},"6":{"tf":3.4641016151377544},"7":{"tf":1.0},"9":{"tf":1.0}}}}},"df":0,"docs":{}},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"_":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}},"df":1,"docs":{"4":{"tf":1.0}}}}}}},"d":{"df":0,"docs":{},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"v":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"w":{"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}},"s":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":7,"docs":{"13":{"tf":1.0},"14":{"tf":2.0},"20":{"tf":1.7320508075688772},"4":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":2.0}}}}}},"t":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":2,"docs":{"0":{"tf":1.0},"19":{"tf":1.0}}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"19":{"tf":1.0}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"df":18,"docs":{"13":{"tf":4.0},"14":{"tf":2.6457513110645907},"15":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":3.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":2.0},"25":{"tf":1.7320508075688772},"26":{"tf":1.0},"5":{"tf":4.0},"6":{"tf":4.0},"7":{"tf":2.6457513110645907},"8":{"tf":2.0},"9":{"tf":1.7320508075688772}}}}}},"y":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}}}}}}},"u":{"c":{"df":0,"docs":{},"h":{"df":2,"docs":{"20":{"tf":1.0},"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{},"p":{"df":0,"docs":{},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":3,"docs":{"14":{"tf":1.0},"5":{"tf":1.0},"7":{"tf":1.0}}}}}}}},"y":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"24":{"tf":1.7320508075688772},"8":{"tf":1.7320508075688772}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"24":{"tf":1.0},"8":{"tf":1.0}}}}}}}},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"20":{"tf":1.0}},"e":{"df":0,"docs":{},"s":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"b":{"a":{"df":0,"docs":{},"r":{".":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"12":{"tf":1.0}}}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"22":{"tf":2.6457513110645907}}}}}}}}},"df":2,"docs":{"1":{"tf":1.0},"12":{"tf":1.0}}}},"df":0,"docs":{}},"df":11,"docs":{"0":{"tf":1.0},"1":{"tf":1.4142135623730951},"12":{"tf":1.0},"13":{"tf":4.58257569495584},"14":{"tf":3.0},"18":{"tf":1.0},"20":{"tf":2.0},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"6":{"tf":4.58257569495584},"7":{"tf":3.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":1,"docs":{"23":{"tf":2.6457513110645907}}}}}},"s":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":3,"docs":{"19":{"tf":1.4142135623730951},"25":{"tf":2.23606797749979},"9":{"tf":2.23606797749979}}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{".":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"(":{"\\"":{"a":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"b":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"11":{"tf":1.0}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"11":{"tf":1.4142135623730951},"26":{"tf":1.0}},"r":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"26":{"tf":1.0}}}}},"df":0,"docs":{}}}}}}}}}}}},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":1,"docs":{"0":{"tf":1.0}},"s":{"df":0,"docs":{},"t":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"t":{"df":0,"docs":{},"l":{"df":9,"docs":{"13":{"tf":2.449489742783178},"14":{"tf":1.7320508075688772},"19":{"tf":1.4142135623730951},"20":{"tf":1.0},"21":{"tf":1.4142135623730951},"23":{"tf":1.4142135623730951},"4":{"tf":1.0},"6":{"tf":2.449489742783178},"7":{"tf":1.7320508075688772}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"o":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":1,"docs":{"4":{"tf":1.0}}}}},"g":{"df":0,"docs":{},"l":{"df":3,"docs":{"10":{"tf":2.0},"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}},"p":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":3,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"6":{"tf":1.0}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{".":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"[":{"\\"":{"/":{"b":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"/":{"df":0,"docs":{},"z":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"14":{"tf":3.7416573867739413},"7":{"tf":3.7416573867739413}}}}},"df":5,"docs":{"1":{"tf":1.4142135623730951},"13":{"tf":3.4641016151377544},"14":{"tf":2.449489742783178},"6":{"tf":3.4641016151377544},"7":{"tf":2.449489742783178}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":4,"docs":{"13":{"tf":2.8284271247461903},"14":{"tf":3.7416573867739413},"6":{"tf":2.8284271247461903},"7":{"tf":3.7416573867739413}}},"df":0,"docs":{}}}}}},"i":{"df":0,"docs":{},"m":{"df":1,"docs":{"4":{"tf":1.0}}}},"u":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"t":{"df":0,"docs":{},"i":{"df":1,"docs":{"19":{"tf":1.0}}},"y":{"_":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"w":{"df":0,"docs":{},"o":{"df":2,"docs":{"0":{"tf":1.0},"4":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"i":{".":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"25":{"tf":2.23606797749979},"9":{"tf":2.23606797749979}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"25":{"tf":1.0},"9":{"tf":1.0}}},"n":{"b":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"x":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"k":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}}}},"p":{"df":3,"docs":{"13":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.0}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":6,"docs":{"0":{"tf":1.0},"12":{"tf":1.0},"22":{"tf":1.0},"25":{"tf":1.0},"5":{"tf":1.4142135623730951},"9":{"tf":1.0}}}},"v":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"u":{"df":5,"docs":{"10":{"tf":2.0},"13":{"tf":1.4142135623730951},"25":{"tf":1.0},"6":{"tf":1.4142135623730951},"9":{"tf":1.0}}}},"r":{"df":0,"docs":{},"i":{"a":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"c":{"df":4,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}}}}},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":2,"docs":{"13":{"tf":2.6457513110645907},"6":{"tf":2.6457513110645907}},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"d":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"22":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":1,"docs":{"22":{"tf":1.0}}}}}}}},"s":{"df":0,"docs":{},"i":{"b":{"df":0,"docs":{},"l":{"df":5,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0}},"e":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"u":{"a":{"df":0,"docs":{},"l":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}}}}},"w":{"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}}}}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"10":{"tf":1.4142135623730951}}}},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":7,"docs":{"13":{"tf":1.4142135623730951},"19":{"tf":2.0},"20":{"tf":2.0},"21":{"tf":1.4142135623730951},"22":{"tf":1.0},"23":{"tf":1.7320508075688772},"6":{"tf":1.4142135623730951}}}}}}},"i":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"i":{"d":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":3,"docs":{"13":{"tf":1.4142135623730951},"22":{"tf":1.0},"6":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":6,"docs":{"13":{"tf":2.8284271247461903},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"18":{"tf":1.0},"20":{"tf":1.0},"6":{"tf":2.8284271247461903}}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"k":{"df":1,"docs":{"19":{"tf":1.0}},"s":{"df":0,"docs":{},"p":{"a":{"c":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"x":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"y":{"a":{"df":0,"docs":{},"n":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"z":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":1,"docs":{"23":{"tf":1.0}}}}}}}},"breadcrumbs":{"root":{"0":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"3":{"0":{"3":{"4":{"4":{"6":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"5":{"0":{"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}},"a":{"b":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{".":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"\\"":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}}},"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":8.06225774829855},"6":{"tf":8.06225774829855}}}}},"df":5,"docs":{"0":{"tf":1.4142135623730951},"1":{"tf":1.4142135623730951},"13":{"tf":11.661903789690601},"5":{"tf":3.1622776601683795},"6":{"tf":11.661903789690601}}}},"v":{"df":11,"docs":{"13":{"tf":2.449489742783178},"14":{"tf":1.7320508075688772},"15":{"tf":1.0},"19":{"tf":1.4142135623730951},"20":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.4142135623730951},"26":{"tf":1.0},"4":{"tf":1.0},"6":{"tf":2.449489742783178},"7":{"tf":1.7320508075688772}},"e":{".":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"_":{"b":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"22":{"tf":1.0}}}}},"df":0,"docs":{}}},"t":{"a":{"b":{"_":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"y":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"d":{"d":{"df":1,"docs":{"11":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{},"g":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}}},"df":0,"docs":{}},"n":{"c":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}}},"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"0":{"tf":1.4142135623730951}}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}}},"r":{"a":{"df":0,"docs":{},"y":{"df":13,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":3.1622776601683795},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.7320508075688772},"22":{"tf":1.0},"25":{"tf":1.7320508075688772},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772},"7":{"tf":3.1622776601683795},"9":{"tf":1.7320508075688772}}}},"df":0,"docs":{}}},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":10,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"17":{"tf":2.449489742783178},"18":{"tf":1.0},"19":{"tf":1.7320508075688772},"20":{"tf":1.0},"23":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}},"df":5,"docs":{"0":{"tf":1.0},"1":{"tf":1.0},"12":{"tf":1.0},"25":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":2,"docs":{"25":{"tf":2.0},"9":{"tf":2.0}}}},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}}}},"s":{"df":0,"docs":{},"e":{"df":2,"docs":{"23":{"tf":1.0},"4":{"tf":1.0}}}}},"df":0,"docs":{},"e":{"df":1,"docs":{"22":{"tf":1.0}},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"h":{"a":{"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"23":{"tf":1.0}}},"o":{"df":0,"docs":{},"w":{"df":3,"docs":{"13":{"tf":1.0},"4":{"tf":1.4142135623730951},"6":{"tf":1.0}}}}}},"g":{"df":1,"docs":{"4":{"tf":1.0}}},"i":{"df":0,"docs":{},"n":{"/":{"df":0,"docs":{},"z":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"d":{"(":{"\\"":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.7320508075688772}}},"df":0,"docs":{}}}},"df":2,"docs":{"0":{"tf":1.0},"5":{"tf":2.0}}},"df":0,"docs":{}}},"l":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":8,"docs":{"10":{"tf":2.0},"13":{"tf":1.4142135623730951},"19":{"tf":2.0},"20":{"tf":2.0},"21":{"tf":1.4142135623730951},"22":{"tf":1.0},"23":{"tf":1.7320508075688772},"6":{"tf":1.4142135623730951}}}},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"n":{"d":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},".":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"y":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}}}},"_":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"5":{"tf":1.0}}}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":1,"docs":{"23":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"y":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":7,"docs":{"13":{"tf":3.0},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":3.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"s":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"df":0,"docs":{}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":1,"docs":{"20":{"tf":1.0}}}}}}},"df":12,"docs":{"1":{"tf":1.0},"10":{"tf":1.4142135623730951},"13":{"tf":3.605551275463989},"14":{"tf":2.23606797749979},"15":{"tf":2.449489742783178},"16":{"tf":2.0},"17":{"tf":1.0},"19":{"tf":3.1622776601683795},"20":{"tf":1.4142135623730951},"23":{"tf":1.0},"6":{"tf":3.605551275463989},"7":{"tf":2.23606797749979}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":6.244997998398398}}}}}}}}},"i":{"df":0,"docs":{},"l":{"d":{"df":6,"docs":{"13":{"tf":1.0},"14":{"tf":3.1622776601683795},"25":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":3.1622776601683795},"9":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"5":{"tf":1.0}}}}},"df":0,"docs":{}}}},"y":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}}},"c":{"6":{"d":{"0":{"df":0,"docs":{},"f":{"5":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"a":{"b":{"df":0,"docs":{},"l":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.7320508075688772}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"n":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.4142135623730951}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"22":{"tf":1.0}}}},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"25":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}}}}},"h":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":3,"docs":{"13":{"tf":1.4142135623730951},"5":{"tf":1.0},"6":{"tf":1.4142135623730951}}}},"r":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"l":{"d":{"df":1,"docs":{"20":{"tf":1.0}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":3,"docs":{"14":{"tf":2.0},"20":{"tf":1.0},"7":{"tf":2.0}}}}}},"df":0,"docs":{}}}},"l":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"_":{"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"y":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"df":0,"docs":{}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"i":{"c":{"df":0,"docs":{},"k":{"df":3,"docs":{"10":{"tf":1.4142135623730951},"25":{"tf":1.0},"9":{"tf":1.0}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":1,"docs":{"17":{"tf":1.0}}},"df":0,"docs":{}}},"df":5,"docs":{"0":{"tf":1.0},"10":{"tf":1.0},"13":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":1.0}}}}},"p":{"b":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"df":0,"docs":{}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}}}}},"o":{"d":{"df":0,"docs":{},"e":{"df":1,"docs":{"19":{"tf":1.0}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":1,"docs":{"26":{"tf":1.0}}}}}}},"df":2,"docs":{"11":{"tf":1.0},"26":{"tf":1.4142135623730951}}}}},"m":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"n":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":3,"docs":{"14":{"tf":1.7320508075688772},"19":{"tf":1.4142135623730951},"7":{"tf":1.7320508075688772}}},"df":0,"docs":{}}},"df":0,"docs":{}},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":2,"docs":{"0":{"tf":1.7320508075688772},"4":{"tf":1.0}}}}},"t":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":2,"docs":{"1":{"tf":1.4142135623730951},"15":{"tf":5.196152422706632}}}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"y":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"t":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"t":{"df":0,"docs":{},"x":{".":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.0},"19":{"tf":1.0},"4":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{")":{".":{"c":{"df":0,"docs":{},"w":{"d":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"df":0,"docs":{},"s":{"(":{")":{"[":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{".":{"a":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":4,"docs":{"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}}}}},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"15":{"tf":1.0}},"e":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"n":{"df":0,"docs":{},"o":{"d":{"df":4,"docs":{"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}}},"df":11,"docs":{"13":{"tf":4.123105625617661},"14":{"tf":1.7320508075688772},"15":{"tf":2.6457513110645907},"16":{"tf":2.449489742783178},"19":{"tf":1.7320508075688772},"20":{"tf":1.0},"22":{"tf":1.0},"24":{"tf":1.0},"6":{"tf":4.123105625617661},"7":{"tf":1.7320508075688772},"8":{"tf":1.0}}}}}}},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}}}}},"w":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.0}}}}}}}},"df":0,"docs":{}},"df":4,"docs":{"14":{"tf":1.0},"19":{"tf":1.0},"4":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}}},"d":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"l":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"f":{"a":{"df":0,"docs":{},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":2.8284271247461903},"25":{"tf":1.4142135623730951},"6":{"tf":2.8284271247461903},"9":{"tf":1.4142135623730951}}}}}},"df":1,"docs":{"0":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.4142135623730951}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"\\"":{"df":0,"docs":{},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}},"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"0":{"tf":1.0},"2":{"tf":1.4142135623730951}}}}}}},"s":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":22,"docs":{"10":{"tf":2.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":7.937253933193772},"14":{"tf":3.4641016151377544},"15":{"tf":3.4641016151377544},"16":{"tf":3.1622776601683795},"17":{"tf":2.6457513110645907},"18":{"tf":2.0},"19":{"tf":4.242640687119285},"20":{"tf":3.872983346207417},"21":{"tf":2.6457513110645907},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"24":{"tf":1.7320508075688772},"25":{"tf":1.4142135623730951},"26":{"tf":1.0},"5":{"tf":2.449489742783178},"6":{"tf":7.937253933193772},"7":{"tf":3.4641016151377544},"8":{"tf":1.7320508075688772},"9":{"tf":1.4142135623730951}}}}}}},"df":0,"docs":{}},"t":{"a":{"c":{"df":0,"docs":{},"h":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":5,"docs":{"13":{"tf":1.4142135623730951},"15":{"tf":1.0},"16":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.4142135623730951}},"e":{"d":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}},"df":0,"docs":{}}}},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":5,"docs":{"13":{"tf":1.0},"14":{"tf":1.7320508075688772},"20":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.7320508075688772}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{".":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{},"m":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.7320508075688772}}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":4,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0}}}}}},"n":{"a":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"k":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"c":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}},"v":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":4,"docs":{"14":{"tf":1.0},"24":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0}},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":3,"docs":{"19":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}}}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{}},"df":6,"docs":{"0":{"tf":1.0},"1":{"tf":1.0},"10":{"tf":1.0},"15":{"tf":1.4142135623730951},"17":{"tf":2.6457513110645907},"5":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":2,"docs":{"15":{"tf":1.0},"17":{"tf":4.123105625617661}}}}}}}}}},"x":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":9,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"15":{"tf":1.0},"19":{"tf":1.0},"3":{"tf":1.4142135623730951},"4":{"tf":2.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.0},"7":{"tf":1.0}},"e":{".":{"df":0,"docs":{},"m":{"d":{"df":1,"docs":{"3":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":3,"docs":{"0":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":5,"docs":{"14":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"26":{"tf":1.0},"7":{"tf":1.0}}}},"t":{"_":{"c":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}}}},"f":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":1,"docs":{"0":{"tf":1.4142135623730951}}}},"n":{"d":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":2,"docs":{"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772}}},"df":0,"docs":{}},"r":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"x":{"df":0,"docs":{},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":9,"docs":{"1":{"tf":1.0},"13":{"tf":2.0},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"17":{"tf":1.0},"18":{"tf":1.4142135623730951},"20":{"tf":1.0},"21":{"tf":2.23606797749979},"6":{"tf":2.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":5,"docs":{"13":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"a":{"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"21":{"tf":4.123105625617661}}}}},"s":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}}},"df":0,"docs":{}}},"n":{"df":23,"docs":{"10":{"tf":2.8284271247461903},"11":{"tf":1.4142135623730951},"12":{"tf":1.4142135623730951},"13":{"tf":11.313708498984761},"14":{"tf":5.0990195135927845},"15":{"tf":4.898979485566356},"16":{"tf":4.47213595499958},"17":{"tf":3.7416573867739413},"18":{"tf":2.8284271247461903},"19":{"tf":6.0},"20":{"tf":5.477225575051661},"21":{"tf":3.7416573867739413},"22":{"tf":3.4641016151377544},"23":{"tf":3.4641016151377544},"24":{"tf":2.449489742783178},"25":{"tf":2.23606797749979},"26":{"tf":1.4142135623730951},"4":{"tf":1.7320508075688772},"5":{"tf":3.872983346207417},"6":{"tf":11.313708498984761},"7":{"tf":5.0990195135927845},"8":{"tf":2.449489742783178},"9":{"tf":2.23606797749979}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.4142135623730951}}}}}},"o":{"c":{"df":0,"docs":{},"u":{"df":3,"docs":{"10":{"tf":1.0},"13":{"tf":2.6457513110645907},"6":{"tf":2.6457513110645907}},"s":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":9,"docs":{"10":{"tf":1.4142135623730951},"13":{"tf":3.3166247903554},"14":{"tf":1.7320508075688772},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"20":{"tf":1.0},"21":{"tf":1.0},"6":{"tf":3.3166247903554},"7":{"tf":1.7320508075688772}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"_":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":0,"docs":{},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"12":{"tf":1.0},"22":{"tf":1.4142135623730951}},"t":{"df":2,"docs":{"0":{"tf":1.0},"22":{"tf":1.7320508075688772}}}}},"df":0,"docs":{}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":1,"docs":{"10":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}}}},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":3,"docs":{"19":{"tf":1.0},"25":{"tf":1.0},"9":{"tf":1.0}}}},"n":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}}},"o":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":2,"docs":{"20":{"tf":1.4142135623730951},"21":{"tf":1.4142135623730951}}},"y":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"l":{"df":0,"docs":{},"o":{"b":{"a":{"df":0,"docs":{},"l":{"df":23,"docs":{"1":{"tf":1.0},"10":{"tf":1.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":1.0},"25":{"tf":1.0},"26":{"tf":1.0},"5":{"tf":3.1622776601683795},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"h":{"a":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}}}},"df":0,"docs":{}},"s":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"23":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"y":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"23":{"tf":1.0}},"l":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"19":{"tf":1.4142135623730951}}},"y":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"k":{"df":1,"docs":{"5":{"tf":1.0}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0}}}}}}}}}},"i":{"1":{"6":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":1,"docs":{"21":{"tf":1.0}}}},"df":0,"docs":{}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":12,"docs":{"13":{"tf":2.6457513110645907},"14":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":2.449489742783178},"18":{"tf":1.7320508075688772},"19":{"tf":2.23606797749979},"20":{"tf":2.449489742783178},"21":{"tf":2.0},"22":{"tf":1.0},"6":{"tf":2.6457513110645907},"7":{"tf":1.0}}},"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":5,"docs":{"13":{"tf":2.0},"20":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.4142135623730951},"6":{"tf":2.0}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":1,"docs":{"1":{"tf":1.4142135623730951}}}},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"5":{"tf":1.0}}}}},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.7320508075688772},"15":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.7320508075688772}}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"_":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}}},"t":{"df":15,"docs":{"13":{"tf":4.47213595499958},"14":{"tf":1.4142135623730951},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":2.449489742783178},"18":{"tf":1.4142135623730951},"19":{"tf":2.449489742783178},"20":{"tf":2.23606797749979},"21":{"tf":1.7320508075688772},"22":{"tf":1.7320508075688772},"23":{"tf":1.4142135623730951},"24":{"tf":1.0},"6":{"tf":4.47213595499958},"7":{"tf":1.4142135623730951},"8":{"tf":1.0}}}},"s":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"23":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"_":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":2,"docs":{"20":{"tf":1.0},"21":{"tf":1.0}},"e":{"d":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"df":0,"docs":{}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":2,"docs":{"20":{"tf":1.0},"22":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"n":{"df":1,"docs":{"19":{"tf":1.0}},"n":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":3,"docs":{"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0}},"i":{"b":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"j":{"df":0,"docs":{},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"y":{"df":6,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":1.4142135623730951},"19":{"tf":1.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.7320508075688772},"7":{"tf":1.4142135623730951}}}},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"n":{"d":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.4142135623730951}}},"df":0,"docs":{}}}},"l":{"df":0,"docs":{},"e":{"a":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{">":{"df":0,"docs":{},"w":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}}}},"df":0,"docs":{},"v":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"25":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":3,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.4142135623730951}}}}},"n":{"df":0,"docs":{},"e":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"19":{"tf":1.0},"6":{"tf":1.7320508075688772}}}},"v":{"df":0,"docs":{},"e":{"df":3,"docs":{"0":{"tf":1.4142135623730951},"13":{"tf":1.0},"6":{"tf":1.0}}}}},"o":{"c":{"a":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}},"df":0,"docs":{}},"df":0,"docs":{},"o":{"df":0,"docs":{},"k":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"m":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"4":{"tf":1.0}}}},"n":{"df":0,"docs":{},"i":{"df":1,"docs":{"23":{"tf":1.0}}}},"p":{"<":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}},"df":10,"docs":{"11":{"tf":1.0},"13":{"tf":1.4142135623730951},"14":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.4142135623730951},"25":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.4142135623730951},"7":{"tf":1.0},"9":{"tf":1.0}}},"r":{"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"23":{"tf":1.0}}}}}},"t":{"c":{"df":0,"docs":{},"h":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"a":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"t":{"a":{"d":{"a":{"df":0,"docs":{},"t":{"a":{"df":1,"docs":{"22":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"o":{"d":{"df":0,"docs":{},"e":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":6,"docs":{"0":{"tf":1.0},"13":{"tf":3.0},"15":{"tf":1.0},"22":{"tf":1.4142135623730951},"5":{"tf":1.0},"6":{"tf":3.0}},"l":{"df":2,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":2,"docs":{"1":{"tf":1.0},"10":{"tf":2.8284271247461903}},"e":{".":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"u":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"10":{"tf":2.0}}}}},"df":0,"docs":{}}}},"v":{"df":0,"docs":{},"e":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}}}}},"x":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"16":{"tf":3.1622776601683795}}}}},"df":2,"docs":{"1":{"tf":1.0},"16":{"tf":3.605551275463989}}}}},"n":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":13,"docs":{"0":{"tf":1.4142135623730951},"11":{"tf":1.0},"13":{"tf":2.23606797749979},"15":{"tf":1.0},"17":{"tf":1.4142135623730951},"18":{"tf":1.4142135623730951},"19":{"tf":1.4142135623730951},"22":{"tf":1.0},"24":{"tf":1.4142135623730951},"26":{"tf":1.4142135623730951},"5":{"tf":1.7320508075688772},"6":{"tf":2.23606797749979},"8":{"tf":1.4142135623730951}},"s":{"df":0,"docs":{},"p":{"a":{"c":{"df":22,"docs":{"10":{"tf":1.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":1.0},"25":{"tf":1.0},"26":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":4,"docs":{"13":{"tf":2.0},"14":{"tf":1.0},"6":{"tf":2.0},"7":{"tf":1.0}}},"x":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}}}},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":7,"docs":{"13":{"tf":1.7320508075688772},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"19":{"tf":1.0},"22":{"tf":1.0},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}}},"df":13,"docs":{"1":{"tf":1.0},"13":{"tf":4.242640687119285},"14":{"tf":1.4142135623730951},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.4142135623730951},"20":{"tf":3.0},"21":{"tf":1.0},"22":{"tf":1.0},"6":{"tf":4.242640687119285},"7":{"tf":1.4142135623730951}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"20":{"tf":5.744562646538029}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"o":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"t":{"a":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":2.0},"5":{"tf":2.449489742783178},"6":{"tf":2.0}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"y":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"w":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":4,"docs":{"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"18":{"tf":1.0},"19":{"tf":1.0}}}}}}},"o":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}},"n":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"_":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.0}}}}},"l":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"v":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}},"p":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"<":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":7,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":1.4142135623730951},"25":{"tf":1.0},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772},"7":{"tf":1.4142135623730951},"9":{"tf":1.0}}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"t":{"df":0,"docs":{},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"d":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"df":0,"docs":{}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":4,"docs":{"0":{"tf":1.0},"1":{"tf":1.0},"2":{"tf":1.0},"3":{"tf":1.0}}}}}}}}},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"20":{"tf":1.0},"21":{"tf":1.0}}}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":3,"docs":{"1":{"tf":1.4142135623730951},"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":2,"docs":{"11":{"tf":1.4142135623730951},"26":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.4142135623730951}}}}},"t":{"df":0,"docs":{},"i":{"a":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"t":{"df":0,"docs":{},"h":{"df":3,"docs":{"19":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}},"y":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"d":{"df":1,"docs":{"15":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"a":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{}}}}},"df":0,"docs":{}}},"h":{"a":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":2,"docs":{"0":{"tf":1.0},"4":{"tf":1.0}}}}},"df":0,"docs":{}},"i":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}},"df":0,"docs":{},"x":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"l":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}},"df":0,"docs":{}},"o":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"5":{"tf":1.0}}}}}}},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"19":{"tf":1.0},"20":{"tf":1.0}}}}}},"v":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"17":{"tf":1.0},"6":{"tf":1.7320508075688772}},"s":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":1,"docs":{"17":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}}},"df":1,"docs":{"5":{"tf":1.0}}}}}}}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{".":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"15":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"o":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":1,"docs":{"19":{"tf":2.0}}}}}},"d":{"df":0,"docs":{},"u":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}}}}},"r":{"df":0,"docs":{},"e":{"a":{"d":{"df":3,"docs":{"24":{"tf":1.0},"26":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"f":{"df":1,"docs":{"1":{"tf":2.0}},"e":{"df":0,"docs":{},"r":{"df":5,"docs":{"0":{"tf":1.0},"14":{"tf":1.7320508075688772},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.7320508075688772}}}}},"g":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.4142135623730951}},"r":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{".":{"df":0,"docs":{},"r":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"2":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":7,"docs":{"0":{"tf":1.0},"1":{"tf":2.23606797749979},"5":{"tf":3.0},"6":{"tf":8.12403840463596},"7":{"tf":3.872983346207417},"8":{"tf":2.449489742783178},"9":{"tf":2.23606797749979}}}}}}},"j":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}},"df":0,"docs":{}}},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":1,"docs":{"5":{"tf":1.0}}}}},"p":{"df":0,"docs":{},"l":{"a":{"c":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}}}},"q":{"df":0,"docs":{},"u":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"s":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"v":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}}}},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"n":{"df":11,"docs":{"15":{"tf":3.4641016151377544},"16":{"tf":3.1622776601683795},"17":{"tf":2.6457513110645907},"18":{"tf":2.0},"19":{"tf":4.123105625617661},"20":{"tf":3.872983346207417},"21":{"tf":2.6457513110645907},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"24":{"tf":1.0},"8":{"tf":1.0}},"t":{"df":0,"docs":{},"y":{"df":0,"docs":{},"p":{"df":9,"docs":{"15":{"tf":2.8284271247461903},"16":{"tf":2.6457513110645907},"17":{"tf":2.449489742783178},"19":{"tf":2.8284271247461903},"20":{"tf":2.449489742783178},"21":{"tf":1.0},"24":{"tf":1.4142135623730951},"26":{"tf":1.0},"8":{"tf":1.4142135623730951}}}}}}}}},"v":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"l":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"g":{"b":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"26":{"tf":1.0}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"25":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"18":{"tf":1.0},"21":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":4,"docs":{"18":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0},"22":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"d":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":4,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.0}},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"m":{"df":3,"docs":{"0":{"tf":1.0},"1":{"tf":1.4142135623730951},"26":{"tf":2.23606797749979}},"e":{".":{"df":0,"docs":{},"r":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"2":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}},"s":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"t":{"df":0,"docs":{},"o":{"_":{"b":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":3,"docs":{"10":{"tf":1.0},"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}}}},"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"p":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}},"df":0,"docs":{}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{},"g":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"25":{"tf":1.7320508075688772},"9":{"tf":1.7320508075688772}}},"df":0,"docs":{}},"df":2,"docs":{"25":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}}}}},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":4.0},"6":{"tf":4.0}}}},"df":0,"docs":{}}},"n":{"d":{"_":{"b":{"df":0,"docs":{},"y":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"y":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}},"df":0,"docs":{}},"q":{"df":0,"docs":{},"u":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"c":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}}}}},"s":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":4,"docs":{"17":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0}}},"df":0,"docs":{}}},"df":8,"docs":{"1":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":1.4142135623730951},"18":{"tf":2.0},"19":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.0},"16":{"tf":1.0},"18":{"tf":3.3166247903554}}}}},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"t":{"_":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":1,"docs":{"10":{"tf":1.0}},"s":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"w":{"a":{"df":0,"docs":{},"r":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":1,"docs":{"10":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"12":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"12":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"l":{"df":0,"docs":{},"e":{"a":{"d":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"\\"":{"<":{"c":{"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"p":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"11":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":1,"docs":{"11":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"w":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"w":{"a":{"df":0,"docs":{},"r":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":1,"docs":{"10":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"s":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":1,"docs":{"10":{"tf":1.0}},"l":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":4,"docs":{"0":{"tf":1.0},"24":{"tf":1.0},"5":{"tf":1.0},"8":{"tf":1.0}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":3,"docs":{"14":{"tf":1.0},"4":{"tf":1.0},"7":{"tf":1.0}}}}},"o":{"df":0,"docs":{},"w":{"df":1,"docs":{"4":{"tf":1.0}}}}},"i":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"d":{"df":0,"docs":{},"e":{"df":1,"docs":{"10":{"tf":1.0}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"df":0,"docs":{},"l":{"df":3,"docs":{"14":{"tf":1.0},"19":{"tf":1.0},"7":{"tf":1.0}}}}},"z":{"df":0,"docs":{},"e":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"n":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":1.0}}}}}}}},"df":0,"docs":{}},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"i":{"df":0,"docs":{},"f":{"df":6,"docs":{"13":{"tf":3.4641016151377544},"14":{"tf":1.0},"25":{"tf":1.0},"6":{"tf":3.4641016151377544},"7":{"tf":1.0},"9":{"tf":1.0}}}}},"df":0,"docs":{}},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"_":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}},"df":1,"docs":{"4":{"tf":1.0}}}}}}},"d":{"df":0,"docs":{},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"v":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"w":{"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}},"s":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":7,"docs":{"13":{"tf":1.0},"14":{"tf":2.0},"20":{"tf":1.7320508075688772},"4":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":2.0}}}}}},"t":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":2,"docs":{"0":{"tf":1.0},"19":{"tf":1.0}}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"19":{"tf":1.0}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"df":18,"docs":{"13":{"tf":4.0},"14":{"tf":2.6457513110645907},"15":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":3.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":2.0},"25":{"tf":1.7320508075688772},"26":{"tf":1.0},"5":{"tf":4.0},"6":{"tf":4.0},"7":{"tf":2.6457513110645907},"8":{"tf":2.0},"9":{"tf":1.7320508075688772}}}}}},"y":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}}}}}}},"u":{"c":{"df":0,"docs":{},"h":{"df":2,"docs":{"20":{"tf":1.0},"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{},"p":{"df":0,"docs":{},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":3,"docs":{"14":{"tf":1.0},"5":{"tf":1.0},"7":{"tf":1.0}}}}}}}},"y":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"24":{"tf":1.7320508075688772},"8":{"tf":1.7320508075688772}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"24":{"tf":2.449489742783178},"8":{"tf":2.449489742783178}}}}}}}},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"20":{"tf":1.0}},"e":{"df":0,"docs":{},"s":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"b":{"a":{"df":0,"docs":{},"r":{".":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"12":{"tf":1.0}}}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"22":{"tf":3.872983346207417}}}}}}}}},"df":2,"docs":{"1":{"tf":1.0},"12":{"tf":2.0}}}},"df":0,"docs":{}},"df":11,"docs":{"0":{"tf":1.0},"1":{"tf":1.4142135623730951},"12":{"tf":1.0},"13":{"tf":4.58257569495584},"14":{"tf":3.0},"18":{"tf":1.0},"20":{"tf":2.0},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"6":{"tf":4.58257569495584},"7":{"tf":3.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":1,"docs":{"23":{"tf":3.872983346207417}}}}}},"s":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":3,"docs":{"19":{"tf":1.4142135623730951},"25":{"tf":2.23606797749979},"9":{"tf":2.23606797749979}}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{".":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"(":{"\\"":{"a":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"b":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"11":{"tf":1.0}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"11":{"tf":2.23606797749979},"26":{"tf":2.0}},"r":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"26":{"tf":1.0}}}}},"df":0,"docs":{}}}}}}}}}}}},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":1,"docs":{"0":{"tf":1.0}},"s":{"df":0,"docs":{},"t":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"t":{"df":0,"docs":{},"l":{"df":9,"docs":{"13":{"tf":2.449489742783178},"14":{"tf":1.7320508075688772},"19":{"tf":1.4142135623730951},"20":{"tf":1.0},"21":{"tf":1.4142135623730951},"23":{"tf":1.4142135623730951},"4":{"tf":1.0},"6":{"tf":2.449489742783178},"7":{"tf":1.7320508075688772}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"o":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":1,"docs":{"4":{"tf":1.0}}}}},"g":{"df":0,"docs":{},"l":{"df":3,"docs":{"10":{"tf":2.0},"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}},"p":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":3,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"6":{"tf":1.0}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{".":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"[":{"\\"":{"/":{"b":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"/":{"df":0,"docs":{},"z":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"14":{"tf":3.7416573867739413},"7":{"tf":3.7416573867739413}}}}},"df":5,"docs":{"1":{"tf":1.4142135623730951},"13":{"tf":3.4641016151377544},"14":{"tf":4.47213595499958},"6":{"tf":3.4641016151377544},"7":{"tf":4.47213595499958}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":4,"docs":{"13":{"tf":2.8284271247461903},"14":{"tf":3.7416573867739413},"6":{"tf":2.8284271247461903},"7":{"tf":3.7416573867739413}}},"df":0,"docs":{}}}}}},"i":{"df":0,"docs":{},"m":{"df":1,"docs":{"4":{"tf":1.0}}}},"u":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"t":{"df":0,"docs":{},"i":{"df":1,"docs":{"19":{"tf":1.0}}},"y":{"_":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"w":{"df":0,"docs":{},"o":{"df":2,"docs":{"0":{"tf":1.0},"4":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"i":{".":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"25":{"tf":2.23606797749979},"9":{"tf":2.23606797749979}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"25":{"tf":2.23606797749979},"9":{"tf":2.23606797749979}}},"n":{"b":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"x":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"k":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}}}},"p":{"df":3,"docs":{"13":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.0}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":6,"docs":{"0":{"tf":1.0},"12":{"tf":1.0},"22":{"tf":1.0},"25":{"tf":1.0},"5":{"tf":1.4142135623730951},"9":{"tf":1.0}}}},"v":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"u":{"df":5,"docs":{"10":{"tf":2.0},"13":{"tf":1.4142135623730951},"25":{"tf":1.0},"6":{"tf":1.4142135623730951},"9":{"tf":1.0}}}},"r":{"df":0,"docs":{},"i":{"a":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"c":{"df":4,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}}}}},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":2,"docs":{"13":{"tf":2.6457513110645907},"6":{"tf":2.6457513110645907}},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"d":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"22":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":1,"docs":{"22":{"tf":1.0}}}}}}}},"s":{"df":0,"docs":{},"i":{"b":{"df":0,"docs":{},"l":{"df":5,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0}},"e":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"u":{"a":{"df":0,"docs":{},"l":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}}}}},"w":{"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}}}}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"10":{"tf":1.4142135623730951}}}},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":7,"docs":{"13":{"tf":1.4142135623730951},"19":{"tf":2.0},"20":{"tf":2.0},"21":{"tf":1.4142135623730951},"22":{"tf":1.0},"23":{"tf":1.7320508075688772},"6":{"tf":1.4142135623730951}}}}}}},"i":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"i":{"d":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":3,"docs":{"13":{"tf":1.4142135623730951},"22":{"tf":1.0},"6":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":6,"docs":{"13":{"tf":2.8284271247461903},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"18":{"tf":1.0},"20":{"tf":1.0},"6":{"tf":2.8284271247461903}}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"k":{"df":1,"docs":{"19":{"tf":1.0}},"s":{"df":0,"docs":{},"p":{"a":{"c":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"x":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"y":{"a":{"df":0,"docs":{},"n":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"z":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":1,"docs":{"23":{"tf":1.0}}}}}}}},"title":{"root":{"a":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"0":{"tf":1.0}}}}},"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}}}}}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":1,"docs":{"0":{"tf":1.0}}}}},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"2":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}}},"df":0,"docs":{}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":1,"docs":{"17":{"tf":1.0}}}}}}}}}},"x":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":2,"docs":{"3":{"tf":1.0},"4":{"tf":1.0}}}}}},"df":0,"docs":{}}},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":1,"docs":{"21":{"tf":1.0}}}}}}}}}},"df":0,"docs":{}}}},"g":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"b":{"a":{"df":0,"docs":{},"l":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":1,"docs":{"20":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":1,"docs":{"1":{"tf":1.0}}}}},"df":0,"docs":{}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":5,"docs":{"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}}}}}}},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"m":{"df":1,"docs":{"26":{"tf":1.0}}}}}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":1,"docs":{"18":{"tf":1.0}}}}}}}}}}},"y":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}}}}}},"t":{"a":{"b":{"b":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"22":{"tf":1.0}}}}}}}}},"df":1,"docs":{"12":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":1,"docs":{"23":{"tf":1.0}}}}}}},"df":0,"docs":{}},"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":2,"docs":{"11":{"tf":1.0},"26":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"i":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}}}},"lang":"English","pipeline":["trimmer","stopWordFilter","stemmer"],"ref":"id","version":"0.9.5"},"results_options":{"limit_results":30,"teaser_word_count":30},"search_options":{"bool":"OR","expand":true,"fields":{"body":{"boost":1},"breadcrumbs":{"boost":1},"title":{"boost":2}}}}')); \ No newline at end of file diff --git a/docs/config-api-book/searchindex-60b5c002.js b/docs/config-api-book/searchindex-60b5c002.js new file mode 100644 index 0000000..bf22cf9 --- /dev/null +++ b/docs/config-api-book/searchindex-60b5c002.js @@ -0,0 +1 @@ +(function(){const target=(window.__EMBERS_CONFIG_API_SEARCH__&&typeof window.__EMBERS_CONFIG_API_SEARCH__==="object")?window.__EMBERS_CONFIG_API_SEARCH__:(window.__EMBERS_CONFIG_API_SEARCH__={});let parsed={};try{parsed=JSON.parse('{"doc_urls":["index.html#embers-config-api","index.html#pages","index.html#definitions","index.html#example","example.html#example","registration-globals.html#registration-globals","registration-action.html#action-registration","registration-tree.html#tree-registration","registration-system.html#system-registration","registration-ui.html#ui-registration","mouse.html#mouse","theme.html#theme","tabbar.html#tabbar","action.html#action","tree.html#tree","context.html#context","mux.html#mux","event-info.html#eventinfo","session-ref.html#sessionref","buffer-ref.html#bufferref","node-ref.html#noderef","floating-ref.html#floatingref","tab-bar-context.html#tabbarcontext","tab-info.html#tabinfo","system-runtime.html#system","ui.html#ui","runtime-theme.html#runtime-theme"],"index":{"documentStore":{"docInfo":{"0":{"body":41,"breadcrumbs":4,"title":3},"1":{"body":37,"breadcrumbs":2,"title":1},"10":{"body":55,"breadcrumbs":6,"title":1},"11":{"body":15,"breadcrumbs":3,"title":1},"12":{"body":16,"breadcrumbs":3,"title":1},"13":{"body":1185,"breadcrumbs":77,"title":1},"14":{"body":202,"breadcrumbs":14,"title":1},"15":{"body":165,"breadcrumbs":14,"title":1},"16":{"body":136,"breadcrumbs":12,"title":1},"17":{"body":91,"breadcrumbs":9,"title":1},"18":{"body":48,"breadcrumbs":6,"title":1},"19":{"body":230,"breadcrumbs":20,"title":1},"2":{"body":2,"breadcrumbs":2,"title":1},"20":{"body":178,"breadcrumbs":17,"title":1},"21":{"body":78,"breadcrumbs":9,"title":1},"22":{"body":75,"breadcrumbs":8,"title":1},"23":{"body":70,"breadcrumbs":8,"title":1},"24":{"body":41,"breadcrumbs":5,"title":1},"25":{"body":76,"breadcrumbs":4,"title":1},"26":{"body":19,"breadcrumbs":6,"title":2},"3":{"body":1,"breadcrumbs":2,"title":1},"4":{"body":50,"breadcrumbs":2,"title":1},"5":{"body":136,"breadcrumbs":16,"title":2},"6":{"body":1185,"breadcrumbs":154,"title":2},"7":{"body":202,"breadcrumbs":28,"title":2},"8":{"body":41,"breadcrumbs":10,"title":2},"9":{"body":76,"breadcrumbs":8,"title":2}},"docs":{"0":{"body":"This reference is generated from the Rust-backed Rhai exports used by Embers. There are two execution phases: registration time: the top-level config file where you declare modes, bindings, named actions, and visual settings runtime: named actions, event handlers, and tab bar formatters that run against live client state Definition files live in defs/.","breadcrumbs":"Overview » Embers Config API","id":"0","title":"Embers Config API"},"1":{"body":"action buffer-ref context event-info floating-ref mouse mux node-ref registration-action registration-globals registration-system registration-tree registration-ui runtime-theme session-ref system-runtime tab-bar-context tab-info tabbar theme tree ui","breadcrumbs":"Overview » Pages","id":"1","title":"Pages"},"10":{"body":"Namespace: global fn set_click_focus fn set_click_focus(mouse: MouseApi, value: bool) Description Toggle focus-on-click behavior. fn set_click_forward fn set_click_forward(mouse: MouseApi, value: bool) Description Toggle forwarding mouse clicks into the focused buffer. fn set_wheel_forward fn set_wheel_forward(mouse: MouseApi, value: bool) Description Toggle wheel event forwarding into the focused buffer. fn set_wheel_scroll fn set_wheel_scroll(mouse: MouseApi, value: bool) Description Toggle client-side wheel scrolling.","breadcrumbs":"Mouse » Mouse » Mouse » Mouse » Mouse » Mouse","id":"10","title":"Mouse"},"11":{"body":"Namespace: global fn set_palette fn set_palette(theme: ThemeApi, palette: Map) Description Add named colors to the theme palette.","breadcrumbs":"Theme » Theme » Theme","id":"11","title":"Theme"},"12":{"body":"Namespace: global fn set_formatter fn set_formatter(tabbar: TabbarApi, callback: FnPtr) Description Register the function used to format the tab bar.","breadcrumbs":"Tabbar » Tabbar » Tabbar","id":"12","title":"Tabbar"},"13":{"body":"Namespace: global fn break_current_node fn break_current_node(_: ActionApi, destination: \\"tab\\" | \\"floating\\") -> Action Description Break the current node into a new tab or floating window.\\n`destination` accepts `tab` or `floating`.\\nExample: `action.break_current_node(\\"floating\\")`. fn cancel_search fn cancel_search(_: ActionApi) -> Action Description Cancel the active search. fn cancel_selection fn cancel_selection(_: ActionApi) -> Action Description Cancel the current selection. fn chain fn chain(_: ActionApi, actions: Array) -> Action Description Chain multiple actions into one composite action. fn clear_pending_keys fn clear_pending_keys(_: ActionApi) -> Action Description Clear any partially-entered key sequence. fn close_floating fn close_floating(_: ActionApi) -> Action Description Close the currently focused floating window. fn close_floating_id fn close_floating_id(_: ActionApi, floating_id: int) -> Action Description Close a floating window by id. fn close_node fn close_node(_: ActionApi, node_id: int) -> Action Description Close a view by node id. fn close_view fn close_view(_: ActionApi) -> Action Description Close the currently focused view. fn commit_search fn commit_search(_: ActionApi) -> Action Description Finalize the active search, keep the current match and cursor position, and leave search\\nmode with the committed result in place. fn copy_selection fn copy_selection(_: ActionApi) -> Action Description Copy the current selection into the clipboard. fn detach_buffer fn detach_buffer(_: ActionApi) -> Action Description Detach the currently focused buffer. fn detach_buffer_id fn detach_buffer_id(_: ActionApi, buffer_id: int) -> Action Description Detach a buffer by id. fn enter_mode fn enter_mode(_: ActionApi, mode: String) -> Action Description Enter a specific input mode by name. fn enter_search_mode fn enter_search_mode(_: ActionApi) -> Action Description Enter incremental search mode. fn enter_select_block fn enter_select_block(_: ActionApi) -> Action Description Enter block selection mode. fn enter_select_char fn enter_select_char(_: ActionApi) -> Action Description Enter character selection mode. fn enter_select_line fn enter_select_line(_: ActionApi) -> Action Description Enter line selection mode. fn focus_buffer fn focus_buffer(_: ActionApi, buffer_id: int) -> Action Description Focus a specific buffer by id. fn focus_down fn focus_down(_: ActionApi) -> Action Description Focus the view below the current node. fn focus_left fn focus_left(_: ActionApi) -> Action Description Example Focus the view to the left of the current node. action.focus_left() fn focus_right fn focus_right(_: ActionApi) -> Action Description Focus the view to the right of the current node. fn focus_up fn focus_up(_: ActionApi) -> Action Description Focus the view above the current node. fn follow_output fn follow_output(_: ActionApi) -> Action Description Re-enable following live output. fn insert_tab_after fn insert_tab_after(_: ActionApi, tabs_node_id: int, title: String, tree: TreeSpec) -> Action Description Insert a tab after a specific tabs node. fn insert_tab_after_current fn insert_tab_after_current(_: ActionApi, title: String, tree: TreeSpec) -> Action Description Insert a tab after the current tab in the focused tabs node. fn insert_tab_before fn insert_tab_before(_: ActionApi, tabs_node_id: int, title: String, tree: TreeSpec) -> Action Description Insert a tab before a specific tabs node. fn insert_tab_before_current fn insert_tab_before_current(_: ActionApi, title: String, tree: TreeSpec) -> Action Description Insert a tab before the current tab. fn join_buffer_here fn join_buffer_here(_: ActionApi, buffer_id: int, placement: \\"tab-after\\" | \\"tab-before\\" | \\"left\\" | \\"right\\" | \\"up\\" | \\"down\\") -> Action Description Attach a buffer at the current node.\\n`placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`.\\nExample: `action.join_buffer_here(12, \\"tab-after\\")`. fn kill_buffer fn kill_buffer(_: ActionApi) -> Action Description Kill the currently focused buffer. fn kill_buffer_id fn kill_buffer_id(_: ActionApi, buffer_id: int) -> Action Description Kill a buffer by id. fn leave_mode fn leave_mode(_: ActionApi) -> Action Description Leave the active input mode. fn move_buffer_to_floating fn move_buffer_to_floating(_: ActionApi, buffer_id: int, options: Map) -> Action Description Options Move a buffer into a new floating window. x (i16): horizontal offset from the anchor (default: 0) y (i16): vertical offset from the anchor (default: 0) width (FloatingSize): window width, as a percentage (e.g., 50%) or pixel value (default: 50%) height (FloatingSize): window height, as a percentage or pixel value (default: 50%) anchor (FloatingAnchor): anchor point for positioning, e.g., “top_left”, “center” (default: center) title (Option): window title (default: none) focus (bool): whether to focus the window after creation (default: true) close_on_empty (bool): whether to close the window when its buffer empties (default: true) fn move_buffer_to_node fn move_buffer_to_node(_: ActionApi, buffer_id: int, node_id: int) -> Action Description Move a buffer into a specific node. fn move_current_node_after fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action Description Move the current node after a sibling. fn move_current_node_before fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action Description Move the current node before a sibling.\\nUse this when the current node is the one being repositioned.\\nExample: `action.move_current_node_before(42)`. fn move_node_after fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action Description Move a node after a sibling.\\nUse this when you need to move a specific node id instead of the current node.\\nExample: `action.move_node_after(10, 42)`. fn move_node_before fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action Description Move a node before a sibling. fn next_current_tabs fn next_current_tabs(_: ActionApi) -> Action Description Select the next tab in the currently focused tabs node. fn next_tab fn next_tab(_: ActionApi, tabs_node_id: int) -> Action Description Select the next tab in a specific tabs node. fn noop fn noop(_: ActionApi) -> Action Description Build a no-op action. fn notify fn notify(_: ActionApi, level: \\"info\\" | \\"warn\\" | \\"error\\", message: String) -> Action Description Emit a client notification. fn open_buffer_history fn open_buffer_history(_: ActionApi, buffer_id: int, scope: \\"visible\\" | \\"full\\", placement: \\"floating\\" | \\"tab\\") -> Action Description Open the history of a buffer in a new view.\\n`scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`.\\nExample: `action.open_buffer_history(12, \\"visible\\", \\"floating\\")`. fn open_floating fn open_floating(_: ActionApi, tree: TreeSpec, options: Map) -> Action Description Open a floating view around the provided tree. fn prev_current_tabs fn prev_current_tabs(_: ActionApi) -> Action Description Select the previous tab in the currently focused tabs node. fn prev_tab fn prev_tab(_: ActionApi, tabs_node_id: int) -> Action Description Select the previous tab in a specific tabs node. fn replace_current_with fn replace_current_with(_: ActionApi, tree: TreeSpec) -> Action Description Replace the focused node with a new tree. fn replace_node fn replace_node(_: ActionApi, node_id: int, tree: TreeSpec) -> Action Description Replace a specific node by id with a new tree. fn reveal_buffer fn reveal_buffer(_: ActionApi, buffer_id: int) -> Action Description Reveal a specific buffer by id. fn run_named_action fn run_named_action(_: ActionApi, name: String) -> Action Description Run another named action by name. fn scroll_line_down fn scroll_line_down(_: ActionApi) -> Action Description Scroll one line downward in local scrollback. fn scroll_line_up fn scroll_line_up(_: ActionApi) -> Action Description Scroll one line upward in local scrollback. fn scroll_page_down fn scroll_page_down(_: ActionApi) -> Action Description Scroll one page downward in local scrollback. fn scroll_page_up fn scroll_page_up(_: ActionApi) -> Action Description Scroll one page upward in local scrollback. fn scroll_to_bottom fn scroll_to_bottom(_: ActionApi) -> Action Description Scroll to the bottom of local scrollback. fn scroll_to_top fn scroll_to_top(_: ActionApi) -> Action Description Scroll to the top of local scrollback. fn search_next fn search_next(_: ActionApi) -> Action Description Jump to the next search match. fn search_prev fn search_prev(_: ActionApi) -> Action Description Jump to the previous search match. fn select_current_tabs fn select_current_tabs(_: ActionApi, index: int) -> Action Description Select a tab by index in the currently focused tabs node. fn select_move_down fn select_move_down(_: ActionApi) -> Action Description Move the active selection down. fn select_move_left fn select_move_left(_: ActionApi) -> Action Description Move the active selection left. fn select_move_right fn select_move_right(_: ActionApi) -> Action Description Move the active selection right. fn select_move_up fn select_move_up(_: ActionApi) -> Action Description Move the active selection up. fn select_tab fn select_tab(_: ActionApi, tabs_node_id: int, index: int) -> Action Description Select a tab by index in a specific tabs node. fn send_bytes fn send_bytes(_: ActionApi, buffer_id: int, bytes: String) -> Action\\nfn send_bytes(_: ActionApi, buffer_id: int, bytes: Array) -> Action Description Send a string of bytes to a specific buffer. fn send_bytes_current fn send_bytes_current(_: ActionApi, bytes: String) -> Action\\nfn send_bytes_current(_: ActionApi, bytes: Array) -> Action Description Send a string of bytes to the focused buffer. fn send_keys fn send_keys(_: ActionApi, buffer_id: int, notation: String) -> Action Description Send a key notation sequence to a specific buffer. fn send_keys_current fn send_keys_current(_: ActionApi, notation: String) -> Action Description Send a key notation sequence to the focused buffer. fn split_with fn split_with(_: ActionApi, direction: \\"h\\" | \\"horizontal\\" | \\"v\\" | \\"vertical\\", tree: TreeSpec) -> Action Description Split the current node and attach the provided tree as the new sibling. fn swap_current_node fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action Description Swap the current node with a sibling. fn toggle_mode fn toggle_mode(_: ActionApi, mode: String) -> Action Description Toggle a named input mode. fn toggle_zoom_node fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action Description Toggle zoom for the specified node id.\\nThere is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the\\nfocused node and `toggle_zoom_node` when you already know the target id. fn unzoom_current_session fn unzoom_current_session(_: ActionApi) -> Action Description Clear the current session\'s active zoom state.\\nThis removes the current session zoom rather than unwinding a stack of prior zooms. fn yank_selection fn yank_selection(_: ActionApi) -> Action Description Copy the current selection into the clipboard. fn zoom_current_node fn zoom_current_node(_: ActionApi) -> Action Description Zoom the session\'s currently focused node.\\nThere is intentionally no separate `zoom_node(node_id)` helper in this API surface.","breadcrumbs":"Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action » Action","id":"13","title":"Action"},"14":{"body":"Namespace: global fn buffer_attach fn buffer_attach(_: TreeApi, buffer_id: int) -> TreeSpec Description Attach an existing buffer by id. fn buffer_current fn buffer_current(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused buffer. fn buffer_empty fn buffer_empty(_: TreeApi) -> TreeSpec Description Build an empty buffer tree node. fn buffer_spawn fn buffer_spawn(_: TreeApi, command: Array) -> TreeSpec\\nfn buffer_spawn(_: TreeApi, command: Array, options: Map) -> TreeSpec Description Example Spawn a new buffer from a command array. Supported options keys are title ( string), cwd ( string), and env\\n( map). Unknown keys are rejected. tree.buffer_spawn([\\"/bin/zsh\\"], #{ title: \\"shell\\" }) fn current_buffer fn current_buffer(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused buffer. fn current_node fn current_node(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused node. fn split fn split(_: TreeApi, direction: String, children: Array) -> TreeSpec\\nfn split(_: TreeApi, direction: String, children: Array, sizes: Array) -> TreeSpec Description Build a split with an explicit direction string. fn split_h fn split_h(_: TreeApi, children: Array) -> TreeSpec Description Build a horizontal split. fn split_v fn split_v(_: TreeApi, children: Array) -> TreeSpec Description Build a vertical split. fn tab fn tab(_: TreeApi, title: String, tree: TreeSpec) -> TabSpec Description Build a single tab specification. fn tabs fn tabs(_: TreeApi, tabs: Array) -> TreeSpec Description Build a tabs container with the first tab active. fn tabs_with_active fn tabs_with_active(_: TreeApi, tabs: Array, active: int) -> TreeSpec Description Build a tabs container with an explicit active tab.","breadcrumbs":"Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree » Tree","id":"14","title":"Tree"},"15":{"body":"Namespace: global fn current_buffer fn current_buffer(context: Context) -> ? Description Example Return the currently focused buffer, if any. ReturnType: BufferRef | () let buffer = ctx.current_buffer();\\nif buffer != () { print(buffer.title());\\n} fn current_floating fn current_floating(context: Context) -> ? Description Return the currently focused floating window, if any. ReturnType: FloatingRef | () fn current_mode fn current_mode(context: Context) -> String Description Return the active input mode name. fn current_node fn current_node(context: Context) -> ? Description Return the currently focused node, if any. ReturnType: NodeRef | () fn current_session fn current_session(context: Context) -> ? Description Return the current session reference, if any. ReturnType: SessionRef | () fn detached_buffers fn detached_buffers(context: Context) -> Array Description Return detached buffers in the current model snapshot. fn event fn event(context: Context) -> ? Description Return the current event payload, if any. ReturnType: EventInfo | () fn find_buffer fn find_buffer(context: Context, buffer_id: int) -> ? Description Find a buffer by numeric id. Returns `()` when it does not exist. ReturnType: BufferRef | () fn find_floating fn find_floating(context: Context, floating_id: int) -> ? Description Find a floating window by numeric id. Returns `()` when it does not exist. ReturnType: FloatingRef | () fn find_node fn find_node(context: Context, node_id: int) -> ? Description Find a node by numeric id. Returns `()` when it does not exist. ReturnType: NodeRef | () fn sessions fn sessions(context: Context) -> Array Description Return every visible session. fn visible_buffers fn visible_buffers(context: Context) -> Array Description Return visible buffers in the current model snapshot.","breadcrumbs":"Context » Context » Context » Context » Context » Context » Context » Context » Context » Context » Context » Context » Context » Context","id":"15","title":"Context"},"16":{"body":"Namespace: global fn current_buffer fn current_buffer(mux: MuxApi) -> ? Description Return the currently focused buffer, if any. ReturnType: BufferRef | () fn current_floating fn current_floating(mux: MuxApi) -> ? Description Return the currently focused floating window, if any. ReturnType: FloatingRef | () fn current_node fn current_node(mux: MuxApi) -> ? Description Return the currently focused node, if any. ReturnType: NodeRef | () fn current_session fn current_session(mux: MuxApi) -> ? Description Return the current session reference, if any. ReturnType: SessionRef | () fn detached_buffers fn detached_buffers(mux: MuxApi) -> Array Description Return detached buffers in the current model snapshot. fn find_buffer fn find_buffer(mux: MuxApi, buffer_id: int) -> ? Description Find a buffer by numeric id. Returns `()` when it does not exist. ReturnType: BufferRef | () fn find_floating fn find_floating(mux: MuxApi, floating_id: int) -> ? Description Find a floating window by numeric id. Returns `()` when it does not exist. ReturnType: FloatingRef | () fn find_node fn find_node(mux: MuxApi, node_id: int) -> ? Description Find a node by numeric id. Returns `()` when it does not exist. ReturnType: NodeRef | () fn sessions fn sessions(mux: MuxApi) -> Array Description Return every visible session. fn visible_buffers fn visible_buffers(mux: MuxApi) -> Array Description Return visible buffers in the current model snapshot.","breadcrumbs":"Mux » Mux » Mux » Mux » Mux » Mux » Mux » Mux » Mux » Mux » Mux » Mux","id":"16","title":"Mux"},"17":{"body":"Namespace: global fn buffer_id fn buffer_id(event: EventInfo) -> ? Description Return the buffer id attached to an event, or `()`. ReturnType: int | () fn client_id fn client_id(event: EventInfo) -> ? Description Return the client id attached to an event, or `()`. ReturnType: int | () fn floating_id fn floating_id(event: EventInfo) -> ? Description Return the floating id attached to an event, or `()`. ReturnType: int | () fn name fn name(event: EventInfo) -> String Description Return the event name. fn node_id fn node_id(event: EventInfo) -> ? Description Return the node id attached to an event, or `()`. ReturnType: int | () fn previous_session_id fn previous_session_id(event: EventInfo) -> ? Description Return the previous session id attached to an event, or `()`. ReturnType: int | () fn session_id fn session_id(event: EventInfo) -> ? Description Return the session id attached to an event, or `()`. ReturnType: int | ()","breadcrumbs":"EventInfo » EventInfo » EventInfo » EventInfo » EventInfo » EventInfo » EventInfo » EventInfo » EventInfo","id":"17","title":"EventInfo"},"18":{"body":"Namespace: global fn floating fn floating(session: SessionRef) -> Array Description Return floating window ids attached to the session. fn id fn id(session: SessionRef) -> int Description Return the numeric session id. fn name fn name(session: SessionRef) -> String Description Return the session name. fn root_node fn root_node(session: SessionRef) -> int Description Return the root tabs node for the session.","breadcrumbs":"SessionRef » SessionRef » SessionRef » SessionRef » SessionRef » SessionRef","id":"18","title":"SessionRef"},"19":{"body":"Namespace: global fn activity fn activity(buffer: BufferRef) -> String Description Return the current activity state name. fn command fn command(buffer: BufferRef) -> Array Description Return the original command vector. fn cwd fn cwd(buffer: BufferRef) -> ? Description Return the working directory, if any. ReturnType: string | () fn env_hint fn env_hint(buffer: BufferRef, key: String) -> ? Description Look up a single environment hint captured on the buffer. ReturnType: string | () fn exit_code fn exit_code(buffer: BufferRef) -> ? Description Return the process exit code, if any. ReturnType: int | () fn history_text fn history_text(buffer: BufferRef) -> String Description Example Return the full captured history text for the buffer. let buffer = ctx.current_buffer();\\nif buffer != () { let history = buffer.history_text();\\n} fn id fn id(buffer: BufferRef) -> int Description Return the numeric buffer id. fn is_attached fn is_attached(buffer: BufferRef) -> bool Description Return whether the buffer is currently attached to a node. fn is_detached fn is_detached(buffer: BufferRef) -> bool Description Return whether the buffer has been detached. fn is_running fn is_running(buffer: BufferRef) -> bool Description Return whether the buffer process is still running. fn is_visible fn is_visible(buffer: BufferRef) -> bool Description Return whether the buffer is visible in the current presentation. fn node_id fn node_id(buffer: BufferRef) -> ? Description Return the attached node id, if any. ReturnType: int | () fn pid fn pid(buffer: BufferRef) -> ? Description Return the process id, if any. ReturnType: int | () fn process_name fn process_name(buffer: BufferRef) -> ? Description Return the detected process name, if any. ReturnType: string | () fn session_id fn session_id(buffer: BufferRef) -> ? Description Return the attached session id, if any. ReturnType: int | () fn snapshot_text fn snapshot_text(buffer: BufferRef, limit: int) -> String Description Return a text snapshot limited to the requested line count. fn title fn title(buffer: BufferRef) -> String Description Return the buffer title. fn tty_path fn tty_path(buffer: BufferRef) -> ? Description Return the controlling TTY path, if any. ReturnType: string | ()","breadcrumbs":"BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef » BufferRef","id":"19","title":"BufferRef"},"2":{"body":"registration.rhai runtime.rhai","breadcrumbs":"Overview » Definitions","id":"2","title":"Definitions"},"20":{"body":"Namespace: global fn active_tab_index fn active_tab_index(node: NodeRef) -> ? Description Return the active tab index, if any. ReturnType: int | () fn buffer fn buffer(node: NodeRef) -> ? Description Return the attached buffer id, if any. ReturnType: int | () fn children fn children(node: NodeRef) -> Array Description Return child node ids. fn geometry fn geometry(node: NodeRef) -> ? Description Return the geometry map, if any. ReturnType: Map | () fn id fn id(node: NodeRef) -> int Description Return the node id. fn is_floating_root fn is_floating_root(node: NodeRef) -> bool Description Return whether the node is the root of a floating window. fn is_focused fn is_focused(node: NodeRef) -> bool Description Return whether the node is focused. fn is_root fn is_root(node: NodeRef) -> bool Description Return whether the node is the session root. fn is_visible fn is_visible(node: NodeRef) -> bool Description Return whether the node is visible in the current presentation. fn kind fn kind(node: NodeRef) -> String Description Return the node kind such as `buffer_view`, `split`, or `tabs`. fn parent fn parent(node: NodeRef) -> ? Description Return the parent node id, if any. ReturnType: int | () fn session_id fn session_id(node: NodeRef) -> int Description Return the owning session id. fn split_direction fn split_direction(node: NodeRef) -> ? Description Return the split direction, if any. ReturnType: string | () fn split_weights fn split_weights(node: NodeRef) -> ? Description Return split weights, if any. ReturnType: Array | () fn tab_titles fn tab_titles(node: NodeRef) -> Array Description Return tab titles on a tabs node.","breadcrumbs":"NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef » NodeRef","id":"20","title":"NodeRef"},"21":{"body":"Namespace: global fn geometry fn geometry(floating: FloatingRef) -> Map Description Return the floating geometry map. fn id fn id(floating: FloatingRef) -> int Description Return the floating id. fn is_focused fn is_focused(floating: FloatingRef) -> bool Description Return whether the floating is focused. fn is_visible fn is_visible(floating: FloatingRef) -> bool Description Return whether the floating is visible. fn root_node fn root_node(floating: FloatingRef) -> int Description Return the root node id. fn session_id fn session_id(floating: FloatingRef) -> int Description Return the owning session id. fn title fn title(floating: FloatingRef) -> ? Description Return the floating title, if any. ReturnType: string | ()","breadcrumbs":"FloatingRef » FloatingRef » FloatingRef » FloatingRef » FloatingRef » FloatingRef » FloatingRef » FloatingRef » FloatingRef","id":"21","title":"FloatingRef"},"22":{"body":"Namespace: global fn active_index fn active_index(bar: TabBarContext) -> int Description Return the active tab index. fn is_root fn is_root(bar: TabBarContext) -> bool Description Return whether the formatted tabs are the root tabs. fn mode fn mode(bar: TabBarContext) -> String Description Return the formatter mode name. fn node_id fn node_id(bar: TabBarContext) -> int Description Return the tabs node id currently being formatted. fn tabs fn tabs(bar: TabBarContext) -> Array Description Return tab metadata used by the formatter. fn viewport_width fn viewport_width(bar: TabBarContext) -> int Description Return the formatter viewport width in cells.","breadcrumbs":"TabBarContext » TabBarContext » TabBarContext » TabBarContext » TabBarContext » TabBarContext » TabBarContext » TabBarContext","id":"22","title":"TabBarContext"},"23":{"body":"Namespace: global fn buffer_count fn buffer_count(tab: TabInfo) -> int Description Return how many buffers are attached to the tab. fn has_activity fn has_activity(tab: TabInfo) -> bool Description Return whether the tab has activity. fn has_bell fn has_bell(tab: TabInfo) -> bool Description Return whether the tab has a bell marker. fn index fn index(tab: TabInfo) -> int Description Return the zero-based tab index. fn is_active fn is_active(tab: TabInfo) -> bool Description Return whether the tab is active. fn title fn title(tab: TabInfo) -> String Description Return the tab title.","breadcrumbs":"TabInfo » TabInfo » TabInfo » TabInfo » TabInfo » TabInfo » TabInfo » TabInfo","id":"23","title":"TabInfo"},"24":{"body":"Namespace: global fn env fn env(_: SystemApi, name: String) -> ? Description Read an environment variable, if it is set. ReturnType: string | () fn now fn now(_: SystemApi) -> int Description Return the current Unix timestamp in seconds. fn which fn which(_: SystemApi, name: String) -> ? Description Resolve an executable from `PATH`, if it is found. ReturnType: string | ()","breadcrumbs":"System » System » System » System » System","id":"24","title":"System"},"25":{"body":"Namespace: global fn bar fn bar(_: UiApi, left: Array, center: Array, right: Array) -> BarSpec Description Build a full bar specification from left, center, and right segments. fn segment fn segment(_: UiApi, text: String) -> BarSegment\\nfn segment(_: UiApi, text: String, options: Map) -> BarSegment Description Create a [`BarSegment`] from a [`UiApi`] receiver and text using default styling. segment(_: UiApi, text: String) -> BarSegment produces plain text with default\\n[ StyleSpec] values and no click target. See the overloaded segment(_: UiApi, text: String, options: Map) -> BarSegment doc for\\nthe full options: Map styling keys.","breadcrumbs":"UI » UI » UI » UI","id":"25","title":"UI"},"26":{"body":"Namespace: global fn color fn color(theme: ThemeRuntimeApi, name: String) -> ? Description Read a named color from the active runtime palette, if it exists. ReturnType: RgbColor | ()","breadcrumbs":"Runtime Theme » Runtime Theme » Runtime Theme","id":"26","title":"Runtime Theme"},"3":{"body":"example.md","breadcrumbs":"Overview » Example","id":"3","title":"Example"},"4":{"body":"This is a trimmed example based on the repository fixture config. It shows the two main phases together. set_leader(\\"\\"); fn shell_tree(ctx) { tree.buffer_spawn( [\\"/bin/zsh\\"], #{ title: \\"shell\\", cwd: if ctx.current_buffer() == () { () } else { ctx.current_buffer().cwd() } } )\\n} fn split_below(ctx) { action.split_with(\\"horizontal\\", shell_tree(ctx))\\n} fn format_tabs(ctx) { let active = ctx.tabs()[ctx.active_index()]; ui.bar([ ui.segment(\\" \\" + active.title() + \\" \\", #{ fg: theme.color(\\"active_fg\\"), bg: theme.color(\\"active_bg\\") }) ], [], [])\\n} define_action(\\"split-below\\", split_below);\\nbind(\\"normal\\", \\"\\\\\\"\\", \\"split-below\\");\\ntheme.set_palette(#{ active_fg: \\"#303446\\", active_bg: \\"#c6d0f5\\"\\n});\\ntabbar.set_formatter(format_tabs);\\nmouse.set_click_focus(true);","breadcrumbs":"Example » Example","id":"4","title":"Example"},"5":{"body":"Namespace: global fn bind fn bind(mode: String, notation: String, action: Action)\\nfn bind(mode: String, notation: String, action_name: String)\\nfn bind(mode: String, notation: String, actions: Array) Description Example Bind a key notation to an [`Action`], a string action name, or an array of actions. Use the Action overload for inline builders such as action.focus_left(), the string\\noverload for a named action registered with define_action, or an array to chain multiple\\nactions in sequence. bind(\\"normal\\", \\"ws\\", \\"workspace-split\\"); fn define_action fn define_action(name: String, callback: FnPtr) Description Register a function pointer as a named action callable from bindings. fn define_mode fn define_mode(mode_name: String)\\nfn define_mode(mode_name: String, options: Map) Description Define a custom input mode with hooks and fallback options. Supported options are fallback, on_enter, and on_leave. fn on fn on(event_name: String, callback: FnPtr) Description Attach a callback to an emitted event such as `buffer_bell`. fn set_leader fn set_leader(notation: String) Description Example Set the leader sequence used in binding notations. set_leader(\\"\\"); fn unbind fn unbind(mode: String, notation: String) Description Remove a previously bound key sequence.","breadcrumbs":"Registration Globals » Registration Globals » Registration Globals » Registration Globals » Registration Globals » Registration Globals » Registration Globals » Registration Globals","id":"5","title":"Registration Globals"},"6":{"body":"Namespace: global fn break_current_node fn break_current_node(_: ActionApi, destination: \\"tab\\" | \\"floating\\") -> Action Description Break the current node into a new tab or floating window.\\n`destination` accepts `tab` or `floating`.\\nExample: `action.break_current_node(\\"floating\\")`. fn cancel_search fn cancel_search(_: ActionApi) -> Action Description Cancel the active search. fn cancel_selection fn cancel_selection(_: ActionApi) -> Action Description Cancel the current selection. fn chain fn chain(_: ActionApi, actions: Array) -> Action Description Chain multiple actions into one composite action. fn clear_pending_keys fn clear_pending_keys(_: ActionApi) -> Action Description Clear any partially-entered key sequence. fn close_floating fn close_floating(_: ActionApi) -> Action Description Close the currently focused floating window. fn close_floating_id fn close_floating_id(_: ActionApi, floating_id: int) -> Action Description Close a floating window by id. fn close_node fn close_node(_: ActionApi, node_id: int) -> Action Description Close a view by node id. fn close_view fn close_view(_: ActionApi) -> Action Description Close the currently focused view. fn commit_search fn commit_search(_: ActionApi) -> Action Description Finalize the active search, keep the current match and cursor position, and leave search\\nmode with the committed result in place. fn copy_selection fn copy_selection(_: ActionApi) -> Action Description Copy the current selection into the clipboard. fn detach_buffer fn detach_buffer(_: ActionApi) -> Action Description Detach the currently focused buffer. fn detach_buffer_id fn detach_buffer_id(_: ActionApi, buffer_id: int) -> Action Description Detach a buffer by id. fn enter_mode fn enter_mode(_: ActionApi, mode: String) -> Action Description Enter a specific input mode by name. fn enter_search_mode fn enter_search_mode(_: ActionApi) -> Action Description Enter incremental search mode. fn enter_select_block fn enter_select_block(_: ActionApi) -> Action Description Enter block selection mode. fn enter_select_char fn enter_select_char(_: ActionApi) -> Action Description Enter character selection mode. fn enter_select_line fn enter_select_line(_: ActionApi) -> Action Description Enter line selection mode. fn focus_buffer fn focus_buffer(_: ActionApi, buffer_id: int) -> Action Description Focus a specific buffer by id. fn focus_down fn focus_down(_: ActionApi) -> Action Description Focus the view below the current node. fn focus_left fn focus_left(_: ActionApi) -> Action Description Example Focus the view to the left of the current node. action.focus_left() fn focus_right fn focus_right(_: ActionApi) -> Action Description Focus the view to the right of the current node. fn focus_up fn focus_up(_: ActionApi) -> Action Description Focus the view above the current node. fn follow_output fn follow_output(_: ActionApi) -> Action Description Re-enable following live output. fn insert_tab_after fn insert_tab_after(_: ActionApi, tabs_node_id: int, title: String, tree: TreeSpec) -> Action Description Insert a tab after a specific tabs node. fn insert_tab_after_current fn insert_tab_after_current(_: ActionApi, title: String, tree: TreeSpec) -> Action Description Insert a tab after the current tab in the focused tabs node. fn insert_tab_before fn insert_tab_before(_: ActionApi, tabs_node_id: int, title: String, tree: TreeSpec) -> Action Description Insert a tab before a specific tabs node. fn insert_tab_before_current fn insert_tab_before_current(_: ActionApi, title: String, tree: TreeSpec) -> Action Description Insert a tab before the current tab. fn join_buffer_here fn join_buffer_here(_: ActionApi, buffer_id: int, placement: \\"tab-after\\" | \\"tab-before\\" | \\"left\\" | \\"right\\" | \\"up\\" | \\"down\\") -> Action Description Attach a buffer at the current node.\\n`placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`.\\nExample: `action.join_buffer_here(12, \\"tab-after\\")`. fn kill_buffer fn kill_buffer(_: ActionApi) -> Action Description Kill the currently focused buffer. fn kill_buffer_id fn kill_buffer_id(_: ActionApi, buffer_id: int) -> Action Description Kill a buffer by id. fn leave_mode fn leave_mode(_: ActionApi) -> Action Description Leave the active input mode. fn move_buffer_to_floating fn move_buffer_to_floating(_: ActionApi, buffer_id: int, options: Map) -> Action Description Options Move a buffer into a new floating window. x (i16): horizontal offset from the anchor (default: 0) y (i16): vertical offset from the anchor (default: 0) width (FloatingSize): window width, as a percentage (e.g., 50%) or pixel value (default: 50%) height (FloatingSize): window height, as a percentage or pixel value (default: 50%) anchor (FloatingAnchor): anchor point for positioning, e.g., “top_left”, “center” (default: center) title (Option): window title (default: none) focus (bool): whether to focus the window after creation (default: true) close_on_empty (bool): whether to close the window when its buffer empties (default: true) fn move_buffer_to_node fn move_buffer_to_node(_: ActionApi, buffer_id: int, node_id: int) -> Action Description Move a buffer into a specific node. fn move_current_node_after fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action Description Move the current node after a sibling. fn move_current_node_before fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action Description Move the current node before a sibling.\\nUse this when the current node is the one being repositioned.\\nExample: `action.move_current_node_before(42)`. fn move_node_after fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action Description Move a node after a sibling.\\nUse this when you need to move a specific node id instead of the current node.\\nExample: `action.move_node_after(10, 42)`. fn move_node_before fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action Description Move a node before a sibling. fn next_current_tabs fn next_current_tabs(_: ActionApi) -> Action Description Select the next tab in the currently focused tabs node. fn next_tab fn next_tab(_: ActionApi, tabs_node_id: int) -> Action Description Select the next tab in a specific tabs node. fn noop fn noop(_: ActionApi) -> Action Description Build a no-op action. fn notify fn notify(_: ActionApi, level: \\"info\\" | \\"warn\\" | \\"error\\", message: String) -> Action Description Emit a client notification. fn open_buffer_history fn open_buffer_history(_: ActionApi, buffer_id: int, scope: \\"visible\\" | \\"full\\", placement: \\"floating\\" | \\"tab\\") -> Action Description Open the history of a buffer in a new view.\\n`scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`.\\nExample: `action.open_buffer_history(12, \\"visible\\", \\"floating\\")`. fn open_floating fn open_floating(_: ActionApi, tree: TreeSpec, options: Map) -> Action Description Open a floating view around the provided tree. fn prev_current_tabs fn prev_current_tabs(_: ActionApi) -> Action Description Select the previous tab in the currently focused tabs node. fn prev_tab fn prev_tab(_: ActionApi, tabs_node_id: int) -> Action Description Select the previous tab in a specific tabs node. fn replace_current_with fn replace_current_with(_: ActionApi, tree: TreeSpec) -> Action Description Replace the focused node with a new tree. fn replace_node fn replace_node(_: ActionApi, node_id: int, tree: TreeSpec) -> Action Description Replace a specific node by id with a new tree. fn reveal_buffer fn reveal_buffer(_: ActionApi, buffer_id: int) -> Action Description Reveal a specific buffer by id. fn run_named_action fn run_named_action(_: ActionApi, name: String) -> Action Description Run another named action by name. fn scroll_line_down fn scroll_line_down(_: ActionApi) -> Action Description Scroll one line downward in local scrollback. fn scroll_line_up fn scroll_line_up(_: ActionApi) -> Action Description Scroll one line upward in local scrollback. fn scroll_page_down fn scroll_page_down(_: ActionApi) -> Action Description Scroll one page downward in local scrollback. fn scroll_page_up fn scroll_page_up(_: ActionApi) -> Action Description Scroll one page upward in local scrollback. fn scroll_to_bottom fn scroll_to_bottom(_: ActionApi) -> Action Description Scroll to the bottom of local scrollback. fn scroll_to_top fn scroll_to_top(_: ActionApi) -> Action Description Scroll to the top of local scrollback. fn search_next fn search_next(_: ActionApi) -> Action Description Jump to the next search match. fn search_prev fn search_prev(_: ActionApi) -> Action Description Jump to the previous search match. fn select_current_tabs fn select_current_tabs(_: ActionApi, index: int) -> Action Description Select a tab by index in the currently focused tabs node. fn select_move_down fn select_move_down(_: ActionApi) -> Action Description Move the active selection down. fn select_move_left fn select_move_left(_: ActionApi) -> Action Description Move the active selection left. fn select_move_right fn select_move_right(_: ActionApi) -> Action Description Move the active selection right. fn select_move_up fn select_move_up(_: ActionApi) -> Action Description Move the active selection up. fn select_tab fn select_tab(_: ActionApi, tabs_node_id: int, index: int) -> Action Description Select a tab by index in a specific tabs node. fn send_bytes fn send_bytes(_: ActionApi, buffer_id: int, bytes: String) -> Action\\nfn send_bytes(_: ActionApi, buffer_id: int, bytes: Array) -> Action Description Send a string of bytes to a specific buffer. fn send_bytes_current fn send_bytes_current(_: ActionApi, bytes: String) -> Action\\nfn send_bytes_current(_: ActionApi, bytes: Array) -> Action Description Send a string of bytes to the focused buffer. fn send_keys fn send_keys(_: ActionApi, buffer_id: int, notation: String) -> Action Description Send a key notation sequence to a specific buffer. fn send_keys_current fn send_keys_current(_: ActionApi, notation: String) -> Action Description Send a key notation sequence to the focused buffer. fn split_with fn split_with(_: ActionApi, direction: \\"h\\" | \\"horizontal\\" | \\"v\\" | \\"vertical\\", tree: TreeSpec) -> Action Description Split the current node and attach the provided tree as the new sibling. fn swap_current_node fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action Description Swap the current node with a sibling. fn toggle_mode fn toggle_mode(_: ActionApi, mode: String) -> Action Description Toggle a named input mode. fn toggle_zoom_node fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action Description Toggle zoom for the specified node id.\\nThere is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the\\nfocused node and `toggle_zoom_node` when you already know the target id. fn unzoom_current_session fn unzoom_current_session(_: ActionApi) -> Action Description Clear the current session\'s active zoom state.\\nThis removes the current session zoom rather than unwinding a stack of prior zooms. fn yank_selection fn yank_selection(_: ActionApi) -> Action Description Copy the current selection into the clipboard. fn zoom_current_node fn zoom_current_node(_: ActionApi) -> Action Description Zoom the session\'s currently focused node.\\nThere is intentionally no separate `zoom_node(node_id)` helper in this API surface.","breadcrumbs":"Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration) » Action (Registration)","id":"6","title":"Action (Registration)"},"7":{"body":"Namespace: global fn buffer_attach fn buffer_attach(_: TreeApi, buffer_id: int) -> TreeSpec Description Attach an existing buffer by id. fn buffer_current fn buffer_current(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused buffer. fn buffer_empty fn buffer_empty(_: TreeApi) -> TreeSpec Description Build an empty buffer tree node. fn buffer_spawn fn buffer_spawn(_: TreeApi, command: Array) -> TreeSpec\\nfn buffer_spawn(_: TreeApi, command: Array, options: Map) -> TreeSpec Description Example Spawn a new buffer from a command array. Supported options keys are title ( string), cwd ( string), and env\\n( map). Unknown keys are rejected. tree.buffer_spawn([\\"/bin/zsh\\"], #{ title: \\"shell\\" }) fn current_buffer fn current_buffer(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused buffer. fn current_node fn current_node(_: TreeApi) -> TreeSpec Description Build a tree reference to the currently focused node. fn split fn split(_: TreeApi, direction: String, children: Array) -> TreeSpec\\nfn split(_: TreeApi, direction: String, children: Array, sizes: Array) -> TreeSpec Description Build a split with an explicit direction string. fn split_h fn split_h(_: TreeApi, children: Array) -> TreeSpec Description Build a horizontal split. fn split_v fn split_v(_: TreeApi, children: Array) -> TreeSpec Description Build a vertical split. fn tab fn tab(_: TreeApi, title: String, tree: TreeSpec) -> TabSpec Description Build a single tab specification. fn tabs fn tabs(_: TreeApi, tabs: Array) -> TreeSpec Description Build a tabs container with the first tab active. fn tabs_with_active fn tabs_with_active(_: TreeApi, tabs: Array, active: int) -> TreeSpec Description Build a tabs container with an explicit active tab.","breadcrumbs":"Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration) » Tree (Registration)","id":"7","title":"Tree (Registration)"},"8":{"body":"Namespace: global fn env fn env(_: SystemApi, name: String) -> ? Description Read an environment variable, if it is set. ReturnType: string | () fn now fn now(_: SystemApi) -> int Description Return the current Unix timestamp in seconds. fn which fn which(_: SystemApi, name: String) -> ? Description Resolve an executable from `PATH`, if it is found. ReturnType: string | ()","breadcrumbs":"System (Registration) » System (Registration) » System (Registration) » System (Registration) » System (Registration)","id":"8","title":"System (Registration)"},"9":{"body":"Namespace: global fn bar fn bar(_: UiApi, left: Array, center: Array, right: Array) -> BarSpec Description Build a full bar specification from left, center, and right segments. fn segment fn segment(_: UiApi, text: String) -> BarSegment\\nfn segment(_: UiApi, text: String, options: Map) -> BarSegment Description Create a [`BarSegment`] from a [`UiApi`] receiver and text using default styling. segment(_: UiApi, text: String) -> BarSegment produces plain text with default\\n[ StyleSpec] values and no click target. See the overloaded segment(_: UiApi, text: String, options: Map) -> BarSegment doc for\\nthe full options: Map styling keys.","breadcrumbs":"UI (Registration) » UI (Registration) » UI (Registration) » UI (Registration)","id":"9","title":"UI (Registration)"}},"length":27,"save":true},"fields":["title","body","breadcrumbs"],"index":{"body":{"root":{"0":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"3":{"0":{"3":{"4":{"4":{"6":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"4":{"2":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"5":{"0":{"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}},"a":{"b":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"c":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{".":{"b":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"k":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"(":{"\\"":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"j":{"df":0,"docs":{},"o":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"(":{"1":{"2":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"(":{"4":{"2":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"(":{"1":{"0":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}},"o":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"y":{"(":{"1":{"2":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"\\"":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}}},"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":8.774964387392123},"6":{"tf":8.774964387392123}}}}},"df":5,"docs":{"0":{"tf":1.4142135623730951},"1":{"tf":1.4142135623730951},"13":{"tf":9.1104335791443},"5":{"tf":3.1622776601683795},"6":{"tf":9.1104335791443}}}},"v":{"df":11,"docs":{"13":{"tf":2.8284271247461903},"14":{"tf":1.7320508075688772},"15":{"tf":1.0},"19":{"tf":1.4142135623730951},"20":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.4142135623730951},"26":{"tf":1.0},"4":{"tf":1.0},"6":{"tf":2.8284271247461903},"7":{"tf":1.7320508075688772}},"e":{".":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"_":{"b":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"22":{"tf":1.0}}}}},"df":0,"docs":{}}},"t":{"a":{"b":{"_":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"y":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"d":{"d":{"df":1,"docs":{"11":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{},"g":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}}},"df":0,"docs":{}},"l":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"a":{"d":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"n":{"c":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}}},"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"p":{"df":0,"docs":{},"i":{"df":3,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"6":{"tf":1.0}}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}}},"r":{"a":{"df":0,"docs":{},"y":{"df":13,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":3.1622776601683795},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.7320508075688772},"22":{"tf":1.0},"25":{"tf":1.7320508075688772},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772},"7":{"tf":3.1622776601683795},"9":{"tf":1.7320508075688772}}}},"df":0,"docs":{}}},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":10,"docs":{"13":{"tf":1.4142135623730951},"14":{"tf":1.0},"17":{"tf":2.449489742783178},"18":{"tf":1.0},"19":{"tf":1.7320508075688772},"20":{"tf":1.0},"23":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.4142135623730951},"7":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}},"df":5,"docs":{"0":{"tf":1.0},"1":{"tf":1.0},"12":{"tf":1.0},"25":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":2,"docs":{"25":{"tf":2.23606797749979},"9":{"tf":2.23606797749979}}}},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}}}},"s":{"df":0,"docs":{},"e":{"df":2,"docs":{"23":{"tf":1.0},"4":{"tf":1.0}}}}},"df":0,"docs":{},"e":{"df":3,"docs":{"13":{"tf":1.0},"22":{"tf":1.0},"6":{"tf":1.0}},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}},"h":{"a":{"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"23":{"tf":1.0}}},"o":{"df":0,"docs":{},"w":{"df":3,"docs":{"13":{"tf":1.0},"4":{"tf":1.4142135623730951},"6":{"tf":1.0}}}}}},"g":{"df":1,"docs":{"4":{"tf":1.0}}},"i":{"df":0,"docs":{},"n":{"/":{"df":0,"docs":{},"z":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"d":{"(":{"\\"":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.7320508075688772}}},"df":0,"docs":{}}}},"df":2,"docs":{"0":{"tf":1.0},"5":{"tf":2.0}}},"df":0,"docs":{}}},"l":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":8,"docs":{"10":{"tf":2.0},"13":{"tf":1.4142135623730951},"19":{"tf":2.0},"20":{"tf":2.0},"21":{"tf":1.4142135623730951},"22":{"tf":1.0},"23":{"tf":1.7320508075688772},"6":{"tf":1.4142135623730951}}}},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"n":{"d":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"r":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"k":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},".":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"y":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}}}},"_":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"5":{"tf":1.0}}}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":1,"docs":{"23":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"y":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":7,"docs":{"13":{"tf":3.3166247903554},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":3.3166247903554},"7":{"tf":1.0}}},"df":0,"docs":{}},"s":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"df":0,"docs":{}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":1,"docs":{"20":{"tf":1.0}}}}}}},"df":12,"docs":{"1":{"tf":1.0},"10":{"tf":1.4142135623730951},"13":{"tf":3.872983346207417},"14":{"tf":2.23606797749979},"15":{"tf":2.449489742783178},"16":{"tf":2.0},"17":{"tf":1.0},"19":{"tf":3.1622776601683795},"20":{"tf":1.4142135623730951},"23":{"tf":1.0},"6":{"tf":3.872983346207417},"7":{"tf":2.23606797749979}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":4.358898943540674}}}}}}}}},"i":{"df":0,"docs":{},"l":{"d":{"df":6,"docs":{"13":{"tf":1.0},"14":{"tf":3.1622776601683795},"25":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":3.1622776601683795},"9":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"5":{"tf":1.0}}}}},"df":0,"docs":{}}}},"y":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}}},"c":{"6":{"d":{"0":{"df":0,"docs":{},"f":{"5":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"a":{"b":{"df":0,"docs":{},"l":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.7320508075688772}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"n":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.4142135623730951}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"22":{"tf":1.0}}}},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"25":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}}}}},"h":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":3,"docs":{"13":{"tf":1.4142135623730951},"5":{"tf":1.0},"6":{"tf":1.4142135623730951}}}},"r":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"l":{"d":{"df":1,"docs":{"20":{"tf":1.0}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":3,"docs":{"14":{"tf":2.0},"20":{"tf":1.0},"7":{"tf":2.0}}}}}},"df":0,"docs":{}}}},"l":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"_":{"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"y":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"df":0,"docs":{}}}}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{}},"i":{"c":{"df":0,"docs":{},"k":{"df":3,"docs":{"10":{"tf":1.4142135623730951},"25":{"tf":1.0},"9":{"tf":1.0}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":1,"docs":{"17":{"tf":1.0}}},"df":0,"docs":{}}},"df":5,"docs":{"0":{"tf":1.0},"10":{"tf":1.0},"13":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":1.0}}}}},"p":{"b":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"df":0,"docs":{}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}}}}},"o":{"d":{"df":0,"docs":{},"e":{"df":1,"docs":{"19":{"tf":1.0}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":1,"docs":{"26":{"tf":1.0}}}}}}},"df":2,"docs":{"11":{"tf":1.0},"26":{"tf":1.4142135623730951}}}}},"m":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"n":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":3,"docs":{"14":{"tf":1.7320508075688772},"19":{"tf":1.4142135623730951},"7":{"tf":1.7320508075688772}}},"df":0,"docs":{}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":2,"docs":{"0":{"tf":1.4142135623730951},"4":{"tf":1.0}}}}},"t":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":2,"docs":{"1":{"tf":1.4142135623730951},"15":{"tf":3.605551275463989}}}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"y":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"t":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"t":{"df":0,"docs":{},"x":{".":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.0},"19":{"tf":1.0},"4":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{")":{".":{"c":{"df":0,"docs":{},"w":{"d":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"df":0,"docs":{},"s":{"(":{")":{"[":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{".":{"a":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":4,"docs":{"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}}}}},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"15":{"tf":1.0}},"e":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"n":{"df":0,"docs":{},"o":{"d":{"df":4,"docs":{"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}}},"df":11,"docs":{"13":{"tf":5.291502622129181},"14":{"tf":1.7320508075688772},"15":{"tf":2.6457513110645907},"16":{"tf":2.449489742783178},"19":{"tf":1.7320508075688772},"20":{"tf":1.0},"22":{"tf":1.0},"24":{"tf":1.0},"6":{"tf":5.291502622129181},"7":{"tf":1.7320508075688772},"8":{"tf":1.0}}}}}},"s":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}}}}},"w":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.0}}}}}}}},"df":0,"docs":{}},"df":4,"docs":{"14":{"tf":1.0},"19":{"tf":1.0},"4":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}}},"d":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"l":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"f":{"a":{"df":0,"docs":{},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":2.8284271247461903},"25":{"tf":1.4142135623730951},"6":{"tf":2.8284271247461903},"9":{"tf":1.4142135623730951}}}}}},"df":1,"docs":{"0":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.4142135623730951}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"\\"":{"df":0,"docs":{},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}},"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"0":{"tf":1.0},"2":{"tf":1.0}}}}}}},"s":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":22,"docs":{"10":{"tf":2.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":8.660254037844387},"14":{"tf":3.4641016151377544},"15":{"tf":3.4641016151377544},"16":{"tf":3.1622776601683795},"17":{"tf":2.6457513110645907},"18":{"tf":2.0},"19":{"tf":4.242640687119285},"20":{"tf":3.872983346207417},"21":{"tf":2.6457513110645907},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"24":{"tf":1.7320508075688772},"25":{"tf":1.4142135623730951},"26":{"tf":1.0},"5":{"tf":2.449489742783178},"6":{"tf":8.660254037844387},"7":{"tf":3.4641016151377544},"8":{"tf":1.7320508075688772},"9":{"tf":1.4142135623730951}}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"t":{"a":{"c":{"df":0,"docs":{},"h":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":5,"docs":{"13":{"tf":1.4142135623730951},"15":{"tf":1.0},"16":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.4142135623730951}},"e":{"d":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}},"df":0,"docs":{}}}},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":5,"docs":{"13":{"tf":1.0},"14":{"tf":1.7320508075688772},"20":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.7320508075688772}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"o":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{".":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{},"m":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":4,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0}}}}}},"n":{"a":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"k":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"c":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}},"v":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":4,"docs":{"14":{"tf":1.0},"24":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0}},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":3,"docs":{"19":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}}}}}},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{}},"df":6,"docs":{"0":{"tf":1.0},"1":{"tf":1.0},"10":{"tf":1.0},"15":{"tf":1.4142135623730951},"17":{"tf":2.6457513110645907},"5":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":2,"docs":{"15":{"tf":1.0},"17":{"tf":2.8284271247461903}}}}}}}}}},"x":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":9,"docs":{"13":{"tf":2.449489742783178},"14":{"tf":1.0},"15":{"tf":1.0},"19":{"tf":1.0},"3":{"tf":1.0},"4":{"tf":1.4142135623730951},"5":{"tf":1.4142135623730951},"6":{"tf":2.449489742783178},"7":{"tf":1.0}},"e":{".":{"df":0,"docs":{},"m":{"d":{"df":1,"docs":{"3":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":3,"docs":{"0":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":5,"docs":{"14":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"26":{"tf":1.0},"7":{"tf":1.0}}}},"t":{"_":{"c":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}}}},"f":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":1,"docs":{"0":{"tf":1.4142135623730951}}}},"n":{"a":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"d":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":2,"docs":{"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772}}},"df":0,"docs":{}},"r":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"x":{"df":0,"docs":{},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":9,"docs":{"1":{"tf":1.0},"13":{"tf":3.1622776601683795},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"17":{"tf":1.0},"18":{"tf":1.4142135623730951},"20":{"tf":1.0},"21":{"tf":2.23606797749979},"6":{"tf":3.1622776601683795}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":5,"docs":{"13":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"a":{"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"21":{"tf":2.8284271247461903}}}}},"s":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}}},"df":0,"docs":{}}},"n":{"df":23,"docs":{"10":{"tf":2.8284271247461903},"11":{"tf":1.4142135623730951},"12":{"tf":1.4142135623730951},"13":{"tf":12.328828005937952},"14":{"tf":5.0990195135927845},"15":{"tf":4.898979485566356},"16":{"tf":4.47213595499958},"17":{"tf":3.7416573867739413},"18":{"tf":2.8284271247461903},"19":{"tf":6.0},"20":{"tf":5.477225575051661},"21":{"tf":3.7416573867739413},"22":{"tf":3.4641016151377544},"23":{"tf":3.4641016151377544},"24":{"tf":2.449489742783178},"25":{"tf":2.23606797749979},"26":{"tf":1.4142135623730951},"4":{"tf":1.7320508075688772},"5":{"tf":3.872983346207417},"6":{"tf":12.328828005937952},"7":{"tf":5.0990195135927845},"8":{"tf":2.449489742783178},"9":{"tf":2.23606797749979}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.4142135623730951}}}}}},"o":{"c":{"df":0,"docs":{},"u":{"df":3,"docs":{"10":{"tf":1.0},"13":{"tf":2.6457513110645907},"6":{"tf":2.6457513110645907}},"s":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":9,"docs":{"10":{"tf":1.4142135623730951},"13":{"tf":3.605551275463989},"14":{"tf":1.7320508075688772},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"20":{"tf":1.0},"21":{"tf":1.0},"6":{"tf":3.605551275463989},"7":{"tf":1.7320508075688772}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"_":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":0,"docs":{},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"12":{"tf":1.0},"22":{"tf":1.4142135623730951}},"t":{"df":2,"docs":{"0":{"tf":1.0},"22":{"tf":1.7320508075688772}}}}},"df":0,"docs":{}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":1,"docs":{"10":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}}}},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":5,"docs":{"13":{"tf":1.4142135623730951},"19":{"tf":1.0},"25":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}},"n":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}}},"o":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":2,"docs":{"20":{"tf":1.4142135623730951},"21":{"tf":1.4142135623730951}}},"y":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"l":{"df":0,"docs":{},"o":{"b":{"a":{"df":0,"docs":{},"l":{"df":23,"docs":{"1":{"tf":1.0},"10":{"tf":1.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":1.0},"25":{"tf":1.0},"26":{"tf":1.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"h":{"a":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}}}},"df":0,"docs":{}},"s":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"23":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"y":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"23":{"tf":1.0}},"l":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"l":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":3,"docs":{"13":{"tf":1.0},"19":{"tf":1.4142135623730951},"6":{"tf":1.0}}},"y":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"k":{"df":1,"docs":{"5":{"tf":1.0}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"14":{"tf":1.0},"6":{"tf":1.4142135623730951},"7":{"tf":1.0}}}}}}}}}},"i":{"1":{"6":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":1,"docs":{"21":{"tf":1.0}}}},"df":0,"docs":{}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":12,"docs":{"13":{"tf":3.1622776601683795},"14":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":2.449489742783178},"18":{"tf":1.7320508075688772},"19":{"tf":2.23606797749979},"20":{"tf":2.449489742783178},"21":{"tf":2.0},"22":{"tf":1.0},"6":{"tf":3.1622776601683795},"7":{"tf":1.0}}},"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":5,"docs":{"13":{"tf":2.0},"20":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.4142135623730951},"6":{"tf":2.0}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":3,"docs":{"1":{"tf":1.4142135623730951},"13":{"tf":1.0},"6":{"tf":1.0}}}},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"5":{"tf":1.0}}}}},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.7320508075688772},"15":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.7320508075688772}}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"_":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}},"t":{"df":0,"docs":{},"e":{"a":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"t":{"df":15,"docs":{"13":{"tf":5.477225575051661},"14":{"tf":1.4142135623730951},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":2.449489742783178},"18":{"tf":1.4142135623730951},"19":{"tf":2.449489742783178},"20":{"tf":2.23606797749979},"21":{"tf":1.7320508075688772},"22":{"tf":1.7320508075688772},"23":{"tf":1.4142135623730951},"24":{"tf":1.0},"6":{"tf":5.477225575051661},"7":{"tf":1.4142135623730951},"8":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}}}}},"s":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"23":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"_":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":2,"docs":{"20":{"tf":1.0},"21":{"tf":1.0}},"e":{"d":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"df":0,"docs":{}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":2,"docs":{"20":{"tf":1.0},"22":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"n":{"df":1,"docs":{"19":{"tf":1.0}},"n":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":3,"docs":{"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0}},"i":{"b":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"j":{"df":0,"docs":{},"o":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":0,"docs":{},"p":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"y":{"df":8,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":1.4142135623730951},"19":{"tf":1.0},"25":{"tf":1.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.7320508075688772},"7":{"tf":1.4142135623730951},"9":{"tf":1.0}}}},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"n":{"d":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"l":{"df":0,"docs":{},"e":{"a":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{">":{"df":0,"docs":{},"w":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}}}},"df":0,"docs":{},"v":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":2.0},"25":{"tf":1.4142135623730951},"6":{"tf":2.0},"9":{"tf":1.4142135623730951}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":3,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.4142135623730951}}}}},"n":{"df":0,"docs":{},"e":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"19":{"tf":1.0},"6":{"tf":1.7320508075688772}}}},"v":{"df":0,"docs":{},"e":{"df":3,"docs":{"0":{"tf":1.4142135623730951},"13":{"tf":1.0},"6":{"tf":1.0}}}}},"o":{"c":{"a":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}},"df":0,"docs":{}},"df":0,"docs":{},"o":{"df":0,"docs":{},"k":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"m":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"4":{"tf":1.0}}}},"n":{"df":0,"docs":{},"i":{"df":1,"docs":{"23":{"tf":1.0}}}},"p":{"<":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}},"df":10,"docs":{"11":{"tf":1.0},"13":{"tf":1.4142135623730951},"14":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.4142135623730951},"25":{"tf":1.7320508075688772},"5":{"tf":1.0},"6":{"tf":1.4142135623730951},"7":{"tf":1.0},"9":{"tf":1.7320508075688772}}},"r":{"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"23":{"tf":1.0}}}}}},"t":{"c":{"df":0,"docs":{},"h":{"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}}},"df":0,"docs":{}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"a":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"t":{"a":{"d":{"a":{"df":0,"docs":{},"t":{"a":{"df":1,"docs":{"22":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"o":{"d":{"df":0,"docs":{},"e":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":6,"docs":{"0":{"tf":1.0},"13":{"tf":3.1622776601683795},"15":{"tf":1.0},"22":{"tf":1.4142135623730951},"5":{"tf":1.0},"6":{"tf":3.1622776601683795}},"l":{"df":2,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":2,"docs":{"1":{"tf":1.0},"10":{"tf":1.4142135623730951}},"e":{".":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"u":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"10":{"tf":2.0}}}}},"df":0,"docs":{}}}},"v":{"df":0,"docs":{},"e":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":3.3166247903554},"6":{"tf":3.3166247903554}}}}},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}}}}},"x":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"16":{"tf":3.1622776601683795}}}}},"df":2,"docs":{"1":{"tf":1.0},"16":{"tf":1.0}}}}},"n":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":13,"docs":{"0":{"tf":1.4142135623730951},"11":{"tf":1.0},"13":{"tf":2.23606797749979},"15":{"tf":1.0},"17":{"tf":1.4142135623730951},"18":{"tf":1.4142135623730951},"19":{"tf":1.4142135623730951},"22":{"tf":1.0},"24":{"tf":1.4142135623730951},"26":{"tf":1.4142135623730951},"5":{"tf":1.7320508075688772},"6":{"tf":2.23606797749979},"8":{"tf":1.4142135623730951}},"s":{"df":0,"docs":{},"p":{"a":{"c":{"df":22,"docs":{"10":{"tf":1.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":1.0},"25":{"tf":1.0},"26":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"w":{"df":4,"docs":{"13":{"tf":2.449489742783178},"14":{"tf":1.0},"6":{"tf":2.449489742783178},"7":{"tf":1.0}}},"x":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}}}},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":7,"docs":{"13":{"tf":2.449489742783178},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"19":{"tf":1.0},"22":{"tf":1.0},"6":{"tf":2.449489742783178}}},"df":0,"docs":{}}},"df":13,"docs":{"1":{"tf":1.0},"13":{"tf":5.5677643628300215},"14":{"tf":1.4142135623730951},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.4142135623730951},"20":{"tf":3.0},"21":{"tf":1.0},"22":{"tf":1.0},"6":{"tf":5.5677643628300215},"7":{"tf":1.4142135623730951}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"20":{"tf":4.0}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"o":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"t":{"a":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":2.0},"5":{"tf":2.449489742783178},"6":{"tf":2.0}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"y":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"w":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":4,"docs":{"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"18":{"tf":1.0},"19":{"tf":1.0}}}}}}},"o":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}},"n":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"_":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.0}}}}},"l":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"v":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}},"p":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"y":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"<":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":7,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":1.4142135623730951},"25":{"tf":1.7320508075688772},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772},"7":{"tf":1.4142135623730951},"9":{"tf":1.7320508075688772}}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"t":{"df":0,"docs":{},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"d":{"df":3,"docs":{"25":{"tf":1.0},"5":{"tf":1.4142135623730951},"9":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"20":{"tf":1.0},"21":{"tf":1.0}}}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":3,"docs":{"1":{"tf":1.0},"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":2,"docs":{"11":{"tf":1.4142135623730951},"26":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.4142135623730951}}}}},"t":{"df":0,"docs":{},"i":{"a":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"t":{"df":0,"docs":{},"h":{"df":3,"docs":{"19":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}},"y":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"d":{"df":1,"docs":{"15":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"a":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{}}}}},"df":0,"docs":{}}},"h":{"a":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":2,"docs":{"0":{"tf":1.0},"4":{"tf":1.0}}}}},"df":0,"docs":{}},"i":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}},"df":0,"docs":{},"x":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"l":{"a":{"c":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}},"df":0,"docs":{}},"o":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"5":{"tf":1.0}}}}}}},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"19":{"tf":1.0},"20":{"tf":1.0}}}}}},"v":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"17":{"tf":1.0},"6":{"tf":1.7320508075688772}},"s":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":1,"docs":{"17":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}}},"df":1,"docs":{"5":{"tf":1.0}}}}}}}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{".":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"15":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"o":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":1,"docs":{"19":{"tf":2.0}}}}}},"d":{"df":0,"docs":{},"u":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}}}}},"r":{"df":0,"docs":{},"e":{"a":{"d":{"df":3,"docs":{"24":{"tf":1.0},"26":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"f":{"df":1,"docs":{"1":{"tf":2.0}},"e":{"df":0,"docs":{},"r":{"df":5,"docs":{"0":{"tf":1.0},"14":{"tf":1.7320508075688772},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.7320508075688772}}}}},"g":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.4142135623730951}},"r":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{".":{"df":0,"docs":{},"r":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"2":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":7,"docs":{"0":{"tf":1.0},"1":{"tf":2.23606797749979},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}}}}}},"j":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}},"df":0,"docs":{}}},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}}},"p":{"df":0,"docs":{},"l":{"a":{"c":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}}}},"q":{"df":0,"docs":{},"u":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"s":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"v":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"n":{"df":11,"docs":{"15":{"tf":3.4641016151377544},"16":{"tf":3.1622776601683795},"17":{"tf":2.6457513110645907},"18":{"tf":2.0},"19":{"tf":4.123105625617661},"20":{"tf":3.872983346207417},"21":{"tf":2.6457513110645907},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"24":{"tf":1.0},"8":{"tf":1.0}},"t":{"df":0,"docs":{},"y":{"df":0,"docs":{},"p":{"df":9,"docs":{"15":{"tf":2.8284271247461903},"16":{"tf":2.6457513110645907},"17":{"tf":2.449489742783178},"19":{"tf":2.8284271247461903},"20":{"tf":2.449489742783178},"21":{"tf":1.0},"24":{"tf":1.4142135623730951},"26":{"tf":1.0},"8":{"tf":1.4142135623730951}}}}}}}}},"v":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"l":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"g":{"b":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"26":{"tf":1.0}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":2.0},"25":{"tf":1.4142135623730951},"6":{"tf":2.0},"9":{"tf":1.4142135623730951}}}}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"18":{"tf":1.0},"21":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":4,"docs":{"18":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0},"22":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"d":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":4,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.0}},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"m":{"df":3,"docs":{"0":{"tf":1.0},"1":{"tf":1.4142135623730951},"26":{"tf":1.4142135623730951}},"e":{".":{"df":0,"docs":{},"r":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"2":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}},"s":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"t":{"df":0,"docs":{},"o":{"_":{"b":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":3,"docs":{"10":{"tf":1.0},"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}}}},"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"p":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}},"df":0,"docs":{}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{},"e":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"g":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"25":{"tf":2.0},"9":{"tf":2.0}}},"df":0,"docs":{}},"df":2,"docs":{"25":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}}}}},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":4.0},"6":{"tf":4.0}}}},"df":0,"docs":{}}},"n":{"d":{"_":{"b":{"df":0,"docs":{},"y":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"y":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}},"df":0,"docs":{}},"p":{"a":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"q":{"df":0,"docs":{},"u":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"c":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}}}}},"s":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"\'":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":4,"docs":{"17":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0}}},"df":0,"docs":{}}},"df":10,"docs":{"1":{"tf":1.0},"13":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":1.4142135623730951},"18":{"tf":2.0},"19":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0},"6":{"tf":1.0}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.0},"16":{"tf":1.0},"18":{"tf":2.23606797749979}}}}},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"t":{"_":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":1,"docs":{"10":{"tf":1.0}},"s":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"w":{"a":{"df":0,"docs":{},"r":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":1,"docs":{"10":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"12":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"12":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"l":{"df":0,"docs":{},"e":{"a":{"d":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"\\"":{"<":{"c":{"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"p":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"11":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":1,"docs":{"11":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"w":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"w":{"a":{"df":0,"docs":{},"r":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":1,"docs":{"10":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"s":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":1,"docs":{"10":{"tf":1.0}},"l":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":4,"docs":{"0":{"tf":1.0},"24":{"tf":1.0},"5":{"tf":1.0},"8":{"tf":1.0}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":3,"docs":{"14":{"tf":1.0},"4":{"tf":1.0},"7":{"tf":1.0}}}}},"o":{"df":0,"docs":{},"w":{"df":1,"docs":{"4":{"tf":1.0}}}}},"i":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"d":{"df":0,"docs":{},"e":{"df":1,"docs":{"10":{"tf":1.0}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"df":0,"docs":{},"l":{"df":3,"docs":{"14":{"tf":1.0},"19":{"tf":1.0},"7":{"tf":1.0}}}}},"z":{"df":0,"docs":{},"e":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"n":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":1.0}}}}}}}},"df":0,"docs":{}},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"i":{"df":0,"docs":{},"f":{"df":6,"docs":{"13":{"tf":3.605551275463989},"14":{"tf":1.0},"25":{"tf":1.0},"6":{"tf":3.605551275463989},"7":{"tf":1.0},"9":{"tf":1.0}},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"_":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}},"df":1,"docs":{"4":{"tf":1.0}}}}}}},"d":{"df":0,"docs":{},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"v":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"w":{"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}},"s":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":7,"docs":{"13":{"tf":1.0},"14":{"tf":2.0},"20":{"tf":1.7320508075688772},"4":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":2.0}}}}}},"t":{"a":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":4,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"19":{"tf":1.0}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"df":18,"docs":{"13":{"tf":3.7416573867739413},"14":{"tf":2.6457513110645907},"15":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":3.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":2.0},"25":{"tf":2.0},"26":{"tf":1.0},"5":{"tf":4.0},"6":{"tf":3.7416573867739413},"7":{"tf":2.6457513110645907},"8":{"tf":2.0},"9":{"tf":2.0}}}}}},"y":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":2,"docs":{"25":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}}}}}}},"u":{"c":{"df":0,"docs":{},"h":{"df":2,"docs":{"20":{"tf":1.0},"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{},"p":{"df":0,"docs":{},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":3,"docs":{"14":{"tf":1.0},"5":{"tf":1.0},"7":{"tf":1.0}}}}}}},"r":{"df":0,"docs":{},"f":{"a":{"c":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"w":{"a":{"df":0,"docs":{},"p":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"y":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"24":{"tf":1.7320508075688772},"8":{"tf":1.7320508075688772}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"24":{"tf":1.0},"8":{"tf":1.0}}}}}}}},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"20":{"tf":1.0}},"e":{"df":0,"docs":{},"s":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"b":{"a":{"df":0,"docs":{},"r":{".":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"12":{"tf":1.0}}}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"22":{"tf":2.6457513110645907}}}}}}}}},"df":2,"docs":{"1":{"tf":1.0},"12":{"tf":1.0}}}},"df":0,"docs":{}},"df":11,"docs":{"0":{"tf":1.0},"1":{"tf":1.4142135623730951},"12":{"tf":1.0},"13":{"tf":5.5677643628300215},"14":{"tf":3.0},"18":{"tf":1.0},"20":{"tf":2.0},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"6":{"tf":5.5677643628300215},"7":{"tf":3.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":1,"docs":{"23":{"tf":2.6457513110645907}}}}}},"s":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.0},"25":{"tf":1.0},"6":{"tf":1.0},"9":{"tf":1.0}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":3,"docs":{"19":{"tf":1.4142135623730951},"25":{"tf":2.449489742783178},"9":{"tf":2.449489742783178}}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{".":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"(":{"\\"":{"a":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"b":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"11":{"tf":1.0}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"11":{"tf":1.4142135623730951},"26":{"tf":1.0}},"r":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"26":{"tf":1.0}}}}},"df":0,"docs":{}}}}}}}}}}}},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":1,"docs":{"0":{"tf":1.0}},"s":{"df":0,"docs":{},"t":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"t":{"df":0,"docs":{},"l":{"df":9,"docs":{"13":{"tf":2.449489742783178},"14":{"tf":1.7320508075688772},"19":{"tf":1.4142135623730951},"20":{"tf":1.0},"21":{"tf":1.4142135623730951},"23":{"tf":1.4142135623730951},"4":{"tf":1.0},"6":{"tf":2.449489742783178},"7":{"tf":1.7320508075688772}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"o":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":1,"docs":{"4":{"tf":1.0}}}}},"g":{"df":0,"docs":{},"l":{"df":3,"docs":{"10":{"tf":2.0},"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"p":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":3,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"6":{"tf":1.0}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{".":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"[":{"\\"":{"/":{"b":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"/":{"df":0,"docs":{},"z":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"14":{"tf":3.7416573867739413},"7":{"tf":3.7416573867739413}}}}},"df":5,"docs":{"1":{"tf":1.4142135623730951},"13":{"tf":3.4641016151377544},"14":{"tf":2.449489742783178},"6":{"tf":3.4641016151377544},"7":{"tf":2.449489742783178}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":4,"docs":{"13":{"tf":2.8284271247461903},"14":{"tf":3.7416573867739413},"6":{"tf":2.8284271247461903},"7":{"tf":3.7416573867739413}}},"df":0,"docs":{}}}}}},"i":{"df":0,"docs":{},"m":{"df":1,"docs":{"4":{"tf":1.0}}}},"u":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"t":{"df":0,"docs":{},"i":{"df":1,"docs":{"19":{"tf":1.0}}},"y":{"_":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"w":{"df":0,"docs":{},"o":{"df":2,"docs":{"0":{"tf":1.0},"4":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"i":{".":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"25":{"tf":2.449489742783178},"9":{"tf":2.449489742783178}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"25":{"tf":1.0},"9":{"tf":1.0}}},"n":{"b":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"x":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"k":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}}},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}}},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"p":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"19":{"tf":1.0},"6":{"tf":1.7320508075688772}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":8,"docs":{"0":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.7320508075688772},"22":{"tf":1.0},"25":{"tf":1.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.7320508075688772},"9":{"tf":1.0}}}},"v":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"u":{"df":5,"docs":{"10":{"tf":2.0},"13":{"tf":1.4142135623730951},"25":{"tf":1.0},"6":{"tf":1.4142135623730951},"9":{"tf":1.0}}}},"r":{"df":0,"docs":{},"i":{"a":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"c":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"14":{"tf":1.0},"6":{"tf":1.4142135623730951},"7":{"tf":1.0}}},"df":0,"docs":{}}}}},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":2,"docs":{"13":{"tf":2.8284271247461903},"6":{"tf":2.8284271247461903}},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"d":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"22":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":1,"docs":{"22":{"tf":1.0}}}}}}}},"s":{"df":0,"docs":{},"i":{"b":{"df":0,"docs":{},"l":{"df":7,"docs":{"13":{"tf":1.7320508075688772},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0},"6":{"tf":1.7320508075688772}},"e":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"u":{"a":{"df":0,"docs":{},"l":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}}}}},"w":{"a":{"df":0,"docs":{},"r":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}}}}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"10":{"tf":1.4142135623730951}}}},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":7,"docs":{"13":{"tf":1.4142135623730951},"19":{"tf":2.0},"20":{"tf":2.0},"21":{"tf":1.4142135623730951},"22":{"tf":1.0},"23":{"tf":1.7320508075688772},"6":{"tf":1.4142135623730951}}}}}}},"i":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"i":{"d":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":3,"docs":{"13":{"tf":1.4142135623730951},"22":{"tf":1.0},"6":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":6,"docs":{"13":{"tf":3.0},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"18":{"tf":1.0},"20":{"tf":1.0},"6":{"tf":3.0}}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"k":{"df":1,"docs":{"19":{"tf":1.0}},"s":{"df":0,"docs":{},"p":{"a":{"c":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"x":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"y":{"a":{"df":0,"docs":{},"n":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"z":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":1,"docs":{"23":{"tf":1.0}}}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}}}}}}},"breadcrumbs":{"root":{"0":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"3":{"0":{"3":{"4":{"4":{"6":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"4":{"2":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"5":{"0":{"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}},"a":{"b":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"c":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{".":{"b":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"k":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"(":{"\\"":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"j":{"df":0,"docs":{},"o":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"(":{"1":{"2":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"(":{"4":{"2":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"(":{"1":{"0":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}},"o":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"y":{"(":{"1":{"2":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"\\"":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}}},"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":8.774964387392123},"6":{"tf":8.774964387392123}}}}},"df":5,"docs":{"0":{"tf":1.4142135623730951},"1":{"tf":1.4142135623730951},"13":{"tf":12.649110640673518},"5":{"tf":3.1622776601683795},"6":{"tf":12.649110640673518}}}},"v":{"df":11,"docs":{"13":{"tf":2.8284271247461903},"14":{"tf":1.7320508075688772},"15":{"tf":1.0},"19":{"tf":1.4142135623730951},"20":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.4142135623730951},"26":{"tf":1.0},"4":{"tf":1.0},"6":{"tf":2.8284271247461903},"7":{"tf":1.7320508075688772}},"e":{".":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"_":{"b":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"22":{"tf":1.0}}}}},"df":0,"docs":{}}},"t":{"a":{"b":{"_":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"y":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"d":{"d":{"df":1,"docs":{"11":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{},"g":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}}},"df":0,"docs":{}},"l":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"a":{"d":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"n":{"c":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}}},"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"p":{"df":0,"docs":{},"i":{"df":3,"docs":{"0":{"tf":1.4142135623730951},"13":{"tf":1.0},"6":{"tf":1.0}}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}}},"r":{"a":{"df":0,"docs":{},"y":{"df":13,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":3.1622776601683795},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.7320508075688772},"22":{"tf":1.0},"25":{"tf":1.7320508075688772},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772},"7":{"tf":3.1622776601683795},"9":{"tf":1.7320508075688772}}}},"df":0,"docs":{}}},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":10,"docs":{"13":{"tf":1.4142135623730951},"14":{"tf":1.0},"17":{"tf":2.449489742783178},"18":{"tf":1.0},"19":{"tf":1.7320508075688772},"20":{"tf":1.0},"23":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.4142135623730951},"7":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}},"df":5,"docs":{"0":{"tf":1.0},"1":{"tf":1.0},"12":{"tf":1.0},"25":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":2,"docs":{"25":{"tf":2.23606797749979},"9":{"tf":2.23606797749979}}}},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}}}},"s":{"df":0,"docs":{},"e":{"df":2,"docs":{"23":{"tf":1.0},"4":{"tf":1.0}}}}},"df":0,"docs":{},"e":{"df":3,"docs":{"13":{"tf":1.0},"22":{"tf":1.0},"6":{"tf":1.0}},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}},"h":{"a":{"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"23":{"tf":1.0}}},"o":{"df":0,"docs":{},"w":{"df":3,"docs":{"13":{"tf":1.0},"4":{"tf":1.4142135623730951},"6":{"tf":1.0}}}}}},"g":{"df":1,"docs":{"4":{"tf":1.0}}},"i":{"df":0,"docs":{},"n":{"/":{"df":0,"docs":{},"z":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"d":{"(":{"\\"":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.7320508075688772}}},"df":0,"docs":{}}}},"df":2,"docs":{"0":{"tf":1.0},"5":{"tf":2.0}}},"df":0,"docs":{}}},"l":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":8,"docs":{"10":{"tf":2.0},"13":{"tf":1.4142135623730951},"19":{"tf":2.0},"20":{"tf":2.0},"21":{"tf":1.4142135623730951},"22":{"tf":1.0},"23":{"tf":1.7320508075688772},"6":{"tf":1.4142135623730951}}}},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"n":{"d":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"r":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"k":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},".":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"y":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}}}},"_":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"5":{"tf":1.0}}}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":1,"docs":{"23":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"y":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":7,"docs":{"13":{"tf":3.3166247903554},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":3.3166247903554},"7":{"tf":1.0}}},"df":0,"docs":{}},"s":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"df":0,"docs":{}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":1,"docs":{"20":{"tf":1.0}}}}}}},"df":12,"docs":{"1":{"tf":1.0},"10":{"tf":1.4142135623730951},"13":{"tf":3.872983346207417},"14":{"tf":2.23606797749979},"15":{"tf":2.449489742783178},"16":{"tf":2.0},"17":{"tf":1.0},"19":{"tf":3.1622776601683795},"20":{"tf":1.4142135623730951},"23":{"tf":1.0},"6":{"tf":3.872983346207417},"7":{"tf":2.23606797749979}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":6.244997998398398}}}}}}}}},"i":{"df":0,"docs":{},"l":{"d":{"df":6,"docs":{"13":{"tf":1.0},"14":{"tf":3.1622776601683795},"25":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":3.1622776601683795},"9":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"5":{"tf":1.0}}}}},"df":0,"docs":{}}}},"y":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}}},"c":{"6":{"d":{"0":{"df":0,"docs":{},"f":{"5":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"a":{"b":{"df":0,"docs":{},"l":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.7320508075688772}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"n":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.4142135623730951}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"22":{"tf":1.0}}}},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"25":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}}}}},"h":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":3,"docs":{"13":{"tf":1.4142135623730951},"5":{"tf":1.0},"6":{"tf":1.4142135623730951}}}},"r":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"l":{"d":{"df":1,"docs":{"20":{"tf":1.0}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":3,"docs":{"14":{"tf":2.0},"20":{"tf":1.0},"7":{"tf":2.0}}}}}},"df":0,"docs":{}}}},"l":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"_":{"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"y":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"df":0,"docs":{}}}}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{}},"i":{"c":{"df":0,"docs":{},"k":{"df":3,"docs":{"10":{"tf":1.4142135623730951},"25":{"tf":1.0},"9":{"tf":1.0}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":1,"docs":{"17":{"tf":1.0}}},"df":0,"docs":{}}},"df":5,"docs":{"0":{"tf":1.0},"10":{"tf":1.0},"13":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":1.0}}}}},"p":{"b":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"df":0,"docs":{}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}}}}},"o":{"d":{"df":0,"docs":{},"e":{"df":1,"docs":{"19":{"tf":1.0}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":1,"docs":{"26":{"tf":1.0}}}}}}},"df":2,"docs":{"11":{"tf":1.0},"26":{"tf":1.4142135623730951}}}}},"m":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"n":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":3,"docs":{"14":{"tf":1.7320508075688772},"19":{"tf":1.4142135623730951},"7":{"tf":1.7320508075688772}}},"df":0,"docs":{}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":2,"docs":{"0":{"tf":1.7320508075688772},"4":{"tf":1.0}}}}},"t":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":2,"docs":{"1":{"tf":1.4142135623730951},"15":{"tf":5.196152422706632}}}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"y":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"t":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"t":{"df":0,"docs":{},"x":{".":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.0},"19":{"tf":1.0},"4":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{")":{".":{"c":{"df":0,"docs":{},"w":{"d":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"df":0,"docs":{},"s":{"(":{")":{"[":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{".":{"a":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":4,"docs":{"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}}}}},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"15":{"tf":1.0}},"e":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"n":{"df":0,"docs":{},"o":{"d":{"df":4,"docs":{"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}}},"df":11,"docs":{"13":{"tf":5.291502622129181},"14":{"tf":1.7320508075688772},"15":{"tf":2.6457513110645907},"16":{"tf":2.449489742783178},"19":{"tf":1.7320508075688772},"20":{"tf":1.0},"22":{"tf":1.0},"24":{"tf":1.0},"6":{"tf":5.291502622129181},"7":{"tf":1.7320508075688772},"8":{"tf":1.0}}}}}},"s":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}}}}},"w":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.0}}}}}}}},"df":0,"docs":{}},"df":4,"docs":{"14":{"tf":1.0},"19":{"tf":1.0},"4":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}}},"d":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"l":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"f":{"a":{"df":0,"docs":{},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":2.8284271247461903},"25":{"tf":1.4142135623730951},"6":{"tf":2.8284271247461903},"9":{"tf":1.4142135623730951}}}}}},"df":1,"docs":{"0":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.4142135623730951}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"\\"":{"df":0,"docs":{},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}},"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"0":{"tf":1.0},"2":{"tf":1.4142135623730951}}}}}}},"s":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":22,"docs":{"10":{"tf":2.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":8.660254037844387},"14":{"tf":3.4641016151377544},"15":{"tf":3.4641016151377544},"16":{"tf":3.1622776601683795},"17":{"tf":2.6457513110645907},"18":{"tf":2.0},"19":{"tf":4.242640687119285},"20":{"tf":3.872983346207417},"21":{"tf":2.6457513110645907},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"24":{"tf":1.7320508075688772},"25":{"tf":1.4142135623730951},"26":{"tf":1.0},"5":{"tf":2.449489742783178},"6":{"tf":8.660254037844387},"7":{"tf":3.4641016151377544},"8":{"tf":1.7320508075688772},"9":{"tf":1.4142135623730951}}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"t":{"a":{"c":{"df":0,"docs":{},"h":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":5,"docs":{"13":{"tf":1.4142135623730951},"15":{"tf":1.0},"16":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.4142135623730951}},"e":{"d":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}},"df":0,"docs":{}}}},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":5,"docs":{"13":{"tf":1.0},"14":{"tf":1.7320508075688772},"20":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.7320508075688772}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"o":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{".":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{},"m":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.7320508075688772}}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":4,"docs":{"13":{"tf":1.0},"14":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0}}}}}},"n":{"a":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"_":{"b":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"k":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"c":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}},"v":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":4,"docs":{"14":{"tf":1.0},"24":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0}},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":3,"docs":{"19":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}}}}}},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{}},"df":6,"docs":{"0":{"tf":1.0},"1":{"tf":1.0},"10":{"tf":1.0},"15":{"tf":1.4142135623730951},"17":{"tf":2.6457513110645907},"5":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":2,"docs":{"15":{"tf":1.0},"17":{"tf":4.123105625617661}}}}}}}}}},"x":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":9,"docs":{"13":{"tf":2.449489742783178},"14":{"tf":1.0},"15":{"tf":1.0},"19":{"tf":1.0},"3":{"tf":1.4142135623730951},"4":{"tf":2.0},"5":{"tf":1.4142135623730951},"6":{"tf":2.449489742783178},"7":{"tf":1.0}},"e":{".":{"df":0,"docs":{},"m":{"d":{"df":1,"docs":{"3":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":3,"docs":{"0":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":5,"docs":{"14":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"26":{"tf":1.0},"7":{"tf":1.0}}}},"t":{"_":{"c":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}}}},"f":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":1,"docs":{"0":{"tf":1.4142135623730951}}}},"n":{"a":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"d":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":2,"docs":{"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772}}},"df":0,"docs":{}},"r":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"x":{"df":0,"docs":{},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":9,"docs":{"1":{"tf":1.0},"13":{"tf":3.1622776601683795},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"17":{"tf":1.0},"18":{"tf":1.4142135623730951},"20":{"tf":1.0},"21":{"tf":2.23606797749979},"6":{"tf":3.1622776601683795}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":5,"docs":{"13":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"a":{"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"21":{"tf":4.123105625617661}}}}},"s":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}}},"df":0,"docs":{}}},"n":{"df":23,"docs":{"10":{"tf":2.8284271247461903},"11":{"tf":1.4142135623730951},"12":{"tf":1.4142135623730951},"13":{"tf":12.328828005937952},"14":{"tf":5.0990195135927845},"15":{"tf":4.898979485566356},"16":{"tf":4.47213595499958},"17":{"tf":3.7416573867739413},"18":{"tf":2.8284271247461903},"19":{"tf":6.0},"20":{"tf":5.477225575051661},"21":{"tf":3.7416573867739413},"22":{"tf":3.4641016151377544},"23":{"tf":3.4641016151377544},"24":{"tf":2.449489742783178},"25":{"tf":2.23606797749979},"26":{"tf":1.4142135623730951},"4":{"tf":1.7320508075688772},"5":{"tf":3.872983346207417},"6":{"tf":12.328828005937952},"7":{"tf":5.0990195135927845},"8":{"tf":2.449489742783178},"9":{"tf":2.23606797749979}},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.4142135623730951}}}}}},"o":{"c":{"df":0,"docs":{},"u":{"df":3,"docs":{"10":{"tf":1.0},"13":{"tf":2.6457513110645907},"6":{"tf":2.6457513110645907}},"s":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":9,"docs":{"10":{"tf":1.4142135623730951},"13":{"tf":3.605551275463989},"14":{"tf":1.7320508075688772},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"20":{"tf":1.0},"21":{"tf":1.0},"6":{"tf":3.605551275463989},"7":{"tf":1.7320508075688772}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"_":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":0,"docs":{},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"12":{"tf":1.0},"22":{"tf":1.4142135623730951}},"t":{"df":2,"docs":{"0":{"tf":1.0},"22":{"tf":1.7320508075688772}}}}},"df":0,"docs":{}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":1,"docs":{"10":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}}}},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":5,"docs":{"13":{"tf":1.4142135623730951},"19":{"tf":1.0},"25":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}},"n":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}}},"o":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":2,"docs":{"20":{"tf":1.4142135623730951},"21":{"tf":1.4142135623730951}}},"y":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"l":{"df":0,"docs":{},"o":{"b":{"a":{"df":0,"docs":{},"l":{"df":23,"docs":{"1":{"tf":1.0},"10":{"tf":1.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":1.0},"25":{"tf":1.0},"26":{"tf":1.0},"5":{"tf":3.1622776601683795},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"h":{"a":{"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}}}},"df":0,"docs":{}},"s":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"23":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"y":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"23":{"tf":1.0}},"l":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"l":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":3,"docs":{"13":{"tf":1.0},"19":{"tf":1.4142135623730951},"6":{"tf":1.0}}},"y":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"k":{"df":1,"docs":{"5":{"tf":1.0}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"14":{"tf":1.0},"6":{"tf":1.4142135623730951},"7":{"tf":1.0}}}}}}}}}},"i":{"1":{"6":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":1,"docs":{"21":{"tf":1.0}}}},"df":0,"docs":{}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":12,"docs":{"13":{"tf":3.1622776601683795},"14":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":2.449489742783178},"18":{"tf":1.7320508075688772},"19":{"tf":2.23606797749979},"20":{"tf":2.449489742783178},"21":{"tf":2.0},"22":{"tf":1.0},"6":{"tf":3.1622776601683795},"7":{"tf":1.0}}},"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":5,"docs":{"13":{"tf":2.0},"20":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.4142135623730951},"6":{"tf":2.0}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":3,"docs":{"1":{"tf":1.4142135623730951},"13":{"tf":1.0},"6":{"tf":1.0}}}},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"5":{"tf":1.0}}}}},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.7320508075688772},"15":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.7320508075688772}}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"_":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}},"t":{"df":0,"docs":{},"e":{"a":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"t":{"df":15,"docs":{"13":{"tf":5.477225575051661},"14":{"tf":1.4142135623730951},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":2.449489742783178},"18":{"tf":1.4142135623730951},"19":{"tf":2.449489742783178},"20":{"tf":2.23606797749979},"21":{"tf":1.7320508075688772},"22":{"tf":1.7320508075688772},"23":{"tf":1.4142135623730951},"24":{"tf":1.0},"6":{"tf":5.477225575051661},"7":{"tf":1.4142135623730951},"8":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}}}}},"s":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"23":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"a":{"c":{"df":0,"docs":{},"h":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"_":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":2,"docs":{"20":{"tf":1.0},"21":{"tf":1.0}},"e":{"d":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"df":0,"docs":{}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":2,"docs":{"20":{"tf":1.0},"22":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"n":{"df":1,"docs":{"19":{"tf":1.0}},"n":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":3,"docs":{"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0}},"i":{"b":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"j":{"df":0,"docs":{},"o":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":0,"docs":{},"p":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"y":{"df":8,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":1.4142135623730951},"19":{"tf":1.0},"25":{"tf":1.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.7320508075688772},"7":{"tf":1.4142135623730951},"9":{"tf":1.0}}}},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"n":{"d":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"l":{"df":0,"docs":{},"e":{"a":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{">":{"df":0,"docs":{},"w":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}}}},"df":0,"docs":{},"v":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":2.0},"25":{"tf":1.4142135623730951},"6":{"tf":2.0},"9":{"tf":1.4142135623730951}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":3,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.4142135623730951}}}}},"n":{"df":0,"docs":{},"e":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"19":{"tf":1.0},"6":{"tf":1.7320508075688772}}}},"v":{"df":0,"docs":{},"e":{"df":3,"docs":{"0":{"tf":1.4142135623730951},"13":{"tf":1.0},"6":{"tf":1.0}}}}},"o":{"c":{"a":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}},"df":0,"docs":{}},"df":0,"docs":{},"o":{"df":0,"docs":{},"k":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"m":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"4":{"tf":1.0}}}},"n":{"df":0,"docs":{},"i":{"df":1,"docs":{"23":{"tf":1.0}}}},"p":{"<":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}},"df":10,"docs":{"11":{"tf":1.0},"13":{"tf":1.4142135623730951},"14":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.4142135623730951},"25":{"tf":1.7320508075688772},"5":{"tf":1.0},"6":{"tf":1.4142135623730951},"7":{"tf":1.0},"9":{"tf":1.7320508075688772}}},"r":{"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"23":{"tf":1.0}}}}}},"t":{"c":{"df":0,"docs":{},"h":{"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}}},"df":0,"docs":{}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"a":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}},"t":{"a":{"d":{"a":{"df":0,"docs":{},"t":{"a":{"df":1,"docs":{"22":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"o":{"d":{"df":0,"docs":{},"e":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":6,"docs":{"0":{"tf":1.0},"13":{"tf":3.1622776601683795},"15":{"tf":1.0},"22":{"tf":1.4142135623730951},"5":{"tf":1.0},"6":{"tf":3.1622776601683795}},"l":{"df":2,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":2,"docs":{"1":{"tf":1.0},"10":{"tf":2.8284271247461903}},"e":{".":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"u":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"10":{"tf":2.0}}}}},"df":0,"docs":{}}}},"v":{"df":0,"docs":{},"e":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":3.3166247903554},"6":{"tf":3.3166247903554}}}}},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}}}}},"x":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"16":{"tf":3.1622776601683795}}}}},"df":2,"docs":{"1":{"tf":1.0},"16":{"tf":3.605551275463989}}}}},"n":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":13,"docs":{"0":{"tf":1.4142135623730951},"11":{"tf":1.0},"13":{"tf":2.23606797749979},"15":{"tf":1.0},"17":{"tf":1.4142135623730951},"18":{"tf":1.4142135623730951},"19":{"tf":1.4142135623730951},"22":{"tf":1.0},"24":{"tf":1.4142135623730951},"26":{"tf":1.4142135623730951},"5":{"tf":1.7320508075688772},"6":{"tf":2.23606797749979},"8":{"tf":1.4142135623730951}},"s":{"df":0,"docs":{},"p":{"a":{"c":{"df":22,"docs":{"10":{"tf":1.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":1.0},"25":{"tf":1.0},"26":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"w":{"df":4,"docs":{"13":{"tf":2.449489742783178},"14":{"tf":1.0},"6":{"tf":2.449489742783178},"7":{"tf":1.0}}},"x":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}}}},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":7,"docs":{"13":{"tf":2.449489742783178},"15":{"tf":1.0},"16":{"tf":1.0},"17":{"tf":1.0},"19":{"tf":1.0},"22":{"tf":1.0},"6":{"tf":2.449489742783178}}},"df":0,"docs":{}}},"df":13,"docs":{"1":{"tf":1.0},"13":{"tf":5.5677643628300215},"14":{"tf":1.4142135623730951},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":1.4142135623730951},"20":{"tf":3.0},"21":{"tf":1.0},"22":{"tf":1.0},"6":{"tf":5.5677643628300215},"7":{"tf":1.4142135623730951}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"20":{"tf":5.744562646538029}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"o":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"t":{"a":{"df":0,"docs":{},"t":{"df":3,"docs":{"13":{"tf":2.0},"5":{"tf":2.449489742783178},"6":{"tf":2.0}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"y":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"w":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":4,"docs":{"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"18":{"tf":1.0},"19":{"tf":1.0}}}}}}},"o":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}}},"n":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"_":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.0}}}}},"l":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"v":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}},"p":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"h":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"y":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"<":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":7,"docs":{"13":{"tf":1.7320508075688772},"14":{"tf":1.4142135623730951},"25":{"tf":1.7320508075688772},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772},"7":{"tf":1.4142135623730951},"9":{"tf":1.7320508075688772}}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"t":{"df":0,"docs":{},"p":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"d":{"df":3,"docs":{"25":{"tf":1.0},"5":{"tf":1.4142135623730951},"9":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":4,"docs":{"0":{"tf":1.0},"1":{"tf":1.0},"2":{"tf":1.0},"3":{"tf":1.0}}}}}}}}},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"20":{"tf":1.0},"21":{"tf":1.0}}}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":3,"docs":{"1":{"tf":1.4142135623730951},"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":2,"docs":{"11":{"tf":1.4142135623730951},"26":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"20":{"tf":1.4142135623730951}}}}},"t":{"df":0,"docs":{},"i":{"a":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"t":{"df":0,"docs":{},"h":{"df":3,"docs":{"19":{"tf":1.0},"24":{"tf":1.0},"8":{"tf":1.0}}}},"y":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"d":{"df":1,"docs":{"15":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"a":{"df":0,"docs":{},"g":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}},"df":0,"docs":{}}}}},"df":0,"docs":{}}},"h":{"a":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":2,"docs":{"0":{"tf":1.0},"4":{"tf":1.0}}}}},"df":0,"docs":{}},"i":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}},"df":0,"docs":{},"x":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"l":{"a":{"c":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}}}}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}},"df":0,"docs":{}},"o":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"5":{"tf":1.0}}}}}}},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":2,"docs":{"19":{"tf":1.0},"20":{"tf":1.0}}}}}},"v":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"17":{"tf":1.0},"6":{"tf":1.7320508075688772}},"s":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"i":{"d":{"(":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}}},"df":1,"docs":{"17":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}}},"df":1,"docs":{"5":{"tf":1.0}}}}}}}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{".":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"15":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"o":{"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":1,"docs":{"19":{"tf":1.0}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":1,"docs":{"19":{"tf":2.0}}}}}},"d":{"df":0,"docs":{},"u":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}}}}},"r":{"df":0,"docs":{},"e":{"a":{"d":{"df":3,"docs":{"24":{"tf":1.0},"26":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"c":{"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"f":{"df":1,"docs":{"1":{"tf":2.0}},"e":{"df":0,"docs":{},"r":{"df":5,"docs":{"0":{"tf":1.0},"14":{"tf":1.7320508075688772},"15":{"tf":1.0},"16":{"tf":1.0},"7":{"tf":1.7320508075688772}}}}},"g":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.4142135623730951}},"r":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{".":{"df":0,"docs":{},"r":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"2":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":7,"docs":{"0":{"tf":1.0},"1":{"tf":2.23606797749979},"5":{"tf":3.0},"6":{"tf":8.831760866327848},"7":{"tf":3.872983346207417},"8":{"tf":2.449489742783178},"9":{"tf":2.23606797749979}}}}}}},"j":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}},"df":0,"docs":{}}},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":3,"docs":{"13":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0}}}}},"p":{"df":0,"docs":{},"l":{"a":{"c":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}},"o":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"4":{"tf":1.0}}}}}}}}}},"q":{"df":0,"docs":{},"u":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"19":{"tf":1.0}}}}}}},"s":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"v":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"t":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"n":{"df":11,"docs":{"15":{"tf":3.4641016151377544},"16":{"tf":3.1622776601683795},"17":{"tf":2.6457513110645907},"18":{"tf":2.0},"19":{"tf":4.123105625617661},"20":{"tf":3.872983346207417},"21":{"tf":2.6457513110645907},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"24":{"tf":1.0},"8":{"tf":1.0}},"t":{"df":0,"docs":{},"y":{"df":0,"docs":{},"p":{"df":9,"docs":{"15":{"tf":2.8284271247461903},"16":{"tf":2.6457513110645907},"17":{"tf":2.449489742783178},"19":{"tf":2.8284271247461903},"20":{"tf":2.449489742783178},"21":{"tf":1.0},"24":{"tf":1.4142135623730951},"26":{"tf":1.0},"8":{"tf":1.4142135623730951}}}}}}}}},"v":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"l":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}}}},"g":{"b":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"26":{"tf":1.0}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":2.0},"25":{"tf":1.4142135623730951},"6":{"tf":2.0},"9":{"tf":1.4142135623730951}}}}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"18":{"tf":1.0},"21":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"18":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":4,"docs":{"18":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0},"22":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"n":{"_":{"df":0,"docs":{},"n":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"d":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":4,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.0}},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"m":{"df":3,"docs":{"0":{"tf":1.0},"1":{"tf":1.4142135623730951},"26":{"tf":2.23606797749979}},"e":{".":{"df":0,"docs":{},"r":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"i":{"df":1,"docs":{"2":{"tf":1.0}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}},"s":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"t":{"df":0,"docs":{},"o":{"_":{"b":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"b":{"a":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":3,"docs":{"10":{"tf":1.0},"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}}}}},"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"h":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"p":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}}}},"df":0,"docs":{}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{},"e":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"g":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"25":{"tf":2.0},"9":{"tf":2.0}}},"df":0,"docs":{}},"df":2,"docs":{"25":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}}}}}}},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"p":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":2,"docs":{"13":{"tf":4.0},"6":{"tf":4.0}}}},"df":0,"docs":{}}},"n":{"d":{"_":{"b":{"df":0,"docs":{},"y":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"k":{"df":0,"docs":{},"e":{"df":0,"docs":{},"y":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"s":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":2,"docs":{"13":{"tf":2.0},"6":{"tf":2.0}}},"df":0,"docs":{}},"p":{"a":{"df":0,"docs":{},"r":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"q":{"df":0,"docs":{},"u":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"c":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"5":{"tf":1.7320508075688772},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}}}}},"s":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"\'":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"_":{"df":0,"docs":{},"i":{"d":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"v":{"df":1,"docs":{"17":{"tf":1.0}}}},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":4,"docs":{"17":{"tf":1.0},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0}}},"df":0,"docs":{}}},"df":10,"docs":{"1":{"tf":1.0},"13":{"tf":1.0},"15":{"tf":1.7320508075688772},"16":{"tf":1.7320508075688772},"17":{"tf":1.4142135623730951},"18":{"tf":2.0},"19":{"tf":1.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0},"6":{"tf":1.0}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":3,"docs":{"15":{"tf":1.0},"16":{"tf":1.0},"18":{"tf":3.3166247903554}}}}},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}},"t":{"_":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"c":{"df":0,"docs":{},"u":{"df":1,"docs":{"10":{"tf":1.0}},"s":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"w":{"a":{"df":0,"docs":{},"r":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":1,"docs":{"10":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"12":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"t":{"a":{"b":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"12":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"l":{"df":0,"docs":{},"e":{"a":{"d":{"df":1,"docs":{"5":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"(":{"\\"":{"<":{"c":{"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":1,"docs":{"5":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"p":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"11":{"tf":1.0}},"e":{"(":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":1,"docs":{"11":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"w":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"w":{"a":{"df":0,"docs":{},"r":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":1,"docs":{"10":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"s":{"c":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":1,"docs":{"10":{"tf":1.0}},"l":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"df":4,"docs":{"0":{"tf":1.0},"24":{"tf":1.0},"5":{"tf":1.0},"8":{"tf":1.0}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":3,"docs":{"14":{"tf":1.0},"4":{"tf":1.0},"7":{"tf":1.0}}}}},"o":{"df":0,"docs":{},"w":{"df":1,"docs":{"4":{"tf":1.0}}}}},"i":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"13":{"tf":2.449489742783178},"6":{"tf":2.449489742783178}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"d":{"df":0,"docs":{},"e":{"df":1,"docs":{"10":{"tf":1.0}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"df":0,"docs":{},"l":{"df":3,"docs":{"14":{"tf":1.0},"19":{"tf":1.0},"7":{"tf":1.0}}}}},"z":{"df":0,"docs":{},"e":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"n":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}}}},"df":3,"docs":{"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":1.0}}}}}}}},"df":0,"docs":{}},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}},"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"i":{"df":0,"docs":{},"f":{"df":6,"docs":{"13":{"tf":3.605551275463989},"14":{"tf":1.0},"25":{"tf":1.0},"6":{"tf":3.605551275463989},"7":{"tf":1.0},"9":{"tf":1.0}},"i":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}},"df":0,"docs":{}},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.4142135623730951},"7":{"tf":1.4142135623730951}}},"df":0,"docs":{}},"_":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"(":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"x":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}},"df":1,"docs":{"4":{"tf":1.0}}}}}}},"d":{"df":0,"docs":{},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"v":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"w":{"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}},"s":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":7,"docs":{"13":{"tf":1.0},"14":{"tf":2.0},"20":{"tf":1.7320508075688772},"4":{"tf":1.0},"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":2.0}}}}}},"t":{"a":{"c":{"df":0,"docs":{},"k":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":4,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"19":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":1,"docs":{"19":{"tf":1.0}}}}},"r":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"df":18,"docs":{"13":{"tf":3.7416573867739413},"14":{"tf":2.6457513110645907},"15":{"tf":1.0},"17":{"tf":1.0},"18":{"tf":1.0},"19":{"tf":3.0},"20":{"tf":1.4142135623730951},"21":{"tf":1.0},"22":{"tf":1.0},"23":{"tf":1.0},"24":{"tf":2.0},"25":{"tf":2.0},"26":{"tf":1.0},"5":{"tf":4.0},"6":{"tf":3.7416573867739413},"7":{"tf":2.6457513110645907},"8":{"tf":2.0},"9":{"tf":2.0}}}}}},"y":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":2,"docs":{"25":{"tf":1.4142135623730951},"9":{"tf":1.4142135623730951}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}},"df":0,"docs":{}}}}}}}},"u":{"c":{"df":0,"docs":{},"h":{"df":2,"docs":{"20":{"tf":1.0},"5":{"tf":1.4142135623730951}}}},"df":0,"docs":{},"p":{"df":0,"docs":{},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":3,"docs":{"14":{"tf":1.0},"5":{"tf":1.0},"7":{"tf":1.0}}}}}}},"r":{"df":0,"docs":{},"f":{"a":{"c":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"w":{"a":{"df":0,"docs":{},"p":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}},"df":0,"docs":{}},"y":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"24":{"tf":1.7320508075688772},"8":{"tf":1.7320508075688772}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"24":{"tf":2.449489742783178},"8":{"tf":2.449489742783178}}}}}}}},"t":{"a":{"b":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"l":{"df":1,"docs":{"20":{"tf":1.0}},"e":{"df":0,"docs":{},"s":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"20":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"b":{"a":{"df":0,"docs":{},"r":{".":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"(":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"t":{"a":{"b":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"12":{"tf":1.0}}}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"22":{"tf":3.872983346207417}}}}}}}}},"df":2,"docs":{"1":{"tf":1.0},"12":{"tf":2.0}}}},"df":0,"docs":{}},"df":11,"docs":{"0":{"tf":1.0},"1":{"tf":1.4142135623730951},"12":{"tf":1.0},"13":{"tf":5.5677643628300215},"14":{"tf":3.0},"18":{"tf":1.0},"20":{"tf":2.0},"22":{"tf":2.449489742783178},"23":{"tf":2.449489742783178},"6":{"tf":5.5677643628300215},"7":{"tf":3.0}},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":1,"docs":{"23":{"tf":3.872983346207417}}}}}},"s":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"_":{"a":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"(":{"_":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}},"df":0,"docs":{}}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":4,"docs":{"13":{"tf":1.0},"25":{"tf":1.0},"6":{"tf":1.0},"9":{"tf":1.0}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":3,"docs":{"19":{"tf":1.4142135623730951},"25":{"tf":2.449489742783178},"9":{"tf":2.449489742783178}}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{".":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"(":{"\\"":{"a":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"v":{"df":0,"docs":{},"e":{"_":{"b":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":1,"docs":{"4":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"11":{"tf":1.0}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"11":{"tf":2.23606797749979},"26":{"tf":2.0}},"r":{"df":0,"docs":{},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"26":{"tf":1.0}}}}},"df":0,"docs":{}}}}}}}}}}}},"i":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":1,"docs":{"0":{"tf":1.0}},"s":{"df":0,"docs":{},"t":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"t":{"df":0,"docs":{},"l":{"df":9,"docs":{"13":{"tf":2.449489742783178},"14":{"tf":1.7320508075688772},"19":{"tf":1.4142135623730951},"20":{"tf":1.0},"21":{"tf":1.4142135623730951},"23":{"tf":1.4142135623730951},"4":{"tf":1.0},"6":{"tf":2.449489742783178},"7":{"tf":1.7320508075688772}},"e":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"21":{"tf":1.0}}}}},"t":{"a":{"b":{"df":1,"docs":{"23":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}}},"o":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":1,"docs":{"4":{"tf":1.0}}}}},"g":{"df":0,"docs":{},"l":{"df":3,"docs":{"10":{"tf":2.0},"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"_":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}},"p":{"_":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":3,"docs":{"0":{"tf":1.0},"13":{"tf":1.0},"6":{"tf":1.0}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{".":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"(":{"[":{"\\"":{"/":{"b":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"/":{"df":0,"docs":{},"z":{"df":0,"docs":{},"s":{"df":0,"docs":{},"h":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"4":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}},"df":0,"docs":{}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"14":{"tf":3.7416573867739413},"7":{"tf":3.7416573867739413}}}}},"df":5,"docs":{"1":{"tf":1.4142135623730951},"13":{"tf":3.4641016151377544},"14":{"tf":4.47213595499958},"6":{"tf":3.4641016151377544},"7":{"tf":4.47213595499958}},"s":{"df":0,"docs":{},"p":{"df":0,"docs":{},"e":{"c":{"df":4,"docs":{"13":{"tf":2.8284271247461903},"14":{"tf":3.7416573867739413},"6":{"tf":2.8284271247461903},"7":{"tf":3.7416573867739413}}},"df":0,"docs":{}}}}}},"i":{"df":0,"docs":{},"m":{"df":1,"docs":{"4":{"tf":1.0}}}},"u":{"df":0,"docs":{},"e":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}}}},"t":{"df":0,"docs":{},"i":{"df":1,"docs":{"19":{"tf":1.0}}},"y":{"_":{"df":0,"docs":{},"p":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{}},"df":1,"docs":{"19":{"tf":1.0}}}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"w":{"df":0,"docs":{},"o":{"df":2,"docs":{"0":{"tf":1.0},"4":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"i":{".":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"4":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":1,"docs":{"4":{"tf":1.0}}}}}},"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":2,"docs":{"25":{"tf":2.449489742783178},"9":{"tf":2.449489742783178}}}}},"df":3,"docs":{"1":{"tf":1.4142135623730951},"25":{"tf":2.23606797749979},"9":{"tf":2.23606797749979}}},"n":{"b":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"(":{"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{},"i":{"df":0,"docs":{},"x":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"k":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":0,"docs":{},"n":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}}},"w":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}}},"z":{"df":0,"docs":{},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"p":{"df":3,"docs":{"13":{"tf":1.7320508075688772},"19":{"tf":1.0},"6":{"tf":1.7320508075688772}},"w":{"a":{"df":0,"docs":{},"r":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"s":{"df":8,"docs":{"0":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.7320508075688772},"22":{"tf":1.0},"25":{"tf":1.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.7320508075688772},"9":{"tf":1.0}}}},"v":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"u":{"df":5,"docs":{"10":{"tf":2.0},"13":{"tf":1.4142135623730951},"25":{"tf":1.0},"6":{"tf":1.4142135623730951},"9":{"tf":1.0}}}},"r":{"df":0,"docs":{},"i":{"a":{"b":{"df":0,"docs":{},"l":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"e":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":1,"docs":{"19":{"tf":1.0}}}}}},"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"c":{"df":4,"docs":{"13":{"tf":1.4142135623730951},"14":{"tf":1.0},"6":{"tf":1.4142135623730951},"7":{"tf":1.0}}},"df":0,"docs":{}}}}},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"w":{"df":2,"docs":{"13":{"tf":2.8284271247461903},"6":{"tf":2.8284271247461903}},"p":{"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"w":{"df":0,"docs":{},"i":{"d":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"(":{"b":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"22":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}},"df":1,"docs":{"22":{"tf":1.0}}}}},"df":0,"docs":{}}}},"df":1,"docs":{"22":{"tf":1.0}}}}}}}},"s":{"df":0,"docs":{},"i":{"b":{"df":0,"docs":{},"l":{"df":7,"docs":{"13":{"tf":1.7320508075688772},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"19":{"tf":1.0},"20":{"tf":1.0},"21":{"tf":1.0},"6":{"tf":1.7320508075688772}},"e":{"_":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":2,"docs":{"15":{"tf":1.0},"16":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"(":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"df":0,"docs":{}},"u":{"a":{"df":0,"docs":{},"l":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}}}}},"w":{"a":{"df":0,"docs":{},"r":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":1,"docs":{"20":{"tf":1.0}}}}}}},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":1,"docs":{"10":{"tf":1.4142135623730951}}}},"t":{"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":7,"docs":{"13":{"tf":1.4142135623730951},"19":{"tf":2.0},"20":{"tf":2.0},"21":{"tf":1.4142135623730951},"22":{"tf":1.0},"23":{"tf":1.7320508075688772},"6":{"tf":1.4142135623730951}}}}}}},"i":{"c":{"df":0,"docs":{},"h":{"(":{"_":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}},"i":{"d":{"df":0,"docs":{},"t":{"df":0,"docs":{},"h":{"df":3,"docs":{"13":{"tf":1.4142135623730951},"22":{"tf":1.0},"6":{"tf":1.4142135623730951}}}}},"df":0,"docs":{},"n":{"d":{"df":0,"docs":{},"o":{"df":0,"docs":{},"w":{"df":6,"docs":{"13":{"tf":3.0},"15":{"tf":1.4142135623730951},"16":{"tf":1.4142135623730951},"18":{"tf":1.0},"20":{"tf":1.0},"6":{"tf":3.0}}}}},"df":0,"docs":{}}},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"k":{"df":1,"docs":{"19":{"tf":1.0}},"s":{"df":0,"docs":{},"p":{"a":{"c":{"df":1,"docs":{"5":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}}},"x":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"y":{"a":{"df":0,"docs":{},"n":{"df":0,"docs":{},"k":{"_":{"df":0,"docs":{},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"z":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"df":1,"docs":{"23":{"tf":1.0}}}}},"o":{"df":0,"docs":{},"o":{"df":0,"docs":{},"m":{"_":{"c":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"_":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"6":{"tf":1.4142135623730951}},"e":{"(":{"_":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}}}}}}},"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"(":{"df":0,"docs":{},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"_":{"df":0,"docs":{},"i":{"d":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"df":2,"docs":{"13":{"tf":2.23606797749979},"6":{"tf":2.23606797749979}}}}}}}},"title":{"root":{"a":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":2,"docs":{"13":{"tf":1.0},"6":{"tf":1.0}}}}}}},"df":0,"docs":{},"p":{"df":0,"docs":{},"i":{"df":1,"docs":{"0":{"tf":1.0}}}}},"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"f":{"df":0,"docs":{},"f":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":1,"docs":{"19":{"tf":1.0}}}}}}}}}}},"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"g":{"df":1,"docs":{"0":{"tf":1.0}}}}},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"15":{"tf":1.0}}}}}}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"2":{"tf":1.0}}}}}}}}},"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":1,"docs":{"0":{"tf":1.0}}}}},"df":0,"docs":{}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":1,"docs":{"17":{"tf":1.0}}}}}}}}}},"x":{"a":{"df":0,"docs":{},"m":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":2,"docs":{"3":{"tf":1.0},"4":{"tf":1.0}}}}}},"df":0,"docs":{}}},"f":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"g":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":1,"docs":{"21":{"tf":1.0}}}}}}}}}},"df":0,"docs":{}}}},"g":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"b":{"a":{"df":0,"docs":{},"l":{"df":1,"docs":{"5":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{}}}},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"s":{"df":1,"docs":{"10":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"x":{"df":1,"docs":{"16":{"tf":1.0}}}}},"n":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":1,"docs":{"20":{"tf":1.0}}}}}}},"df":0,"docs":{}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":1,"docs":{"1":{"tf":1.0}}}}},"df":0,"docs":{}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":0,"docs":{},"i":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":5,"docs":{"5":{"tf":1.0},"6":{"tf":1.0},"7":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}}}}}}},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"m":{"df":1,"docs":{"26":{"tf":1.0}}}}}}}},"s":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":1,"docs":{"18":{"tf":1.0}}}}}}}}}}},"y":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":2,"docs":{"24":{"tf":1.0},"8":{"tf":1.0}}}}}}}},"t":{"a":{"b":{"b":{"a":{"df":0,"docs":{},"r":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":0,"docs":{},"t":{"df":1,"docs":{"22":{"tf":1.0}}}}}}}}},"df":1,"docs":{"12":{"tf":1.0}}}},"df":0,"docs":{}},"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":0,"docs":{},"f":{"df":0,"docs":{},"o":{"df":1,"docs":{"23":{"tf":1.0}}}}}}},"df":0,"docs":{}},"df":0,"docs":{},"h":{"df":0,"docs":{},"e":{"df":0,"docs":{},"m":{"df":0,"docs":{},"e":{"df":2,"docs":{"11":{"tf":1.0},"26":{"tf":1.0}}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":2,"docs":{"14":{"tf":1.0},"7":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"i":{"df":2,"docs":{"25":{"tf":1.0},"9":{"tf":1.0}}}}}}},"lang":"English","pipeline":["trimmer","stopWordFilter","stemmer"],"ref":"id","version":"0.9.5"},"results_options":{"limit_results":30,"teaser_word_count":30},"search_options":{"bool":"OR","expand":true,"fields":{"body":{"boost":1},"breadcrumbs":{"boost":1},"title":{"boost":2}}}}');}catch(error){console.error("Failed to parse Embers config API search index:", error);}Object.assign(target, parsed);})(); \ No newline at end of file diff --git a/docs/config-api-book/session-ref.html b/docs/config-api-book/session-ref.html index ff161b1..27bd2c1 100644 --- a/docs/config-api-book/session-ref.html +++ b/docs/config-api-book/session-ref.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/system-runtime.html b/docs/config-api-book/system-runtime.html index 767e640..9ef939c 100644 --- a/docs/config-api-book/system-runtime.html +++ b/docs/config-api-book/system-runtime.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/tab-bar-context.html b/docs/config-api-book/tab-bar-context.html index 605c2a9..79d63e1 100644 --- a/docs/config-api-book/tab-bar-context.html +++ b/docs/config-api-book/tab-bar-context.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/tab-info.html b/docs/config-api-book/tab-info.html index 30180eb..d8f5cdc 100644 --- a/docs/config-api-book/tab-info.html +++ b/docs/config-api-book/tab-info.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/tabbar.html b/docs/config-api-book/tabbar.html index ae33ba3..316a3e2 100644 --- a/docs/config-api-book/tabbar.html +++ b/docs/config-api-book/tabbar.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/theme.html b/docs/config-api-book/theme.html index a7a2fc0..369a5ff 100644 --- a/docs/config-api-book/theme.html +++ b/docs/config-api-book/theme.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/tree.html b/docs/config-api-book/tree.html index b358b52..8d6820e 100644 --- a/docs/config-api-book/tree.html +++ b/docs/config-api-book/tree.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; diff --git a/docs/config-api-book/ui.html b/docs/config-api-book/ui.html index d100f0d..b900037 100644 --- a/docs/config-api-book/ui.html +++ b/docs/config-api-book/ui.html @@ -35,7 +35,7 @@ const path_to_root = ""; const default_light_theme = "light"; const default_dark_theme = "navy"; - window.path_to_searchindex_js = "searchindex-256e957a.js"; + window.path_to_searchindex_js = "searchindex-60b5c002.js"; @@ -214,6 +214,8 @@

fn segment

Create a [`BarSegment`] from a [`UiApi`] receiver and text using default styling.

segment(_: UiApi, text: String) -> BarSegment produces plain text with default [StyleSpec] values and no click target.

+

See the overloaded segment(_: UiApi, text: String, options: Map) -> BarSegment doc for +the full options: Map styling keys.

diff --git a/docs/config-api/action.md b/docs/config-api/action.md index 9dc1ed9..23bc276 100644 --- a/docs/config-api/action.md +++ b/docs/config-api/action.md @@ -2,6 +2,30 @@ ```Namespace: global``` +
+

fn break_current_node

+ +```rust,ignore +fn break_current_node(_: ActionApi, destination: "tab" | "floating") -> Action +``` + +
+
+ +
+ +
+Break the current node into a new tab or floating window. +`destination` accepts `tab` or `floating`. +Example: `action.break_current_node("floating")`. +
+ +
+
+

fn cancel_search

@@ -175,6 +199,29 @@ Description Close the currently focused view.
+

+
+
+
+

fn commit_search

+ +```rust,ignore +fn commit_search(_: ActionApi) -> Action +``` + +
+
+ +
+ +
+Finalize the active search, keep the current match and cursor position, and leave search +mode with the committed result in place. +
+

@@ -581,6 +628,30 @@ Description Insert a tab before the current tab.
+
+

+
+
+

fn join_buffer_here

+ +```rust,ignore +fn join_buffer_here(_: ActionApi, buffer_id: int, placement: "tab-after" | "tab-before" | "left" | "right" | "up" | "down") -> Action +``` + +
+
+ +
+ +
+Attach a buffer at the current node. +`placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. +Example: `action.join_buffer_here(12, "tab-after")`. +
+

@@ -706,6 +777,98 @@ Description Move a buffer into a specific node.
+
+
+
+
+

fn move_current_node_after

+ +```rust,ignore +fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action +``` + +
+
+ +
+ +
+Move the current node after a sibling. +
+ +
+
+
+
+

fn move_current_node_before

+ +```rust,ignore +fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action +``` + +
+
+ +
+ +
+Move the current node before a sibling. +Use this when the current node is the one being repositioned. +Example: `action.move_current_node_before(42)`. +
+ +
+
+
+
+

fn move_node_after

+ +```rust,ignore +fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action +``` + +
+
+ +
+ +
+Move a node after a sibling. +Use this when you need to move a specific node id instead of the current node. +Example: `action.move_node_after(10, 42)`. +
+ +
+
+
+
+

fn move_node_before

+ +```rust,ignore +fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action +``` + +
+
+ +
+ +
+Move a node before a sibling. +
+

@@ -779,7 +942,7 @@ Build a no-op action.

fn notify

```rust,ignore -fn notify(_: ActionApi, level: String, message: String) -> Action +fn notify(_: ActionApi, level: "info" | "warn" | "error", message: String) -> Action ```
@@ -794,6 +957,30 @@ Description Emit a client notification.
+ + +
+
+

fn open_buffer_history

+ +```rust,ignore +fn open_buffer_history(_: ActionApi, buffer_id: int, scope: "visible" | "full", placement: "floating" | "tab") -> Action +``` + +
+
+ +
+ +
+Open the history of a buffer in a new view. +`scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. +Example: `action.open_buffer_history(12, "visible", "floating")`. +
+

@@ -1353,7 +1540,7 @@ Send a key notation sequence to the focused buffer.

fn split_with

```rust,ignore -fn split_with(_: ActionApi, direction: String, tree: TreeSpec) -> Action +fn split_with(_: ActionApi, direction: "h" | "horizontal" | "v" | "vertical", tree: TreeSpec) -> Action ```
@@ -1368,6 +1555,28 @@ Description Split the current node and attach the provided tree as the new sibling.
+ + +
+
+

fn swap_current_node

+ +```rust,ignore +fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action +``` + +
+
+ +
+ +
+Swap the current node with a sibling. +
+

@@ -1390,6 +1599,53 @@ Description Toggle a named input mode. + + +
+
+

fn toggle_zoom_node

+ +```rust,ignore +fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action +``` + +
+
+ +
+ +
+Toggle zoom for the specified node id. +There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the +focused node and `toggle_zoom_node` when you already know the target id. +
+ +
+
+
+
+

fn unzoom_current_session

+ +```rust,ignore +fn unzoom_current_session(_: ActionApi) -> Action +``` + +
+
+ +
+ +
+Clear the current session's active zoom state. +This removes the current session zoom rather than unwinding a stack of prior zooms. +
+

@@ -1415,3 +1671,26 @@ Copy the current selection into the clipboard.
+
+

fn zoom_current_node

+ +```rust,ignore +fn zoom_current_node(_: ActionApi) -> Action +``` + +
+
+ +
+ +
+Zoom the session's currently focused node. +There is intentionally no separate `zoom_node(node_id)` helper in this API surface. +
+ +
+
+
diff --git a/docs/config-api/defs/registration.rhai b/docs/config-api/defs/registration.rhai index 752c140..44b90d2 100644 --- a/docs/config-api/defs/registration.rhai +++ b/docs/config-api/defs/registration.rhai @@ -9,14 +9,39 @@ fn bar(_: UiApi, left: array, center: array, right: array) -> BarSpec; /// /// `segment(_: UiApi, text: String) -> BarSegment` produces plain text with default /// [`StyleSpec`] values and no click target. +/// +/// See the overloaded `segment(_: UiApi, text: String, options: Map) -> BarSegment` doc for +/// the full `options: Map` styling keys. fn segment(_: UiApi, text: string) -> BarSegment; /// Create a [`BarSegment`] from a [`UiApi`] receiver, text, and an `options: Map`. /// +/// See the main `segment(_: UiApi, text: String) -> BarSegment` doc for the shared behavior. +/// /// `segment(_: UiApi, text: String, options: Map) -> BarSegment` supports `fg`, `bg`, -/// `bold`, `italic`, `underline`, `dim`, and `target` keys to override styling and attach an -/// optional interaction target. `dim` is a boolean that renders the text with reduced -/// intensity for a muted appearance. +/// `bold`, `italic`, `underline`, `dim`, `blink`, and `target` keys to override styling and +/// attach an optional interaction target. +/// +/// `fg` is the foreground color and accepts a standard CSS color name, a hex code such as +/// `#ff0000`, or an RGB/RGBA string such as `rgb(255,0,0)` or `rgba(255,0,0,0.5)`. +/// `bg` is the background color and accepts the same color formats. +/// `bold`, `italic`, `underline`, `dim`, and `blink` are boolean `true`/`false` flags. +/// `dim` renders the text with reduced intensity for a muted appearance. +/// `blink` enables blinking text for that segment, but many modern terminal emulators ignore +/// or disable blink by default, so blinking text may not appear consistently. +/// `target` is an optional interaction target, usually either a string identifier such as +/// `"myTarget"` or a structured object such as `#{ type: "callback", id: "save" }`, +/// depending on the consumer API. +/// +/// Examples: +/// - `#{ fg: "#ff0000", bg: "rgba(0,0,0,0.5)", bold: true }` +/// - `#{ target: "myTarget" }` +/// - `#{ target: #{ type: "callback", id: "save" } }` +/// +/// Accessibility note: blinking text can be distracting and may trigger seizures for some +/// users. Use `blink` sparingly, prefer non-animated emphasis such as `dim` or color/weight +/// changes, follow WCAG guidance to avoid flashing content, and respect reduced-motion +/// preferences before enabling `blink`. fn segment(_: UiApi, text: string, options: map) -> BarSegment; /// Read an environment variable, if it is set. @@ -82,6 +107,11 @@ fn tabs(_: TreeApi, tabs: array) -> TreeSpec; /// Build a tabs container with an explicit active tab. fn tabs_with_active(_: TreeApi, tabs: array, active: int) -> TreeSpec; +/// Break the current node into a new tab or floating window. +/// `destination` accepts `tab` or `floating`. +/// Example: `action.break_current_node("floating")`. +fn break_current_node(_: ActionApi, destination: string) -> Action; + /// Cancel the active search. fn cancel_search(_: ActionApi) -> Action; @@ -106,6 +136,10 @@ fn close_node(_: ActionApi, node_id: int) -> Action; /// Close the currently focused view. fn close_view(_: ActionApi) -> Action; +/// Finalize the active search, keep the current match and cursor position, and leave search +/// mode with the committed result in place. +fn commit_search(_: ActionApi) -> Action; + /// Copy the current selection into the clipboard. fn copy_selection(_: ActionApi) -> Action; @@ -166,6 +200,11 @@ fn insert_tab_before(_: ActionApi, tabs_node_id: int, title: string, tree: TreeS /// Insert a tab before the current tab. fn insert_tab_before_current(_: ActionApi, title: string, tree: TreeSpec) -> Action; +/// Attach a buffer at the current node. +/// `placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. +/// Example: `action.join_buffer_here(12, "tab-after")`. +fn join_buffer_here(_: ActionApi, buffer_id: int, placement: string) -> Action; + /// Kill the currently focused buffer. fn kill_buffer(_: ActionApi) -> Action; @@ -192,6 +231,22 @@ fn move_buffer_to_floating(_: ActionApi, buffer_id: int, options: map) -> Action /// Move a buffer into a specific node. fn move_buffer_to_node(_: ActionApi, buffer_id: int, node_id: int) -> Action; +/// Move the current node after a sibling. +fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action; + +/// Move the current node before a sibling. +/// Use this when the current node is the one being repositioned. +/// Example: `action.move_current_node_before(42)`. +fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action; + +/// Move a node after a sibling. +/// Use this when you need to move a specific node id instead of the current node. +/// Example: `action.move_node_after(10, 42)`. +fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action; + +/// Move a node before a sibling. +fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action; + /// Select the next tab in the currently focused tabs node. fn next_current_tabs(_: ActionApi) -> Action; @@ -204,6 +259,11 @@ fn noop(_: ActionApi) -> Action; /// Emit a client notification. fn notify(_: ActionApi, level: string, message: string) -> Action; +/// Open the history of a buffer in a new view. +/// `scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. +/// Example: `action.open_buffer_history(12, "visible", "floating")`. +fn open_buffer_history(_: ActionApi, buffer_id: int, scope: string, placement: string) -> Action; + /// Open a floating view around the provided tree. fn open_floating(_: ActionApi, tree: TreeSpec, options: map) -> Action; @@ -297,12 +357,28 @@ fn send_keys_current(_: ActionApi, notation: string) -> Action; /// Split the current node and attach the provided tree as the new sibling. fn split_with(_: ActionApi, direction: string, tree: TreeSpec) -> Action; +/// Swap the current node with a sibling. +fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action; + /// Toggle a named input mode. fn toggle_mode(_: ActionApi, mode: string) -> Action; +/// Toggle zoom for the specified node id. +/// There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the +/// focused node and `toggle_zoom_node` when you already know the target id. +fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action; + +/// Clear the current session's active zoom state. +/// This removes the current session zoom rather than unwinding a stack of prior zooms. +fn unzoom_current_session(_: ActionApi) -> Action; + /// Copy the current selection into the clipboard. fn yank_selection(_: ActionApi) -> Action; +/// Zoom the session's currently focused node. +/// There is intentionally no separate `zoom_node(node_id)` helper in this API surface. +fn zoom_current_node(_: ActionApi) -> Action; + /// Toggle focus-on-click behavior. /// /// # rhai-autodocs:index:22 diff --git a/docs/config-api/defs/runtime.rhai b/docs/config-api/defs/runtime.rhai index ca8da20..902b976 100644 --- a/docs/config-api/defs/runtime.rhai +++ b/docs/config-api/defs/runtime.rhai @@ -14,14 +14,39 @@ fn bar(_: UiApi, left: array, center: array, right: array) -> BarSpec; /// /// `segment(_: UiApi, text: String) -> BarSegment` produces plain text with default /// [`StyleSpec`] values and no click target. +/// +/// See the overloaded `segment(_: UiApi, text: String, options: Map) -> BarSegment` doc for +/// the full `options: Map` styling keys. fn segment(_: UiApi, text: string) -> BarSegment; /// Create a [`BarSegment`] from a [`UiApi`] receiver, text, and an `options: Map`. /// +/// See the main `segment(_: UiApi, text: String) -> BarSegment` doc for the shared behavior. +/// /// `segment(_: UiApi, text: String, options: Map) -> BarSegment` supports `fg`, `bg`, -/// `bold`, `italic`, `underline`, `dim`, and `target` keys to override styling and attach an -/// optional interaction target. `dim` is a boolean that renders the text with reduced -/// intensity for a muted appearance. +/// `bold`, `italic`, `underline`, `dim`, `blink`, and `target` keys to override styling and +/// attach an optional interaction target. +/// +/// `fg` is the foreground color and accepts a standard CSS color name, a hex code such as +/// `#ff0000`, or an RGB/RGBA string such as `rgb(255,0,0)` or `rgba(255,0,0,0.5)`. +/// `bg` is the background color and accepts the same color formats. +/// `bold`, `italic`, `underline`, `dim`, and `blink` are boolean `true`/`false` flags. +/// `dim` renders the text with reduced intensity for a muted appearance. +/// `blink` enables blinking text for that segment, but many modern terminal emulators ignore +/// or disable blink by default, so blinking text may not appear consistently. +/// `target` is an optional interaction target, usually either a string identifier such as +/// `"myTarget"` or a structured object such as `#{ type: "callback", id: "save" }`, +/// depending on the consumer API. +/// +/// Examples: +/// - `#{ fg: "#ff0000", bg: "rgba(0,0,0,0.5)", bold: true }` +/// - `#{ target: "myTarget" }` +/// - `#{ target: #{ type: "callback", id: "save" } }` +/// +/// Accessibility note: blinking text can be distracting and may trigger seizures for some +/// users. Use `blink` sparingly, prefer non-animated emphasis such as `dim` or color/weight +/// changes, follow WCAG guidance to avoid flashing content, and respect reduced-motion +/// preferences before enabling `blink`. fn segment(_: UiApi, text: string, options: map) -> BarSegment; /// Read an environment variable, if it is set. @@ -131,6 +156,11 @@ fn tabs(_: TreeApi, tabs: array) -> TreeSpec; /// Build a tabs container with an explicit active tab. fn tabs_with_active(_: TreeApi, tabs: array, active: int) -> TreeSpec; +/// Break the current node into a new tab or floating window. +/// `destination` accepts `tab` or `floating`. +/// Example: `action.break_current_node("floating")`. +fn break_current_node(_: ActionApi, destination: string) -> Action; + /// Cancel the active search. fn cancel_search(_: ActionApi) -> Action; @@ -155,6 +185,10 @@ fn close_node(_: ActionApi, node_id: int) -> Action; /// Close the currently focused view. fn close_view(_: ActionApi) -> Action; +/// Finalize the active search, keep the current match and cursor position, and leave search +/// mode with the committed result in place. +fn commit_search(_: ActionApi) -> Action; + /// Copy the current selection into the clipboard. fn copy_selection(_: ActionApi) -> Action; @@ -215,6 +249,11 @@ fn insert_tab_before(_: ActionApi, tabs_node_id: int, title: string, tree: TreeS /// Insert a tab before the current tab. fn insert_tab_before_current(_: ActionApi, title: string, tree: TreeSpec) -> Action; +/// Attach a buffer at the current node. +/// `placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. +/// Example: `action.join_buffer_here(12, "tab-after")`. +fn join_buffer_here(_: ActionApi, buffer_id: int, placement: string) -> Action; + /// Kill the currently focused buffer. fn kill_buffer(_: ActionApi) -> Action; @@ -241,6 +280,22 @@ fn move_buffer_to_floating(_: ActionApi, buffer_id: int, options: map) -> Action /// Move a buffer into a specific node. fn move_buffer_to_node(_: ActionApi, buffer_id: int, node_id: int) -> Action; +/// Move the current node after a sibling. +fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action; + +/// Move the current node before a sibling. +/// Use this when the current node is the one being repositioned. +/// Example: `action.move_current_node_before(42)`. +fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action; + +/// Move a node after a sibling. +/// Use this when you need to move a specific node id instead of the current node. +/// Example: `action.move_node_after(10, 42)`. +fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action; + +/// Move a node before a sibling. +fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action; + /// Select the next tab in the currently focused tabs node. fn next_current_tabs(_: ActionApi) -> Action; @@ -253,6 +308,11 @@ fn noop(_: ActionApi) -> Action; /// Emit a client notification. fn notify(_: ActionApi, level: string, message: string) -> Action; +/// Open the history of a buffer in a new view. +/// `scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. +/// Example: `action.open_buffer_history(12, "visible", "floating")`. +fn open_buffer_history(_: ActionApi, buffer_id: int, scope: string, placement: string) -> Action; + /// Open a floating view around the provided tree. fn open_floating(_: ActionApi, tree: TreeSpec, options: map) -> Action; @@ -346,12 +406,28 @@ fn send_keys_current(_: ActionApi, notation: string) -> Action; /// Split the current node and attach the provided tree as the new sibling. fn split_with(_: ActionApi, direction: string, tree: TreeSpec) -> Action; +/// Swap the current node with a sibling. +fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action; + /// Toggle a named input mode. fn toggle_mode(_: ActionApi, mode: string) -> Action; +/// Toggle zoom for the specified node id. +/// There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the +/// focused node and `toggle_zoom_node` when you already know the target id. +fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action; + +/// Clear the current session's active zoom state. +/// This removes the current session zoom rather than unwinding a stack of prior zooms. +fn unzoom_current_session(_: ActionApi) -> Action; + /// Copy the current selection into the clipboard. fn yank_selection(_: ActionApi) -> Action; +/// Zoom the session's currently focused node. +/// There is intentionally no separate `zoom_node(node_id)` helper in this API surface. +fn zoom_current_node(_: ActionApi) -> Action; + /// Return the active tab index. fn active_index(bar: TabBarContext) -> int; diff --git a/docs/config-api/registration-action.md b/docs/config-api/registration-action.md index 489a075..6dc76ed 100644 --- a/docs/config-api/registration-action.md +++ b/docs/config-api/registration-action.md @@ -2,6 +2,30 @@ ```Namespace: global``` +
+

fn break_current_node

+ +```rust,ignore +fn break_current_node(_: ActionApi, destination: "tab" | "floating") -> Action +``` + +
+
+ +
+ +
+Break the current node into a new tab or floating window. +`destination` accepts `tab` or `floating`. +Example: `action.break_current_node("floating")`. +
+ +
+
+

fn cancel_search

@@ -175,6 +199,29 @@ Description Close the currently focused view.
+ + +
+
+

fn commit_search

+ +```rust,ignore +fn commit_search(_: ActionApi) -> Action +``` + +
+
+ +
+ +
+Finalize the active search, keep the current match and cursor position, and leave search +mode with the committed result in place. +
+

@@ -581,6 +628,30 @@ Description Insert a tab before the current tab. + + +
+
+

fn join_buffer_here

+ +```rust,ignore +fn join_buffer_here(_: ActionApi, buffer_id: int, placement: "tab-after" | "tab-before" | "left" | "right" | "up" | "down") -> Action +``` + +
+
+ +
+ +
+Attach a buffer at the current node. +`placement` accepts `tab-after`, `tab-before`, `left`, `right`, `up`, or `down`. +Example: `action.join_buffer_here(12, "tab-after")`. +
+

@@ -706,6 +777,98 @@ Description Move a buffer into a specific node. + + +
+
+

fn move_current_node_after

+ +```rust,ignore +fn move_current_node_after(_: ActionApi, sibling_node_id: int) -> Action +``` + +
+
+ +
+ +
+Move the current node after a sibling. +
+ +
+
+
+
+

fn move_current_node_before

+ +```rust,ignore +fn move_current_node_before(_: ActionApi, sibling_node_id: int) -> Action +``` + +
+
+ +
+ +
+Move the current node before a sibling. +Use this when the current node is the one being repositioned. +Example: `action.move_current_node_before(42)`. +
+ +
+
+
+
+

fn move_node_after

+ +```rust,ignore +fn move_node_after(_: ActionApi, node_id: int, sibling_node_id: int) -> Action +``` + +
+
+ +
+ +
+Move a node after a sibling. +Use this when you need to move a specific node id instead of the current node. +Example: `action.move_node_after(10, 42)`. +
+ +
+
+
+
+

fn move_node_before

+ +```rust,ignore +fn move_node_before(_: ActionApi, node_id: int, sibling_node_id: int) -> Action +``` + +
+
+ +
+ +
+Move a node before a sibling. +
+

@@ -779,7 +942,7 @@ Build a no-op action.

fn notify

```rust,ignore -fn notify(_: ActionApi, level: String, message: String) -> Action +fn notify(_: ActionApi, level: "info" | "warn" | "error", message: String) -> Action ```
@@ -794,6 +957,30 @@ Description Emit a client notification.
+ + +
+
+

fn open_buffer_history

+ +```rust,ignore +fn open_buffer_history(_: ActionApi, buffer_id: int, scope: "visible" | "full", placement: "floating" | "tab") -> Action +``` + +
+
+ +
+ +
+Open the history of a buffer in a new view. +`scope` accepts `visible` or `full`. `placement` accepts `floating` or `tab`. +Example: `action.open_buffer_history(12, "visible", "floating")`. +
+

@@ -1353,7 +1540,7 @@ Send a key notation sequence to the focused buffer.

fn split_with

```rust,ignore -fn split_with(_: ActionApi, direction: String, tree: TreeSpec) -> Action +fn split_with(_: ActionApi, direction: "h" | "horizontal" | "v" | "vertical", tree: TreeSpec) -> Action ```
@@ -1368,6 +1555,28 @@ Description Split the current node and attach the provided tree as the new sibling.
+ + +
+
+

fn swap_current_node

+ +```rust,ignore +fn swap_current_node(_: ActionApi, sibling_node_id: int) -> Action +``` + +
+
+ +
+ +
+Swap the current node with a sibling. +
+

@@ -1390,6 +1599,53 @@ Description Toggle a named input mode. + + +
+
+

fn toggle_zoom_node

+ +```rust,ignore +fn toggle_zoom_node(_: ActionApi, node_id: int) -> Action +``` + +
+
+ +
+ +
+Toggle zoom for the specified node id. +There is intentionally no `toggle_zoom_current_node`; use `zoom_current_node` for the +focused node and `toggle_zoom_node` when you already know the target id. +
+ +
+
+
+
+

fn unzoom_current_session

+ +```rust,ignore +fn unzoom_current_session(_: ActionApi) -> Action +``` + +
+
+ +
+ +
+Clear the current session's active zoom state. +This removes the current session zoom rather than unwinding a stack of prior zooms. +
+

@@ -1415,3 +1671,26 @@ Copy the current selection into the clipboard.
+
+

fn zoom_current_node

+ +```rust,ignore +fn zoom_current_node(_: ActionApi) -> Action +``` + +
+
+ +
+ +
+Zoom the session's currently focused node. +There is intentionally no separate `zoom_node(node_id)` helper in this API surface. +
+ +
+
+
diff --git a/docs/config-api/registration-ui.md b/docs/config-api/registration-ui.md index f5f2b37..53058a8 100644 --- a/docs/config-api/registration-ui.md +++ b/docs/config-api/registration-ui.md @@ -45,6 +45,9 @@ Create a [`BarSegment`] from a [`UiApi`] receiver and text using default styling `segment(_: UiApi, text: String) -> BarSegment` produces plain text with default [`StyleSpec`] values and no click target. + +See the overloaded `segment(_: UiApi, text: String, options: Map) -> BarSegment` doc for +the full `options: Map` styling keys. diff --git a/docs/config-api/ui.md b/docs/config-api/ui.md index a6baf6e..3a4fe59 100644 --- a/docs/config-api/ui.md +++ b/docs/config-api/ui.md @@ -45,6 +45,9 @@ Create a [`BarSegment`] from a [`UiApi`] receiver and text using default styling `segment(_: UiApi, text: String) -> BarSegment` produces plain text with default [`StyleSpec`] values and no click target. + +See the overloaded `segment(_: UiApi, text: String, options: Map) -> BarSegment` doc for +the full `options: Map` styling keys. diff --git a/docs/input-routing-policy.md b/docs/input-routing-policy.md new file mode 100644 index 0000000..4e186cd --- /dev/null +++ b/docs/input-routing-policy.md @@ -0,0 +1,83 @@ +# Input routing policy + +This note defines how Embers decides whether input is handled locally or forwarded to the focused terminal buffer. + +## Routing ownership + +Input-routing policy currently lives in the client layer. + +- `ConfiguredClient` owns key, paste, mouse, and focus routing decisions +- the server receives terminal-bound bytes as `InputRequest::Send` +- the server/runtime path writes those bytes directly to the focused buffer runtime + +Today, routing is decided before the protocol request is sent. The server does not reinterpret keybindings after it receives `InputRequest::Send`. + +## Key routing decision tree + +For normal key events, `ConfiguredClient::handle_key` follows this order: + +1. If the client is in search mode, search-prompt handling wins. +2. Otherwise, the key is resolved against the current mode's bindings. +3. Exact matches execute configured actions. +4. Prefix matches stay pending and do not forward bytes yet. +5. Unmatched sequences follow the current mode's fallback policy: + - `Passthrough`: send the key sequence to the focused buffer + - `Ignore`: consume it locally without terminal output + +That means partial leader or prefix sequences must never leak into the terminal while they are still unresolved. + +## Modes and fallback + +Built-in fallback policy is: + +- `normal`: passthrough +- `copy`: ignore +- `search`: ignore +- `select`: ignore + +Changing modes clears any pending key sequence. Reloading config also clears pending input if the current mode still exists, or resets the client back to `normal` if it no longer does. + +## Prefix and leader behavior + +Leader bindings are expanded into ordinary key sequences during config compilation. At runtime there is no separate "leader state" object; the pending sequence in `InputState` is the source of truth. + +While a sequence is still a prefix: + +- no bytes are sent to the buffer +- no local fallback runs yet +- the client waits for the next key to decide whether the sequence resolves or falls back + +## Local actions vs terminal passthrough + +Most exact matches execute locally, but Embers intentionally avoids stealing some keys from terminal apps. + +Configured bindings are forwarded to the terminal instead of executing locally when: + +- the focused view is in alternate screen and the bound actions are all local search/select/scroll actions, or +- the client is in normal mode with `follow_output` enabled, has no active search or selection state, and the bound actions require local search/select context + +This keeps fullscreen terminal apps from losing keys that would otherwise be meaningful inside the application. + +## Script-generated terminal input + +Scripted actions fit into the same pipeline after binding resolution: + +- `Action::SendKeys` converts notation into bytes +- `Action::SendBytes` uses the provided bytes directly +- both resolve a target buffer (`current` or explicit) +- both send `InputRequest::Send` + +So scripted terminal input uses the same protocol/runtime path as unmapped passthrough keys. + +## Special input paths + +- paste uses `handle_paste`; if the focused buffer has bracketed paste enabled, Embers wraps the payload in `ESC [ 200~` / `ESC [ 201~` +- focus events use `handle_focus_event`; they forward `ESC [ I` / `ESC [ O` only when the program requested focus reporting +- mouse events use `handle_mouse`; they either drive local scroll/focus behavior or send encoded mouse bytes when mouse reporting is enabled + +## Code anchors + +- Input resolution: `crates/embers-client/src/input/keymap.rs` +- Mode state and fallback policy: `crates/embers-client/src/input/modes.rs` +- Live routing and scripted action delivery: `crates/embers-client/src/configured_client.rs` +- Protocol input dispatch: `crates/embers-server/src/server.rs` diff --git a/docs/input-routing.md b/docs/input-routing.md new file mode 100644 index 0000000..7fdce3e --- /dev/null +++ b/docs/input-routing.md @@ -0,0 +1,34 @@ +# Input routing + +This is the short-form map of how Embers routes terminal input today. + +The detailed contract and rationale live in `docs/input-routing-policy.md`. This file exists as the +plan-facing entry point for the same behavior. + +## Summary + +- `ConfiguredClient` decides whether a key is handled locally or passed through to the focused + buffer. +- Leader and prefix sequences stay client-local until they resolve or are cleared. +- Copy/select/search modes suppress passthrough for the bindings they own. +- Scripted `send_keys*` and `send_bytes*` actions bypass local rendering logic and write bytes to + the target buffer runtime. +- Hidden or detached buffers do not receive arbitrary typed input unless a script or explicit + command targets them directly. + +## Current boundaries + +- Terminal-facing input ultimately lands in the buffer runtime, not in layout state. +- The `RawByteRouter` remains the explicit seam for raw-byte policy, but normal live PTY input is + currently decided earlier by the configured client and keymap. +- Config reload clears pending prefixes before the next key is interpreted under the new bindings. + +## Primary regression coverage + +- `crates/embers-client/tests/configured_client.rs` + - prefix no-leak behavior + - copy-mode passthrough suppression + - scripted `send_keys` / `send_bytes` + - reload clearing pending prefixes + +For the full policy text, see `docs/input-routing-policy.md`. diff --git a/docs/render-source-contract.md b/docs/render-source-contract.md new file mode 100644 index 0000000..ff06765 --- /dev/null +++ b/docs/render-source-contract.md @@ -0,0 +1,29 @@ +## Render-source contract + +Phase 8 locks down which server surfaces the client uses for terminal rendering. + +### Authoritative sources + +The server remains authoritative for both layout and terminal state. + +- `SessionSnapshot` provides layout topology plus durable buffer metadata such as title, activity, attachment, and PTY size. +- `VisibleSnapshotResponse` provides the current visible terminal surface for one buffer, including visible lines, cursor state, viewport position, alternate-screen mode, and other terminal-mode flags. +- Full capture and scrollback slices stay on-demand APIs and are not part of the normal render loop. + +The client does not consume terminal diffs. It renders from full visible snapshots, with `RenderInvalidated` acting as a hint that a buffer should be refreshed before the next user-visible render. + +### Freshness expectations + +`RenderInvalidated` means the visible snapshot for that buffer may be stale. The client refreshes invalidated visible leaves (leaf nodes in the layout tree corresponding to visible buffers) before rendering and then updates the display so updated titles, alternate-screen flags, and visible lines are used together. + +For event handling, the client also refreshes the affected `BufferRecord` before dispatching `RenderInvalidated` hooks. That keeps metadata-only consumers such as bell automation aligned with the latest server state. + +### Metadata synchronization + +Visible snapshots may carry title and mode changes that affect UI immediately. Buffer metadata still lives on the durable `BufferRecord`, so activity, bell state, and detached-buffer discovery remain queryable even when a buffer is hidden. + +Hidden buffers do not eagerly fetch fresh visible snapshots just because they were invalidated. Their visible state is refreshed when they become visible or when a caller explicitly requests capture. Their metadata, however, continues to flow through buffer/session refresh paths. + +### Detached buffers + +Detached buffers are discovered through `BufferRequest::List` / `Get`, and their visible surface is queried explicitly through the same capture endpoints as attached buffers. That keeps detached previews and background metadata within the same contract as attached terminal runtimes. diff --git a/docs/terminal-backend-boundary.md b/docs/terminal-backend-boundary.md new file mode 100644 index 0000000..c6835b2 --- /dev/null +++ b/docs/terminal-backend-boundary.md @@ -0,0 +1,83 @@ +# Terminal backend boundary + +This note defines the PTY-to-render pipeline boundary Embers relies on today. + +## Pipeline + +The active PTY pipeline is: + +```text +PTY bytes + -> RawByteRouter + -> TerminalBackend + -> snapshot / metadata / damage + -> protocol responses and render invalidation +``` + +For live PTY buffers, that pipeline runs inside the runtime keeper (`KeeperSurface` in `crates/embers-server/src/buffer_runtime.rs`). The server process talks to the keeper over the runtime socket and does not own a second terminal parser. + +## Raw routing seam + +`RawByteRouter` is the only layer allowed to inspect or rewrite raw terminal bytes before they hit the backend. + +Today it is intentionally minimal: + +- input routing is passthrough +- output routing forwards bytes directly to the backend + +That seam exists so future work can add: + +- protocol-aware passthrough decisions +- special-case interception +- metadata extraction that must happen before backend ingestion + +The router should not own terminal screen state, scrollback, or view/layout concerns. + +## Backend ownership + +`TerminalBackend` owns terminal emulation state: + +- ANSI/terminal parsing +- primary/alternate screen state +- cursor state +- scrollback capture +- visible snapshot generation +- damage tracking +- terminal mode reporting (alternate screen, mouse, focus, bracketed paste) + +Backends must be able to: + +- ingest output bytes +- resize +- produce a visible snapshot +- produce full capture / scrollback slices +- surface metadata +- surface one-shot activity and damage signals + +## Metadata outside the backend + +The server still owns buffer-level metadata records: + +- buffer title field mirrored onto the `Buffer` +- activity/bell state mirrored onto the `Buffer` +- last snapshot sequence on the `Buffer` +- render invalidation events + +The backend reports the current terminal metadata; the server persists the last observed values onto the durable buffer record. + +## Alternate-screen policy + +Alternate-screen state is owned by the backend. + +- `visible_snapshot` always reflects the currently active screen +- the `alternate_screen` mode bit tells clients whether the visible snapshot is from the alternate buffer +- when alternate screen exits, the visible snapshot returns to the primary screen state managed by the backend +- full capture and scrollback are backend-defined state, not layout-defined state + +In practice, Embers currently inherits alternate-screen capture semantics from the alacritty backend implementation, and the tests lock that behavior down at the backend boundary. + +## Code anchors + +- Raw routing + backend trait: `crates/embers-server/src/terminal_backend.rs` +- Keeper-owned surface: `crates/embers-server/src/buffer_runtime.rs` +- Server/runtime wiring: `crates/embers-server/src/server.rs` diff --git a/docs/terminal-capture-model.md b/docs/terminal-capture-model.md new file mode 100644 index 0000000..4eaa09b --- /dev/null +++ b/docs/terminal-capture-model.md @@ -0,0 +1,108 @@ +# Terminal capture model + +This note defines the terminal snapshot and capture semantics Embers exposes from PTY-backed buffers. + +## Terminology + +- durable buffer runtime - the long-lived PTY runtime owned by `BufferRuntimeHandle`, including its + runtime keeper process and the terminal state it preserves for a `Buffer` +- backend - the `TerminalBackend` implementation used by that runtime keeper to store and expose + visible state, full capture, and scrollback + +## Capture surfaces + +Embers exposes three related but distinct capture surfaces for PTY buffers: + +- `capture_snapshot`: the full backend-defined capture for the buffer +- `capture_visible_snapshot`: the renderer-facing view of the currently visible screen +- `capture_scrollback_slice`: a paged read over the backend-defined scrollback history + +All three are sourced from the durable buffer runtime (`BufferRuntimeHandle` -> runtime keeper -> `TerminalBackend`), not from layout state. + +## Full snapshot semantics + +`capture_snapshot` is the "capture pane/buffer" source of truth for PTY buffers. + +It returns: + +- the current snapshot sequence +- the buffer's current PTY size +- the backend's full captured lines +- the terminal title if the backend has one +- the buffer cwd tracked by the server + +For PTY buffers, this is a runtime capture, not a view capture. Moving, detaching, or reattaching the buffer does not change which terminal state is returned. + +## Visible snapshot semantics + +`capture_visible_snapshot` is the authoritative render input for a PTY buffer. + +It returns: + +- the current visible lines from the active screen +- viewport position and total line count +- terminal mode bits such as alternate screen, mouse reporting, focus reporting, and bracketed paste +- cursor metadata +- size, sequence, title, and cwd + +The visible snapshot always reflects the screen the backend considers active at the moment of capture. + +## Scrollback semantics + +`capture_scrollback_slice` pages through backend-defined history without changing visible state. + +It returns: + +- `start_line`: the effective start of the returned slice +- `total_lines`: the full scrollback length at capture time +- `lines`: the requested window into that history + +Repeated reads without new output should be stable: the same buffer state should yield the same full snapshot, visible snapshot, and scrollback slice. + +## Detached and exited buffers + +Detached PTY buffers remain capturable. + +While detached: + +- full capture remains available +- visible snapshot remains available +- scrollback slices remain available +- title and other backend metadata remain available +- the most recent PTY size remains the size reported by capture APIs until another resize arrives + +Exited PTY buffers also remain capturable as long as the buffer record still exists. Writes, +resizes, and kills must fail after exit, but capture reads continue to work. In practice callers +should expect an error result rather than a silent no-op: writes and kills surface a conflict such +as `buffer 12 has already exited`, while resize requests can fail with `buffer runtime has already +exited`. + +## Alternate-screen policy + +Alternate-screen ownership stays in the backend. + +- `capture_visible_snapshot` reflects the active screen and reports whether alternate screen is active +- full capture and scrollback semantics follow the backend's own history model +- leaving alternate screen returns visible capture to the primary screen state the backend preserved + +## Testing notes + +Embers locks down the observable behavior with tests rather than layering extra server-side +alternate-screen state on top of the backend. + +## Resize behavior + +Resize updates the PTY and the keeper-owned backend surface. Future full and visible captures report the latest size and remain coherent across: + +- direct resize requests +- detach while preserving the retained size +- reattach into differently sized views once a new resize is applied + +## Code anchors + +As of 2026-04-16: + +- Runtime capture implementation: `crates/embers-server/src/server.rs` +- Runtime keeper capture surface: `crates/embers-server/src/buffer_runtime.rs` +- Backend-visible snapshot behavior: `crates/embers-server/src/terminal_backend.rs` +- PTY integration coverage: `crates/embers-test-support/tests/buffer_runtime.rs` diff --git a/docs/terminal-runtime.md b/docs/terminal-runtime.md new file mode 100644 index 0000000..0d3e7b5 --- /dev/null +++ b/docs/terminal-runtime.md @@ -0,0 +1,94 @@ +# Terminal runtime contract + +This note locks down the runtime contract Embers uses for PTY-backed buffers. + +## Runtime ownership + +`Buffer` is the durable terminal runtime record. A PTY-backed `Buffer` owns: + +- the PTY/process command, cwd, and environment hints +- runtime identity and lifecycle state (`Created`, `Running`, `Interrupted`, `Exited`) +- the runtime keeper socket path used to reconnect after restore +- attachment state (`Attached(NodeId)` or `Detached`) +- the authoritative PTY size policy +- terminal-facing metadata surfaced outside layout code: + - title + - activity/bell state + - last snapshot sequence +- the snapshot source of truth via the buffer runtime/keeper backend + +`BufferView` is a renderer/layout attachment only. It owns: + +- session/layout placement (`NodeId`, parentage, split/tab membership) +- focus/zoom/follow-output view flags +- the last render size used by that view + +`BufferView` does not own PTY handles, process lifetime, terminal parsing state, or scrollback. + +## Closing a view vs killing a buffer + +Closing a view removes the `BufferView` node and transitions the buffer to `Detached`. + +- the PTY/process keeps running +- output continues accumulating +- snapshots and scrollback remain queryable +- title/activity metadata remains on the buffer record + +Killing a buffer targets the runtime, not the view tree. + +- the PTY child is terminated +- the buffer transitions to `Exited` +- if the buffer was still attached, it remains `Attached(NodeId)` until that view is later closed or replaced +- capture remains available from the terminal backend snapshot state +- the buffer record may later be cleaned up once it is detached + +## Attachment and move semantics + +`Attached -> Detached` happens when the owning view is closed or explicitly detached. + +`Detached -> Attached` happens when the buffer is attached to a new leaf. + +A "move" is a composite operation: + +1. detach or close the old view +2. keep the buffer runtime alive +3. attach the same buffer to the target leaf + +Moving tabs, leaves, or floating roots must never reset PTY state because the runtime stays with the `Buffer`, not the `BufferView`. + +## Detached buffer policy + +Detached PTY buffers keep the last assigned `pty_size` until another explicit resize arrives. + +While detached: + +- output continues to accumulate +- full and visible snapshots remain available +- scrollback remains available +- activity/bell state continues to update on the buffer record + +Reattaching a detached buffer does not recreate the process or reset terminal state. It only gives the durable runtime a new view attachment. + +## Runtime lifecycle transitions + +The intended transition graph is: + +- `Created -> Running` when the runtime keeper is spawned or restored +- `Running -> Exited` when the child process terminates normally or is explicitly killed +- `Running/Created -> Interrupted` when a restore cannot reconnect to a keeper +- `Exited -> cleaned up` when a detached exited buffer is removed from server state + +## Attachment transitions + +- `Attached(NodeId) -> Detached` when a view closes or the buffer is explicitly detached +- `Detached -> Attached(NodeId)` when the buffer is attached to a leaf + +The composite "move" operation is `Attached(NodeId) -> Detached -> Attached(NodeId)` while the +same runtime stays alive. + +## Code anchors + +- Runtime model: `crates/embers-server/src/model.rs` +- State transitions: `crates/embers-server/src/state.rs` +- Runtime keeper and PTY lifecycle: `crates/embers-server/src/buffer_runtime.rs` +- Server/runtime wiring: `crates/embers-server/src/server.rs` diff --git a/docs/terminal-test-matrix.md b/docs/terminal-test-matrix.md new file mode 100644 index 0000000..e86d120 --- /dev/null +++ b/docs/terminal-test-matrix.md @@ -0,0 +1,43 @@ +# Terminal test matrix + +This matrix maps the terminal-validation plan to the concrete regression suites that now guard the +behavior. + +## Runtime and backend contracts + +| Concern | Primary tests | Notes | +| --- | --- | --- | +| Buffer runtime ownership and PTY lifecycle | `crates/embers-test-support/tests/buffer_runtime.rs` | Locks down runtime state transitions and detached-buffer policy. | +| Backend boundary and capture semantics | `crates/embers-server/tests/backend.rs` and `crates/embers-server/tests/buffer_lifecycle.rs` | Verifies backend ownership, activity bookkeeping, and server-side lifecycle rules. | +| Byte-stream features and alternate-screen parsing | `crates/embers-server/tests/backend.rs` and `crates/embers-client/tests/e2e.rs` | Current coverage lives in the server/backend regression tests plus the end-to-end alternate-screen client checks. | + +## Client and render-source contracts + +| Concern | Primary tests | Notes | +| --- | --- | --- | +| Input routing and scripted actions | `crates/embers-client/tests/configured_client.rs` | Prefix handling, passthrough rules, scripted `send_keys` / `send_bytes`, reload behavior. | +| Activity, bell, and hidden-buffer metadata | `crates/embers-client/tests/e2e.rs` and `crates/embers-server/tests/buffer_lifecycle.rs` | Hidden and detached buffers keep activity, bell, and continuity metadata coherent. | +| Render invalidation and snapshot freshness | `crates/embers-client/tests/configured_client.rs` and `crates/embers-client/tests/e2e.rs` | Confirms the client renders from refreshed authoritative snapshots, not stale cache. | +| Full-screen and alternate-screen behavior | `crates/embers-client/tests/e2e.rs` | Verifies enter/exit semantics, hidden fullscreen buffers, and primary-screen restoration. | + +## Real PTY end-to-end workflows + +| Workflow | Primary tests | What it proves | +| --- | --- | --- | +| Spawn real client in PTY and run shell I/O | `crates/embers-cli/tests/interactive.rs::embers_without_subcommand_starts_server_and_client` | Embers can host a real shell and stay reachable through both the live client and CLI. | +| Attach, switch, detach, and reveal live clients | `crates/embers-cli/tests/interactive.rs` | Live PTY clients stay synchronized with server session targeting. | +| Local scrollback and OSC52 selection | `crates/embers-cli/tests/interactive.rs` | Client-local terminal UX features work under a real PTY. | +| Scripted input path in a live PTY client | `crates/embers-cli/tests/interactive.rs::scripted_input_bindings_reach_the_live_terminal_in_pty` | Scripted bindings still reach the focused terminal runtime end to end. | +| Config reload during terminal interaction | `crates/embers-cli/tests/interactive.rs::config_reload_updates_live_bindings_without_breaking_terminal_io` | Reloaded bindings take effect without breaking ongoing terminal input/output. | +| Split, move, detach, and reattach continuity | `crates/embers-cli/tests/interactive.rs::live_pty_client_preserves_buffers_across_layout_and_attachment_changes` | Layout and attachment changes do not reset the underlying shell process. | +| Hidden-buffer bell visibility and reveal continuity | `crates/embers-cli/tests/interactive.rs::hidden_buffer_bells_surface_in_the_attached_client_and_reveal_buffered_output` | Hidden buffers keep accumulating output, surface bell state, and reveal coherent content later. | +| Full-screen app entry and exit in the live client | `crates/embers-cli/tests/interactive.rs::fullscreen_terminal_transitions_render_in_the_live_client_pty` | The attached PTY client renders alternate-screen transitions the same way the backend models them. | + +## Harness notes + +- `crates/embers-test-support/src/pty.rs` provides the reusable PTY harness used by the CLI + integration tests. +- PTY read helpers include the recent output tail in timeout errors so failures are debuggable + without rerunning under a separate recorder. +- `crates/embers-cli/tests/interactive.rs` serializes PTY-heavy tests with a shared lock to reduce + flaky PTY pressure in CI. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..d973ae5 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1776255774, + "narHash": "sha256-psVTpH6PK3q1htMJpmdz1hLF5pQgEshu7gQWgKO6t6Y=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "566acc07c54dc807f91625bb286cb9b321b5f42a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5eec9ce --- /dev/null +++ b/flake.nix @@ -0,0 +1,45 @@ +{ + description = "Embers development shell with pinned flatc"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + outputs = { nixpkgs, ... }: + let + systems = [ + "aarch64-darwin" + "x86_64-darwin" + "aarch64-linux" + "x86_64-linux" + ]; + + forAllSystems = f: + nixpkgs.lib.genAttrs systems (system: + f (import nixpkgs { inherit system; })); + in + { + packages = forAllSystems (pkgs: { + flatc = pkgs.flatbuffers; + default = pkgs.flatbuffers; + }); + + apps = forAllSystems (pkgs: { + flatc = { + type = "app"; + program = "${pkgs.flatbuffers}/bin/flatc"; + }; + default = { + type = "app"; + program = "${pkgs.flatbuffers}/bin/flatc"; + }; + }); + + devShells = forAllSystems (pkgs: { + default = pkgs.mkShell { + packages = [ + pkgs.flatbuffers + pkgs.mdbook + ]; + }; + }); + }; +}