From 61216c10449cf3497003c8026940620c5a0f3115 Mon Sep 17 00:00:00 2001 From: senstella Date: Thu, 9 Oct 2025 00:41:58 +0900 Subject: [PATCH] sort of a gui --- Cargo.lock | 156 ++++++++++++++++++++- Cargo.toml | 2 +- src/fossil.rs | 4 +- src/gui.rs | 201 ++++++++------------------- src/main.rs | 20 ++- src/matcher.rs | 34 +++-- src/mcp.rs | 368 ++----------------------------------------------- src/prompt.txt | 340 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 594 insertions(+), 531 deletions(-) create mode 100644 src/prompt.txt diff --git a/Cargo.lock b/Cargo.lock index e2a5797..e6214c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,6 +377,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -890,6 +899,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "detect-desktop-environment" version = "0.2.0" @@ -1676,6 +1694,7 @@ checksum = "88acfabc84ec077eaf9ede3457ffa3a104626d79022a9bf7f296093b1d60c73f" dependencies = [ "iced_core", "iced_futures", + "iced_highlighter", "iced_renderer", "iced_widget", "iced_winit", @@ -1775,6 +1794,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "iced_highlighter" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad88b25a1328cd4bb0b72d8e20f8207c0433649dc788f67e911423b9406f45c" +dependencies = [ + "iced_core", + "once_cell", + "syntect", +] + [[package]] name = "iced_renderer" version = "0.13.0" @@ -1844,6 +1874,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81429e1b950b0e4bca65be4c4278fea6678ea782030a411778f26fa9f8983e1d" dependencies = [ + "iced_highlighter", "iced_renderer", "iced_runtime", "num-traits", @@ -2052,6 +2083,12 @@ dependencies = [ "redox_syscall 0.5.18", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -2333,6 +2370,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-format" version = "0.4.4" @@ -2622,6 +2665,28 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.9.4", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "orbclient" version = "0.3.48" @@ -2841,6 +2906,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml 0.38.3", + "serde", + "time", +] + [[package]] name = "png" version = "0.17.16" @@ -2868,6 +2946,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2916,6 +3000,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.41" @@ -3668,6 +3761,27 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.17", + "walkdir", + "yaml-rust", +] + [[package]] name = "sys-locale" version = "0.3.2" @@ -3748,6 +3862,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -4307,7 +4452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.37.5", "quote", ] @@ -4885,6 +5030,15 @@ version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yazi" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 3207b39..eb11bd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] anyhow = "1.0.100" -iced = { version = "0.13.1", features = ["tokio"] } +iced = { version = "0.13.1", features = ["tokio", "highlighter"] } iced_aw = { version = "0.11.0", features = ["tab_bar"] } imara-diff = "0.2.0" nucleo-matcher = "0.3.1" diff --git a/src/fossil.rs b/src/fossil.rs index 6700709..9f8f32d 100644 --- a/src/fossil.rs +++ b/src/fossil.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::sync::{Arc, Mutex}; use imara_diff::{Algorithm, BasicLineDiffPrinter, Diff, InternedInput, UnifiedDiffConfig}; @@ -65,7 +65,7 @@ impl Fossil { #[derive(Debug, Clone, Default)] pub struct FossilManager { - pub fossils: Arc>>, + pub fossils: Arc>>, } impl FossilManager { diff --git a/src/gui.rs b/src/gui.rs index 982eebe..0d98455 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,115 +1,86 @@ +use iced::highlighter; +use std::{collections::HashMap, path::Path, time::Duration}; + use crate::fossil::FossilManager; use iced::{ - Element, + Element, Subscription, time, widget::{button, column, row, text, text_editor}, }; use iced_aw::{TabLabel, Tabs}; -use std::collections::HashMap; -#[derive(Default)] pub struct State { fossil_manager: FossilManager, - tab_editors: HashMap, active_tab: Option, - tab_order: Vec, + theme: highlighter::Theme, + editor_contents: HashMap, } #[derive(Debug, Clone)] pub enum Message { - Edit(String, text_editor::Action), TabSelected(String), - CreateFossil, - SyncFossil(String), - DeleteFossil(String), - RefreshFromManager, + CheckForChanges, } impl State { pub fn new() -> Self { - Self::default() + Self { + fossil_manager: FossilManager::default(), + active_tab: None, + editor_contents: HashMap::new(), + theme: highlighter::Theme::SolarizedDark, + } } pub fn with_shared_manager(fossil_manager: FossilManager) -> Self { - Self { + let mut state = Self { fossil_manager, - tab_editors: HashMap::new(), active_tab: None, - tab_order: Vec::new(), - } + editor_contents: HashMap::new(), + theme: highlighter::Theme::SolarizedDark, + }; + state.sync_editor_contents(); + state } - pub fn add_fossil(&mut self, id: String, content: String) { - // Add to fossil manager - { - let mut fossils = self.fossil_manager.fossils.lock().unwrap(); - fossils.insert(id.clone(), crate::fossil::Fossil::new(content.clone())); - } - - // Add editor tab - self.tab_editors - .insert(id.clone(), text_editor::Content::with_text(&content)); - self.tab_order.push(id.clone()); - self.active_tab = Some(id); - } - - pub fn sync_fossil(&mut self, id: &str) { - if let Some(editor_content) = self.tab_editors.get(id) { - let content = editor_content.text(); - let mut fossils = self.fossil_manager.fossils.lock().unwrap(); - if let Some(fossil) = fossils.get_mut(id) { - fossil.commit(content); - } - } - } - - pub fn load_fossil_to_editor(&mut self, id: &str) { + fn sync_editor_contents(&mut self) { let fossils = self.fossil_manager.fossils.lock().unwrap(); - if let Some(fossil) = fossils.get(id) { - if let Some(latest_content) = fossil.latest() { - self.tab_editors.insert( - id.to_string(), - text_editor::Content::with_text(latest_content), - ); - } + self.editor_contents.clear(); + + for (id, fossil) in &*fossils { + self.editor_contents.insert( + id.clone(), + text_editor::Content::with_text(&fossil.latest().unwrap()), + ); } } - pub fn refresh_from_manager(&mut self) { - let fossils = self.fossil_manager.fossils.lock().unwrap(); - let fossil_ids: Vec = fossils.keys().cloned().collect(); - drop(fossils); - - // Add new fossils that don't exist in GUI - for id in &fossil_ids { - if !self.tab_editors.contains_key(id) { - self.load_fossil_to_editor(id); - self.tab_order.push(id.clone()); - } - } - - // Remove fossils that no longer exist in manager - let gui_fossil_ids: Vec = self.tab_editors.keys().cloned().collect(); - for id in gui_fossil_ids { - if !fossil_ids.contains(&id) { - self.tab_editors.remove(&id); - self.tab_order.retain(|x| x != &id); - if self.active_tab.as_ref() == Some(&id) { - self.active_tab = self.tab_order.first().cloned(); - } - } - } - - // Update active tab if none is selected but tabs exist - if self.active_tab.is_none() && !self.tab_order.is_empty() { - self.active_tab = Some(self.tab_order[0].clone()); - } + pub fn subscription(&self) -> Subscription { + time::every(Duration::from_millis(100)).map(|_| Message::CheckForChanges) } } +fn tab_content<'a>(state: &'a State, id: &str) -> Element<'a, Message> { + state + .editor_contents + .get(id) + .map(|content| { + text_editor(content) + .highlight( + Path::new(id) + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or("py"), + state.theme, + ) + .into() + }) + .unwrap_or_else(|| text("No content").into()) +} + pub fn view(state: &State) -> Element<'_, Message> { let mut tabs = Tabs::new(Message::TabSelected); - - for id in &state.tab_order { + let fossils = state.fossil_manager.fossils.lock().unwrap(); + for (id, _) in &*fossils { tabs = tabs.push( id.clone(), TabLabel::Text(id.clone()), @@ -118,82 +89,24 @@ pub fn view(state: &State) -> Element<'_, Message> { } let tabs = if let Some(active_id) = &state.active_tab { - tabs.set_active_tab(active_id) + if fossils.contains_key(active_id) { + tabs.set_active_tab(active_id) + } else { + tabs + } } else { tabs }; - - let controls = row![ - button("New Fossil").on_press(Message::CreateFossil), - button("Sync Active").on_press_maybe( - state - .active_tab - .as_ref() - .map(|id| Message::SyncFossil(id.clone())) - ), - button("Delete Active").on_press_maybe( - state - .active_tab - .as_ref() - .map(|id| Message::DeleteFossil(id.clone())) - ), - button("Refresh").on_press(Message::RefreshFromManager), - ] - .spacing(10) - .padding(10); - - column![controls, tabs].spacing(10).padding(10).into() -} - -fn tab_content<'a>(state: &'a State, fossil_id: &str) -> Element<'a, Message> { - if let Some(content) = state.tab_editors.get(fossil_id) { - text_editor(content) - .placeholder("Edit your fossil content here...") - .on_action({ - let fossil_id = fossil_id.to_string(); - move |action| Message::Edit(fossil_id.clone(), action) - }) - .into() - } else { - text("No content available").into() - } + column![tabs].spacing(10).padding(10).into() } pub fn update(state: &mut State, message: Message) -> iced::Task { match message { - Message::Edit(fossil_id, action) => { - if let Some(content) = state.tab_editors.get_mut(&fossil_id) { - content.perform(action); - } - } Message::TabSelected(tab_id) => { state.active_tab = Some(tab_id); } - Message::CreateFossil => { - let new_id = format!("fossil_{}", state.tab_order.len() + 1); - state.add_fossil(new_id, String::new()); - } - Message::SyncFossil(fossil_id) => { - state.sync_fossil(&fossil_id); - } - Message::DeleteFossil(fossil_id) => { - // Remove from fossil manager - { - let mut fossils = state.fossil_manager.fossils.lock().unwrap(); - fossils.remove(&fossil_id); - } - - // Remove from GUI state - state.tab_editors.remove(&fossil_id); - state.tab_order.retain(|id| id != &fossil_id); - - // Update active tab - if state.active_tab.as_ref() == Some(&fossil_id) { - state.active_tab = state.tab_order.first().cloned(); - } - } - Message::RefreshFromManager => { - state.refresh_from_manager(); + Message::CheckForChanges => { + state.sync_editor_contents(); } } iced::Task::none() diff --git a/src/main.rs b/src/main.rs index c591679..d33674b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use crate::{ - gui::{update, view, State}, - mcp::FossilEditor, fossil::FossilManager, + gui::{State, update, view}, + mcp::FossilMCP, }; use iced::Task; use rmcp::{ServiceExt, transport::stdio}; @@ -15,7 +15,7 @@ mod mcp; fn main() -> iced::Result { tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into())) + .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::WARN.into())) .with_writer(std::io::stderr) .with_ansi(false) .init(); @@ -23,11 +23,11 @@ fn main() -> iced::Result { let rt = Runtime::new().unwrap(); // Create shared fossil manager - let shared_fossil_manager = FossilManager::new(); + let fossil_manager = FossilManager::new(); - let editor = FossilEditor::with_shared_state(shared_fossil_manager.clone()); + let mcp = FossilMCP::with_shared_state(fossil_manager.clone()); rt.spawn(async { - let service = editor + let service = mcp .serve(stdio()) .await .inspect_err(|e| { @@ -39,11 +39,7 @@ fn main() -> iced::Result { }); iced::application("Fossil Editor", update, view) + .subscription(State::subscription) .default_font(iced::Font::MONOSPACE) - .run_with(move || { - let mut state = State::with_shared_manager(shared_fossil_manager); - // Add a sample fossil to start with - state.add_fossil("sample.py".to_string(), "def hello():\n print('Hello, World!')".to_string()); - (state, Task::none()) - }) + .run_with(move || (State::with_shared_manager(fossil_manager), Task::none())) } diff --git a/src/matcher.rs b/src/matcher.rs index f33c521..befd893 100644 --- a/src/matcher.rs +++ b/src/matcher.rs @@ -1,4 +1,4 @@ -use seal::pair::{AlignmentSet, InMemoryAlignmentMatrix, SmithWaterman, Step}; +use seal::pair::{AlignmentSet, MemoryMappedAlignmentMatrix, SmithWaterman, Step}; pub fn match_lines(code: &str, start: &str, end: &str) -> Option<(usize, usize)> { let code_chars: Vec = code.chars().collect(); @@ -11,7 +11,7 @@ pub fn match_lines(code: &str, start: &str, end: &str) -> Option<(usize, usize)> if start.ends_with(end) { let pattern_chars: Vec = start.chars().collect(); - let set: AlignmentSet = + let set: AlignmentSet = AlignmentSet::new(code_chars.len(), pattern_chars.len(), strategy, |x, y| { code_chars[x] == pattern_chars[y] }) @@ -39,7 +39,7 @@ pub fn match_lines(code: &str, start: &str, end: &str) -> Option<(usize, usize)> Some((start_line, end_line)) } else { let start_chars: Vec = start.chars().collect(); - let start_set: AlignmentSet = AlignmentSet::new( + let start_set: AlignmentSet = AlignmentSet::new( code_chars.len(), start_chars.len(), strategy.clone(), @@ -61,27 +61,31 @@ pub fn match_lines(code: &str, start: &str, end: &str) -> Option<(usize, usize)> let end_chars: Vec = end.chars().collect(); let remaining_code: Vec = code_chars[start_char_pos..].to_vec(); - let end_set: AlignmentSet = + let end_set: AlignmentSet = AlignmentSet::new(remaining_code.len(), end_chars.len(), strategy, |x, y| { remaining_code[x] == end_chars[y] }) .ok()?; - let end_alignment = end_set.local_alignments().next().unwrap(); - let end_char_pos_relative = end_alignment - .steps() - .filter_map(|step| { - if let Step::Align { x, .. } = step { - Some(x) - } else { - None - } + let end_char_pos_relative = end_set + .local_alignments() + .filter_map(|end_alignment| { + end_alignment + .steps() + .filter_map(|step| { + if let Step::Align { x, .. } = step { + Some(x) + } else { + None + } + }) + .last() }) - .last()?; + .min()?; let end_char_pos = start_char_pos + end_char_pos_relative; - let start_line = code[..start_char_pos].lines().count().saturating_sub(1); + let start_line = code[..=start_char_pos].lines().count().saturating_sub(1); let end_line = code[..=end_char_pos].lines().count().saturating_sub(1); Some((start_line, end_line)) diff --git a/src/mcp.rs b/src/mcp.rs index 137badb..73ee8ec 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -34,11 +34,11 @@ pub struct FuzzyEditRequest { #[schemars(description = "The unique identifier for the fossil to edit.")] pub id: String, #[schemars( - description = "The start of the text to replace. Please try your best to provide the whole line. Do not attempt to mess with it since it will make it worse." + description = "The start of the text to replace. Please try your best to provide the whole line." )] pub start_pattern: String, #[schemars( - description = "The end of the text to replace. Please try your best to provide the whole line. Do not attempt to mess with it since it will make it worse." + description = "The end of the text to replace. Please try your best to provide the whole line." )] pub end_pattern: String, #[schemars( @@ -66,20 +66,13 @@ pub struct GetVersionRequest { } #[derive(Debug, Clone)] -pub struct FossilEditor { +pub struct FossilMCP { tool_router: ToolRouter, state: FossilManager, } #[tool_router] -impl FossilEditor { - pub fn new() -> Self { - Self { - tool_router: Self::tool_router(), - state: FossilManager::new(), - } - } - +impl FossilMCP { pub fn with_shared_state(state: FossilManager) -> Self { Self { tool_router: Self::tool_router(), @@ -207,7 +200,7 @@ impl FossilEditor { let lines: Vec<&str> = current_code.lines().collect(); let mut new_code_lines = Vec::new(); - new_code_lines.extend_from_slice(&lines[..start_line + 1]); + new_code_lines.extend_from_slice(&lines[..start_line]); new_code_lines.push(&req.replacement); new_code_lines.extend_from_slice(&lines[end_line + 1..]); @@ -273,7 +266,7 @@ impl FossilEditor { } #[tool_handler] -impl ServerHandler for FossilEditor { +impl ServerHandler for FossilMCP { async fn list_resources( &self, _request: Option, @@ -349,349 +342,12 @@ impl ServerHandler for FossilEditor { fn get_info(&self) -> ServerInfo { ServerInfo { server_info: Implementation::from_build_env(), - instructions: Some( - r#"# Fossil MCP Server - Complete User Guide - - ## Overview - Fossil is an MCP server that provides git-like version control for code artifacts with fuzzy pattern matching. It enables precise, incremental code editing without rewriting entire files. - - ## Core Concepts - - ### What is a Fossil? - A \"fossil\" is a versioned code artifact stored in memory. Each fossil has: - - A unique ID (e.g., `\"main.py\"`, `\"config-v2.json\"`) - - Full version history (like git commits) - - Support for fuzzy pattern matching during edits - - ### Why Fuzzy Matching? - Traditional string replacement requires exact character-by-character matches. Fuzzy matching allows you to: - - Ignore whitespace differences (spaces, tabs, blank lines) - - Match patterns even if formatting changed slightly - - Target code sections without knowing exact formatting - - ## Available Functions - - ### 1. `create_fossil` - Creates a new fossil with initial content. - - **Parameters:** - - `id` (string, required): Unique identifier for the fossil - - Use descriptive names like `\"api-handler.py\"` or `\"utils-v1.js\"` - - This ID is used to reference the fossil in all other operations - - `content` (string, required): Initial code/text content - - Can be any text content (code, JSON, markdown, etc.) - - **Example:** - ``` - create_fossil( - id: \"calculator.py\", - content: \"def add(a, b):\ - return a + b\" - ) - ``` - - **Returns:** Success message with the fossil ID - - --- - - ### 2. `list_fossils` - Lists all currently managed fossils. - - **Parameters:** None - - **Returns:** Array of fossil IDs currently in memory - - **Example output:** - ``` - Current fossils: main.py, config.json, utils.js - ``` - - --- - - ### 3. `fuzzy_edit` - Replaces a section of code using fuzzy pattern matching. This is the primary editing function. - - **Parameters:** - - `id` (string, required): The fossil ID to edit - - `start_pattern` (string, required): Pattern marking the START of the section to replace - - Should be distinctive enough to find the right location - - Doesn't need exact whitespace - fuzzy matching handles variations - - **Best practice:** Use complete lines when possible - - `end_pattern` (string, required): Pattern marking the END of the section to replace - - The matched section includes both start and end patterns - - **Best practice:** Use complete lines when possible - - `replacement` (string, required): New code that replaces the matched section - - Empty string `\"\"` performs deletion (equivalent to `delete_fossil` functionality) - - **How it works:** - 1. Finds the first occurrence of `start_pattern` in the file - 2. Finds the first occurrence of `end_pattern` after the start - 3. Replaces everything from start to end (inclusive) with `replacement` - - **Important Notes:** - - Patterns are fuzzy matched - whitespace differences are tolerated - - Both start and end patterns are INCLUDED in the replacement - - If you want to keep the start/end patterns, include them in the replacement - - Empty replacement = deletion - - **Example 1 - Simple replacement:** - ``` - # Original code: - def greet(name): - return \"Hello\" - - # Edit: - fuzzy_edit( - id: \"main.py\", - start_pattern: \"def greet(name):\", - end_pattern: \"return \\\"Hello\\\"\", - replacement: \"def greet(name):\ - return f\\\"Hello, {name}!\\\"\" - ) - - # Result: - def greet(name): - return f\"Hello, {name}!\" - ``` - - **Example 2 - Adding a parameter:** - ``` - # Original: - def calculate(x, y): - - # Edit: - fuzzy_edit( - id: \"calc.py\", - start_pattern: \"def calculate(x, y):\", - end_pattern: \"def calculate(x, y):\", - replacement: \"def calculate(x, y, operation='add'):\" - ) - - # Result: - def calculate(x, y, operation='add'): - ``` - - **Example 3 - Deletion (empty replacement):** - ``` - fuzzy_edit( - id: \"main.py\", - start_pattern: \"\# TODO: remove this\", - end_pattern: \"\# End of temporary code\", - replacement: \"\" - ) - ``` - - **Example 4 - Append to end of file:** - ``` - # Target the last line and rewrite from there - fuzzy_edit( - id: \"main.py\", - start_pattern: \"if __name__ == '__main__':\", - end_pattern: \" main()\", - replacement: \"if __name__ == '__main__':\ - main()\ - \ - def new_function():\ - pass\" - ) - ``` - - **Returns:** - - Success message with a clean diff showing what changed - - The diff uses `@@ line numbers @@` format - - `+` indicates added lines, `-` indicates removed lines - - --- - - ### 4. `get_version` - Retrieves a specific version of a fossil without modifying it. Use this to preview history. - - **Parameters:** - - `id` (string, required): The fossil ID - - `version` (integer, optional): Version index to retrieve - - `0` = initial version (first creation) - - `1, 2, 3...` = subsequent versions (counting from start) - - `-1` = latest version (default) - - `-2` = previous version - - `-3` = two versions ago, etc. - - **Example:** - ``` - get_version(id: \"main.py\", version: 0) # See original - get_version(id: \"main.py\", version: -1) # See latest - get_version(id: \"main.py\", version: -2) # See previous - ``` - - **Returns:** The complete content of the fossil at that version - - --- - - ### 5. `revert` - Undoes recent edits by rolling back to a previous version. This actually modifies the fossil. - - **Parameters:** - - `id` (string, required): The fossil ID to revert - - `steps` (integer, optional): Number of edits to undo (default: 1) - - `steps: 1` = undo last edit - - `steps: 2` = undo last 2 edits - - `steps: 5` = undo last 5 edits - - **Example:** - ``` - revert(id: \"main.py\", steps: 1) # Undo last edit - revert(id: \"main.py\", steps: 3) # Undo last 3 edits - ``` - - **Returns:** - - Success message with diff showing what was reverted - - The fossil is now at the earlier version - - **Use cases:** - - \"Oops, that edit broke something\" → `revert(steps: 1)` - - \"Let's try a different approach\" → `revert()` then make new edit - - \"Go back to working version\" → `revert(steps: 5)` - - --- - - ### 6. `delete_fossil` - Permanently deletes a fossil and all its history. - - **Parameters:** - - `id` (string, required): The fossil ID to delete - - **Returns:** Success confirmation - - **Warning:** This cannot be undone! The entire fossil and its version history are removed from memory. - - --- - - ## Workflow Patterns - - ### Pattern 1: Create and Iterate - ``` - 1. create_fossil(id: \"app.py\", content: \"initial code\") - 2. fuzzy_edit(id: \"app.py\", ...) # Add feature - 3. fuzzy_edit(id: \"app.py\", ...) # Fix bug - 4. fuzzy_edit(id: \"app.py\", ...) # Refactor - 5. If something breaks: revert(id: \"app.py\", steps: 1) - ``` - - ### Pattern 2: Surgical Edits - ``` - # Only change what needs changing - fuzzy_edit( - start_pattern: \"def problematic_function\", - end_pattern: \"return result\", - replacement: \"def fixed_function...\ - return new_result\" - ) - ``` - - ### Pattern 3: Exploration with Safety Net - ``` - 1. get_version(id: \"main.py\", version: -1) # Check current state - 2. fuzzy_edit(...) # Try experimental change - 3. Test it mentally/logically - 4. If it works: continue - 5. If it breaks: revert(steps: 1) - ``` - - ### Pattern 4: Append Pattern - ``` - # To add to end of file, target the last line(s) - fuzzy_edit( - start_pattern: \"last_line_or_function\", - end_pattern: \"last_line_or_function\", - replacement: \"last_line_or_function\ - \ - new_content_here\" - ) - ``` - - ## Best Practices - - ### ✅ DO: - - Use distinctive patterns that uniquely identify the location - - Include complete lines in patterns when possible - - Use `get_version` to check current state before editing - - Use `revert` freely - it's there to help you experiment - - Keep fossil IDs descriptive and clear - - Remember that both start and end patterns are INCLUDED in the match - - ### ❌ DON'T: - - Use overly generic patterns that might match multiple locations - - Worry about exact whitespace in patterns - fuzzy matching handles it - - Rewrite entire files - use targeted edits instead - - Forget that the start/end patterns get replaced too - - Use patterns that are too short (might match wrong location) - - ## Advanced Tips - - ### Tip 1: Multi-line Patterns - Patterns can span multiple lines. Use `\ - ` for newlines: - ``` - start_pattern: \"class MyClass:\ - def __init__\" - ``` - - ### Tip 2: Partial Line Matching - You don't need the entire line, just enough to be distinctive: - ``` - start_pattern: \"def calculate(\" # Matches any line starting with this - ``` - - ### Tip 3: Pattern Selection Strategy - - **Too specific:** Might not match due to whitespace differences - - **Too generic:** Might match wrong location - - **Just right:** Distinctive enough to be unique, fuzzy enough to be flexible - - ### Tip 4: When Edits Fail - If fuzzy matching can't find your pattern: - 1. Use `get_version` to see the current actual content - 2. Check if your pattern actually exists in the file - 3. Try a more distinctive or simpler pattern - 4. Remember: patterns are case-sensitive for content (but whitespace-flexible) - - ## Error Handling - - If a fuzzy match fails, you'll get an error message. Common causes: - - Pattern doesn't exist in the file - - Pattern exists multiple times (uses first match) - - Pattern text doesn't match actual content - - **Solution:** Use `get_version` to inspect the current content and adjust your pattern. - - ## Comparison to Traditional Artifacts - - ### Traditional Artifacts: - - ❌ Full file rewrites every edit - - ❌ Exact string matching (brittle) - - ❌ No version history - - ❌ No undo capability - - ❌ Token-heavy - - ### Fossil System: - - ✅ Surgical edits to specific sections - - ✅ Fuzzy pattern matching (robust) - - ✅ Full version history - - ✅ Git-like revert functionality - - ✅ Token-efficient - - ## Quick Reference - - **Create:** `create_fossil(id, content)` - **Edit:** `fuzzy_edit(id, start_pattern, end_pattern, replacement)` - **Undo:** `revert(id, steps)` - **View:** `get_version(id, version)` - **List:** `list_fossils()` - **Delete:** `delete_fossil(id)` - - --- - - Remember: Fossil enables **iterative, precise code editing** with full version control. Use fuzzy patterns to target sections, make incremental changes, and revert freely when needed. It's designed to make LLM-assisted coding efficient and safe.`,"#.into(), - ), - capabilities: ServerCapabilities::builder().enable_tools().enable_resources().enable_tool_list_changed().build(), + instructions: Some(include_str!("prompt.txt").into()), + capabilities: ServerCapabilities::builder() + .enable_tools() + .enable_resources() + .enable_tool_list_changed() + .build(), ..Default::default() } } diff --git a/src/prompt.txt b/src/prompt.txt new file mode 100644 index 0000000..9457212 --- /dev/null +++ b/src/prompt.txt @@ -0,0 +1,340 @@ +# Fossil MCP Server - Complete User Guide + +## Overview +Fossil is an MCP server that provides git-like version control for code artifacts with fuzzy pattern matching. It enables precise, incremental code editing without rewriting entire files. + +## Core Concepts + +### What is a Fossil? +A "fossil" is a versioned code artifact stored in memory. Each fossil has: +- A unique ID (e.g., `"main.py"`, `"config-v2.json"`) +- Full version history (like git commits) +- Support for fuzzy pattern matching during edits + +### Why Fuzzy Matching? +Traditional string replacement requires exact character-by-character matches. Fuzzy matching allows you to: +- Ignore whitespace differences (spaces, tabs, blank lines) +- Match patterns even if formatting changed slightly +- Target code sections without knowing exact formatting + +## Available Functions + +### 1. `create_fossil` +Creates a new fossil with initial content. + +**Parameters:** +- `id` (string, required): Unique identifier for the fossil + - Use descriptive names like `"api-handler.py"` or `"utils-v1.js"` + - This ID is used to reference the fossil in all other operations +- `content` (string, required): Initial code/text content + - Can be any text content (code, JSON, markdown, etc.) + +**Example:** +``` +create_fossil( + id: "calculator.py", + content: "def add(a, b):\ + return a + b" +) +``` + +**Returns:** Success message with the fossil ID + +--- + +### 2. `list_fossils` +Lists all currently managed fossils. + +**Parameters:** None + +**Returns:** Array of fossil IDs currently in memory + +**Example output:** +``` +Current fossils: main.py, config.json, utils.js +``` + +--- + +### 3. `fuzzy_edit` +Replaces a section of code using fuzzy pattern matching. This is the primary editing function. + +**Parameters:** +- `id` (string, required): The fossil ID to edit +- `start_pattern` (string, required): Pattern marking the START of the section to replace + - Should be distinctive enough to find the right location + - Doesn't need exact whitespace - fuzzy matching handles variations + - **Best practice:** Use complete lines when possible +- `end_pattern` (string, required): Pattern marking the END of the section to replace + - The matched section includes both start and end patterns + - **Best practice:** Use complete lines when possible +- `replacement` (string, required): New code that replaces the matched section + - Empty string `""` performs deletion (equivalent to `delete_fossil` functionality) + +**How it works:** +1. Finds the first occurrence of `start_pattern` in the file +2. Finds the first occurrence of `end_pattern` after the start +3. Replaces everything from start to end (inclusive) with `replacement` + +**Important Notes:** +- Patterns are fuzzy matched - whitespace differences are tolerated +- Both start and end patterns are INCLUDED in the replacement +- If you want to keep the start/end patterns, include them in the replacement +- Empty replacement = deletion + +**Example 1 - Simple replacement:** +``` +# Original code: +def greet(name): + return "Hello" + +# Edit: +fuzzy_edit( + id: "main.py", + start_pattern: "def greet(name):", + end_pattern: "return \\"Hello\\"", + replacement: "def greet(name):\ + return f\\"Hello, {name}!\\"" +) + +# Result: +def greet(name): + return f"Hello, {name}!" +``` + +**Example 2 - Adding a parameter:** +``` +# Original: +def calculate(x, y): + +# Edit: +fuzzy_edit( + id: "calc.py", + start_pattern: "def calculate(x, y):", + end_pattern: "def calculate(x, y):", + replacement: "def calculate(x, y, operation='add'):" +) + +# Result: +def calculate(x, y, operation='add'): +``` + +**Example 3 - Deletion (empty replacement):** +``` +fuzzy_edit( + id: "main.py", + start_pattern: "\# TODO: remove this", + end_pattern: "\# End of temporary code", + replacement: "" +) +``` + +**Example 4 - Append to end of file:** +``` +# Target the last line and rewrite from there +fuzzy_edit( + id: "main.py", + start_pattern: "if __name__ == '__main__':", + end_pattern: " main()", + replacement: "if __name__ == '__main__':\ + main()\ +\ +def new_function():\ + pass" +) +``` + +**Returns:** +- Success message with a clean diff showing what changed +- The diff uses `@@ line numbers @@` format +- `+` indicates added lines, `-` indicates removed lines + +--- + +### 4. `get_version` +Retrieves a specific version of a fossil without modifying it. Use this to preview history. + +**Parameters:** +- `id` (string, required): The fossil ID +- `version` (integer, optional): Version index to retrieve + - `0` = initial version (first creation) + - `1, 2, 3...` = subsequent versions (counting from start) + - `-1` = latest version (default) + - `-2` = previous version + - `-3` = two versions ago, etc. + +**Example:** +``` +get_version(id: "main.py", version: 0) # See original +get_version(id: "main.py", version: -1) # See latest +get_version(id: "main.py", version: -2) # See previous +``` + +**Returns:** The complete content of the fossil at that version + +--- + +### 5. `revert` +Undoes recent edits by rolling back to a previous version. This actually modifies the fossil. + +**Parameters:** +- `id` (string, required): The fossil ID to revert +- `steps` (integer, optional): Number of edits to undo (default: 1) + - `steps: 1` = undo last edit + - `steps: 2` = undo last 2 edits + - `steps: 5` = undo last 5 edits + +**Example:** +``` +revert(id: "main.py", steps: 1) # Undo last edit +revert(id: "main.py", steps: 3) # Undo last 3 edits +``` + +**Returns:** +- Success message with diff showing what was reverted +- The fossil is now at the earlier version + +**Use cases:** +- "Oops, that edit broke something" → `revert(steps: 1)` +- "Let's try a different approach" → `revert()` then make new edit +- "Go back to working version" → `revert(steps: 5)` + +--- + +### 6. `delete_fossil` +Permanently deletes a fossil and all its history. + +**Parameters:** +- `id` (string, required): The fossil ID to delete + +**Returns:** Success confirmation + +**Warning:** This cannot be undone! The entire fossil and its version history are removed from memory. + +--- + +## Workflow Patterns + +### Pattern 1: Create and Iterate +``` +1. create_fossil(id: "app.py", content: "initial code") +2. fuzzy_edit(id: "app.py", ...) # Add feature +3. fuzzy_edit(id: "app.py", ...) # Fix bug +4. fuzzy_edit(id: "app.py", ...) # Refactor +5. If something breaks: revert(id: "app.py", steps: 1) +``` + +### Pattern 2: Surgical Edits +``` +# Only change what needs changing +fuzzy_edit( + start_pattern: "def problematic_function", + end_pattern: "return result", + replacement: "def fixed_function...\ + return new_result" +) +``` + +### Pattern 3: Exploration with Safety Net +``` +1. get_version(id: "main.py", version: -1) # Check current state +2. fuzzy_edit(...) # Try experimental change +3. Test it mentally/logically +4. If it works: continue +5. If it breaks: revert(steps: 1) +``` + +### Pattern 4: Append Pattern +``` +# To add to end of file, target the last line(s) +fuzzy_edit( + start_pattern: "last_line_or_function", + end_pattern: "last_line_or_function", + replacement: "last_line_or_function\ +\ +new_content_here" +) +``` + +## Best Practices + +### ✅ DO: +- Use distinctive patterns that uniquely identify the location +- Include complete lines in patterns when possible +- Use `get_version` to check current state before editing +- Use `revert` freely - it's there to help you experiment +- Keep fossil IDs descriptive and clear +- Remember that both start and end patterns are INCLUDED in the match + +### ❌ DON'T: +- Use overly generic patterns that might match multiple locations +- Worry about exact whitespace in patterns - fuzzy matching handles it +- Rewrite entire files - use targeted edits instead +- Forget that the start/end patterns get replaced too +- Use patterns that are too short (might match wrong location) + +## Advanced Tips + +### Tip 1: Multi-line Patterns +Patterns can span multiple lines. Use `\ +` for newlines: +``` +start_pattern: "class MyClass:\ + def __init__" +``` + +### Tip 2: Partial Line Matching +You don't need the entire line, just enough to be distinctive: +``` +start_pattern: "def calculate(" # Matches any line starting with this +``` + +### Tip 3: Pattern Selection Strategy +- **Too specific:** Might not match due to whitespace differences +- **Too generic:** Might match wrong location +- **Just right:** Distinctive enough to be unique, fuzzy enough to be flexible + +### Tip 4: When Edits Fail +If fuzzy matching can't find your pattern: +1. Use `get_version` to see the current actual content +2. Check if your pattern actually exists in the file +3. Try a more distinctive or simpler pattern +4. Remember: patterns are case-sensitive for content (but whitespace-flexible) + +## Error Handling + +If a fuzzy match fails, you'll get an error message. Common causes: +- Pattern doesn't exist in the file +- Pattern exists multiple times (uses first match) +- Pattern text doesn't match actual content + +**Solution:** Use `get_version` to inspect the current content and adjust your pattern. + +## Comparison to Traditional Artifacts + +### Traditional Artifacts: +- ❌ Full file rewrites every edit +- ❌ Exact string matching (brittle) +- ❌ No version history +- ❌ No undo capability +- ❌ Token-heavy + +### Fossil System: +- ✅ Surgical edits to specific sections +- ✅ Fuzzy pattern matching (robust) +- ✅ Full version history +- ✅ Git-like revert functionality +- ✅ Token-efficient + +## Quick Reference + +**Create:** `create_fossil(id, content)` +**Edit:** `fuzzy_edit(id, start_pattern, end_pattern, replacement)` +**Undo:** `revert(id, steps)` +**View:** `get_version(id, version)` +**List:** `list_fossils()` +**Delete:** `delete_fossil(id)` + +--- + +Remember: Fossil enables **iterative, precise code editing** with full version control. Use fuzzy patterns to target sections, make incremental changes, and revert freely when needed. It's designed to make LLM-assisted coding efficient and safe.