sort of a gui
This commit is contained in:
156
Cargo.lock
generated
156
Cargo.lock
generated
@@ -377,6 +377,15 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -890,6 +899,15 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b"
|
checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "detect-desktop-environment"
|
name = "detect-desktop-environment"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1676,6 +1694,7 @@ checksum = "88acfabc84ec077eaf9ede3457ffa3a104626d79022a9bf7f296093b1d60c73f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"iced_core",
|
"iced_core",
|
||||||
"iced_futures",
|
"iced_futures",
|
||||||
|
"iced_highlighter",
|
||||||
"iced_renderer",
|
"iced_renderer",
|
||||||
"iced_widget",
|
"iced_widget",
|
||||||
"iced_winit",
|
"iced_winit",
|
||||||
@@ -1775,6 +1794,17 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"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]]
|
[[package]]
|
||||||
name = "iced_renderer"
|
name = "iced_renderer"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -1844,6 +1874,7 @@ version = "0.13.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81429e1b950b0e4bca65be4c4278fea6678ea782030a411778f26fa9f8983e1d"
|
checksum = "81429e1b950b0e4bca65be4c4278fea6678ea782030a411778f26fa9f8983e1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"iced_highlighter",
|
||||||
"iced_renderer",
|
"iced_renderer",
|
||||||
"iced_runtime",
|
"iced_runtime",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@@ -2052,6 +2083,12 @@ dependencies = [
|
|||||||
"redox_syscall 0.5.18",
|
"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]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
@@ -2333,6 +2370,12 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-format"
|
name = "num-format"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -2622,6 +2665,28 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
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]]
|
[[package]]
|
||||||
name = "orbclient"
|
name = "orbclient"
|
||||||
version = "0.3.48"
|
version = "0.3.48"
|
||||||
@@ -2841,6 +2906,19 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
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]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.17.16"
|
version = "0.17.16"
|
||||||
@@ -2868,6 +2946,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -2916,6 +3000,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.38.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.41"
|
version = "1.0.41"
|
||||||
@@ -3668,6 +3761,27 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
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]]
|
[[package]]
|
||||||
name = "sys-locale"
|
name = "sys-locale"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -3748,6 +3862,37 @@ dependencies = [
|
|||||||
"cfg-if",
|
"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]]
|
[[package]]
|
||||||
name = "tiny-skia"
|
name = "tiny-skia"
|
||||||
version = "0.11.4"
|
version = "0.11.4"
|
||||||
@@ -4307,7 +4452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3"
|
checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quick-xml",
|
"quick-xml 0.37.5",
|
||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4885,6 +5030,15 @@ version = "0.8.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
|
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]]
|
[[package]]
|
||||||
name = "yazi"
|
name = "yazi"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.100"
|
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"] }
|
iced_aw = { version = "0.11.0", features = ["tab_bar"] }
|
||||||
imara-diff = "0.2.0"
|
imara-diff = "0.2.0"
|
||||||
nucleo-matcher = "0.3.1"
|
nucleo-matcher = "0.3.1"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use imara_diff::{Algorithm, BasicLineDiffPrinter, Diff, InternedInput, UnifiedDiffConfig};
|
use imara_diff::{Algorithm, BasicLineDiffPrinter, Diff, InternedInput, UnifiedDiffConfig};
|
||||||
@@ -65,7 +65,7 @@ impl Fossil {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct FossilManager {
|
pub struct FossilManager {
|
||||||
pub fossils: Arc<Mutex<HashMap<String, Fossil>>>,
|
pub fossils: Arc<Mutex<BTreeMap<String, Fossil>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FossilManager {
|
impl FossilManager {
|
||||||
|
|||||||
191
src/gui.rs
191
src/gui.rs
@@ -1,115 +1,86 @@
|
|||||||
|
use iced::highlighter;
|
||||||
|
use std::{collections::HashMap, path::Path, time::Duration};
|
||||||
|
|
||||||
use crate::fossil::FossilManager;
|
use crate::fossil::FossilManager;
|
||||||
use iced::{
|
use iced::{
|
||||||
Element,
|
Element, Subscription, time,
|
||||||
widget::{button, column, row, text, text_editor},
|
widget::{button, column, row, text, text_editor},
|
||||||
};
|
};
|
||||||
use iced_aw::{TabLabel, Tabs};
|
use iced_aw::{TabLabel, Tabs};
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
fossil_manager: FossilManager,
|
fossil_manager: FossilManager,
|
||||||
tab_editors: HashMap<String, text_editor::Content>,
|
|
||||||
active_tab: Option<String>,
|
active_tab: Option<String>,
|
||||||
tab_order: Vec<String>,
|
theme: highlighter::Theme,
|
||||||
|
editor_contents: HashMap<String, text_editor::Content>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
Edit(String, text_editor::Action),
|
|
||||||
TabSelected(String),
|
TabSelected(String),
|
||||||
CreateFossil,
|
CheckForChanges,
|
||||||
SyncFossil(String),
|
|
||||||
DeleteFossil(String),
|
|
||||||
RefreshFromManager,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn new() -> Self {
|
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 {
|
pub fn with_shared_manager(fossil_manager: FossilManager) -> Self {
|
||||||
Self {
|
let mut state = Self {
|
||||||
fossil_manager,
|
fossil_manager,
|
||||||
tab_editors: HashMap::new(),
|
|
||||||
active_tab: None,
|
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) {
|
fn sync_editor_contents(&mut self) {
|
||||||
// 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) {
|
|
||||||
let fossils = self.fossil_manager.fossils.lock().unwrap();
|
let fossils = self.fossil_manager.fossils.lock().unwrap();
|
||||||
if let Some(fossil) = fossils.get(id) {
|
self.editor_contents.clear();
|
||||||
if let Some(latest_content) = fossil.latest() {
|
|
||||||
self.tab_editors.insert(
|
for (id, fossil) in &*fossils {
|
||||||
id.to_string(),
|
self.editor_contents.insert(
|
||||||
text_editor::Content::with_text(latest_content),
|
id.clone(),
|
||||||
|
text_editor::Content::with_text(&fossil.latest().unwrap()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh_from_manager(&mut self) {
|
pub fn subscription(&self) -> Subscription<Message> {
|
||||||
let fossils = self.fossil_manager.fossils.lock().unwrap();
|
time::every(Duration::from_millis(100)).map(|_| Message::CheckForChanges)
|
||||||
let fossil_ids: Vec<String> = 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
|
fn tab_content<'a>(state: &'a State, id: &str) -> Element<'a, Message> {
|
||||||
let gui_fossil_ids: Vec<String> = self.tab_editors.keys().cloned().collect();
|
state
|
||||||
for id in gui_fossil_ids {
|
.editor_contents
|
||||||
if !fossil_ids.contains(&id) {
|
.get(id)
|
||||||
self.tab_editors.remove(&id);
|
.map(|content| {
|
||||||
self.tab_order.retain(|x| x != &id);
|
text_editor(content)
|
||||||
if self.active_tab.as_ref() == Some(&id) {
|
.highlight(
|
||||||
self.active_tab = self.tab_order.first().cloned();
|
Path::new(id)
|
||||||
}
|
.extension()
|
||||||
}
|
.and_then(|ext| ext.to_str())
|
||||||
}
|
.unwrap_or("py"),
|
||||||
|
state.theme,
|
||||||
// Update active tab if none is selected but tabs exist
|
)
|
||||||
if self.active_tab.is_none() && !self.tab_order.is_empty() {
|
.into()
|
||||||
self.active_tab = Some(self.tab_order[0].clone());
|
})
|
||||||
}
|
.unwrap_or_else(|| text("No content").into())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(state: &State) -> Element<'_, Message> {
|
pub fn view(state: &State) -> Element<'_, Message> {
|
||||||
let mut tabs = Tabs::new(Message::TabSelected);
|
let mut tabs = Tabs::new(Message::TabSelected);
|
||||||
|
let fossils = state.fossil_manager.fossils.lock().unwrap();
|
||||||
for id in &state.tab_order {
|
for (id, _) in &*fossils {
|
||||||
tabs = tabs.push(
|
tabs = tabs.push(
|
||||||
id.clone(),
|
id.clone(),
|
||||||
TabLabel::Text(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 {
|
let tabs = if let Some(active_id) = &state.active_tab {
|
||||||
|
if fossils.contains_key(active_id) {
|
||||||
tabs.set_active_tab(active_id)
|
tabs.set_active_tab(active_id)
|
||||||
} else {
|
} else {
|
||||||
tabs
|
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 {
|
} else {
|
||||||
text("No content available").into()
|
tabs
|
||||||
}
|
};
|
||||||
|
column![tabs].spacing(10).padding(10).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(state: &mut State, message: Message) -> iced::Task<Message> {
|
pub fn update(state: &mut State, message: Message) -> iced::Task<Message> {
|
||||||
match message {
|
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) => {
|
Message::TabSelected(tab_id) => {
|
||||||
state.active_tab = Some(tab_id);
|
state.active_tab = Some(tab_id);
|
||||||
}
|
}
|
||||||
Message::CreateFossil => {
|
Message::CheckForChanges => {
|
||||||
let new_id = format!("fossil_{}", state.tab_order.len() + 1);
|
state.sync_editor_contents();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iced::Task::none()
|
iced::Task::none()
|
||||||
|
|||||||
20
src/main.rs
20
src/main.rs
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
gui::{update, view, State},
|
|
||||||
mcp::FossilEditor,
|
|
||||||
fossil::FossilManager,
|
fossil::FossilManager,
|
||||||
|
gui::{State, update, view},
|
||||||
|
mcp::FossilMCP,
|
||||||
};
|
};
|
||||||
use iced::Task;
|
use iced::Task;
|
||||||
use rmcp::{ServiceExt, transport::stdio};
|
use rmcp::{ServiceExt, transport::stdio};
|
||||||
@@ -15,7 +15,7 @@ mod mcp;
|
|||||||
|
|
||||||
fn main() -> iced::Result {
|
fn main() -> iced::Result {
|
||||||
tracing_subscriber::fmt()
|
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_writer(std::io::stderr)
|
||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
.init();
|
.init();
|
||||||
@@ -23,11 +23,11 @@ fn main() -> iced::Result {
|
|||||||
let rt = Runtime::new().unwrap();
|
let rt = Runtime::new().unwrap();
|
||||||
|
|
||||||
// Create shared fossil manager
|
// 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 {
|
rt.spawn(async {
|
||||||
let service = editor
|
let service = mcp
|
||||||
.serve(stdio())
|
.serve(stdio())
|
||||||
.await
|
.await
|
||||||
.inspect_err(|e| {
|
.inspect_err(|e| {
|
||||||
@@ -39,11 +39,7 @@ fn main() -> iced::Result {
|
|||||||
});
|
});
|
||||||
|
|
||||||
iced::application("Fossil Editor", update, view)
|
iced::application("Fossil Editor", update, view)
|
||||||
|
.subscription(State::subscription)
|
||||||
.default_font(iced::Font::MONOSPACE)
|
.default_font(iced::Font::MONOSPACE)
|
||||||
.run_with(move || {
|
.run_with(move || (State::with_shared_manager(fossil_manager), Task::none()))
|
||||||
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())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)> {
|
pub fn match_lines(code: &str, start: &str, end: &str) -> Option<(usize, usize)> {
|
||||||
let code_chars: Vec<char> = code.chars().collect();
|
let code_chars: Vec<char> = 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) {
|
if start.ends_with(end) {
|
||||||
let pattern_chars: Vec<char> = start.chars().collect();
|
let pattern_chars: Vec<char> = start.chars().collect();
|
||||||
let set: AlignmentSet<InMemoryAlignmentMatrix> =
|
let set: AlignmentSet<MemoryMappedAlignmentMatrix> =
|
||||||
AlignmentSet::new(code_chars.len(), pattern_chars.len(), strategy, |x, y| {
|
AlignmentSet::new(code_chars.len(), pattern_chars.len(), strategy, |x, y| {
|
||||||
code_chars[x] == pattern_chars[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))
|
Some((start_line, end_line))
|
||||||
} else {
|
} else {
|
||||||
let start_chars: Vec<char> = start.chars().collect();
|
let start_chars: Vec<char> = start.chars().collect();
|
||||||
let start_set: AlignmentSet<InMemoryAlignmentMatrix> = AlignmentSet::new(
|
let start_set: AlignmentSet<MemoryMappedAlignmentMatrix> = AlignmentSet::new(
|
||||||
code_chars.len(),
|
code_chars.len(),
|
||||||
start_chars.len(),
|
start_chars.len(),
|
||||||
strategy.clone(),
|
strategy.clone(),
|
||||||
@@ -61,14 +61,16 @@ pub fn match_lines(code: &str, start: &str, end: &str) -> Option<(usize, usize)>
|
|||||||
|
|
||||||
let end_chars: Vec<char> = end.chars().collect();
|
let end_chars: Vec<char> = end.chars().collect();
|
||||||
let remaining_code: Vec<char> = code_chars[start_char_pos..].to_vec();
|
let remaining_code: Vec<char> = code_chars[start_char_pos..].to_vec();
|
||||||
let end_set: AlignmentSet<InMemoryAlignmentMatrix> =
|
let end_set: AlignmentSet<MemoryMappedAlignmentMatrix> =
|
||||||
AlignmentSet::new(remaining_code.len(), end_chars.len(), strategy, |x, y| {
|
AlignmentSet::new(remaining_code.len(), end_chars.len(), strategy, |x, y| {
|
||||||
remaining_code[x] == end_chars[y]
|
remaining_code[x] == end_chars[y]
|
||||||
})
|
})
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
let end_alignment = end_set.local_alignments().next().unwrap();
|
let end_char_pos_relative = end_set
|
||||||
let end_char_pos_relative = end_alignment
|
.local_alignments()
|
||||||
|
.filter_map(|end_alignment| {
|
||||||
|
end_alignment
|
||||||
.steps()
|
.steps()
|
||||||
.filter_map(|step| {
|
.filter_map(|step| {
|
||||||
if let Step::Align { x, .. } = step {
|
if let Step::Align { x, .. } = step {
|
||||||
@@ -77,11 +79,13 @@ pub fn match_lines(code: &str, start: &str, end: &str) -> Option<(usize, usize)>
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.last()?;
|
.last()
|
||||||
|
})
|
||||||
|
.min()?;
|
||||||
|
|
||||||
let end_char_pos = start_char_pos + end_char_pos_relative;
|
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);
|
let end_line = code[..=end_char_pos].lines().count().saturating_sub(1);
|
||||||
|
|
||||||
Some((start_line, end_line))
|
Some((start_line, end_line))
|
||||||
|
|||||||
368
src/mcp.rs
368
src/mcp.rs
@@ -34,11 +34,11 @@ pub struct FuzzyEditRequest {
|
|||||||
#[schemars(description = "The unique identifier for the fossil to edit.")]
|
#[schemars(description = "The unique identifier for the fossil to edit.")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
#[schemars(
|
#[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,
|
pub start_pattern: String,
|
||||||
#[schemars(
|
#[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,
|
pub end_pattern: String,
|
||||||
#[schemars(
|
#[schemars(
|
||||||
@@ -66,20 +66,13 @@ pub struct GetVersionRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FossilEditor {
|
pub struct FossilMCP {
|
||||||
tool_router: ToolRouter<Self>,
|
tool_router: ToolRouter<Self>,
|
||||||
state: FossilManager,
|
state: FossilManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool_router]
|
#[tool_router]
|
||||||
impl FossilEditor {
|
impl FossilMCP {
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
tool_router: Self::tool_router(),
|
|
||||||
state: FossilManager::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_shared_state(state: FossilManager) -> Self {
|
pub fn with_shared_state(state: FossilManager) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tool_router: Self::tool_router(),
|
tool_router: Self::tool_router(),
|
||||||
@@ -207,7 +200,7 @@ impl FossilEditor {
|
|||||||
let lines: Vec<&str> = current_code.lines().collect();
|
let lines: Vec<&str> = current_code.lines().collect();
|
||||||
let mut new_code_lines = Vec::new();
|
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.push(&req.replacement);
|
||||||
new_code_lines.extend_from_slice(&lines[end_line + 1..]);
|
new_code_lines.extend_from_slice(&lines[end_line + 1..]);
|
||||||
|
|
||||||
@@ -273,7 +266,7 @@ impl FossilEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tool_handler]
|
#[tool_handler]
|
||||||
impl ServerHandler for FossilEditor {
|
impl ServerHandler for FossilMCP {
|
||||||
async fn list_resources(
|
async fn list_resources(
|
||||||
&self,
|
&self,
|
||||||
_request: Option<PaginatedRequestParam>,
|
_request: Option<PaginatedRequestParam>,
|
||||||
@@ -349,349 +342,12 @@ impl ServerHandler for FossilEditor {
|
|||||||
fn get_info(&self) -> ServerInfo {
|
fn get_info(&self) -> ServerInfo {
|
||||||
ServerInfo {
|
ServerInfo {
|
||||||
server_info: Implementation::from_build_env(),
|
server_info: Implementation::from_build_env(),
|
||||||
instructions: Some(
|
instructions: Some(include_str!("prompt.txt").into()),
|
||||||
r#"# Fossil MCP Server - Complete User Guide
|
capabilities: ServerCapabilities::builder()
|
||||||
|
.enable_tools()
|
||||||
## Overview
|
.enable_resources()
|
||||||
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.
|
.enable_tool_list_changed()
|
||||||
|
.build(),
|
||||||
## 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(),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
340
src/prompt.txt
Normal file
340
src/prompt.txt
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user