diff --git a/Cargo.lock b/Cargo.lock index a1a0de4..e9e636a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,18 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] [[package]] name = "android-tzdata" @@ -34,9 +43,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -49,33 +58,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", @@ -84,9 +93,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" +checksum = "d615619615a650c571269c00dca41db04b9210037fa76ed8239f70404ab56985" dependencies = [ "bzip2", "deflate64", @@ -145,7 +154,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -154,11 +163,22 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bytes" @@ -187,9 +207,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.24" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "jobserver", "libc", @@ -198,9 +218,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chrono" @@ -218,9 +238,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -228,9 +248,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -240,9 +260,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", @@ -252,15 +272,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "core-foundation-sys" @@ -322,7 +342,25 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.60.2", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", ] [[package]] @@ -343,9 +381,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -467,7 +505,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -494,6 +532,98 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "grep" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308ae749734e28d749a86f33212c7b756748568ce332f970ac1d9cd8531f32e6" +dependencies = [ + "grep-cli", + "grep-matcher", + "grep-printer", + "grep-regex", + "grep-searcher", +] + +[[package]] +name = "grep-cli" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f1288f0e06f279f84926fa4c17e3fcd2a22b357927a82f2777f7be26e4cec0" +dependencies = [ + "bstr", + "globset", + "libc", + "log", + "termcolor", + "winapi-util", +] + +[[package]] +name = "grep-matcher" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a3141a10a43acfedc7c98a60a834d7ba00dfe7bec9071cbfc19b55b292ac02" +dependencies = [ + "memchr", +] + +[[package]] +name = "grep-printer" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c112110ae4a891aa4d83ab82ecf734b307497d066f437686175e83fbd4e013fe" +dependencies = [ + "bstr", + "grep-matcher", + "grep-searcher", + "log", + "serde", + "serde_json", + "termcolor", +] + +[[package]] +name = "grep-regex" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edd147c7e3296e7a26bd3a81345ce849557d5a8e48ed88f736074e760f91f7e" +dependencies = [ + "bstr", + "grep-matcher", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "grep-searcher" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b6c14b3fc2e0a107d6604d3231dec0509e691e62447104bc385a46a7892cda" +dependencies = [ + "bstr", + "encoding_rs", + "encoding_rs_io", + "grep-matcher", + "log", + "memchr", + "memmap2", +] + [[package]] name = "heck" version = "0.5.0" @@ -591,9 +721,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" [[package]] name = "liblzma" @@ -606,9 +736,9 @@ dependencies = [ [[package]] name = "liblzma-sys" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5839bad90c3cc2e0b8c4ed8296b80e86040240f81d46b9c0e9bc8dd51ddd3af1" +checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" dependencies = [ "cc", "libc", @@ -633,9 +763,9 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -649,15 +779,24 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] @@ -669,7 +808,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -717,9 +856,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -727,15 +866,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -796,15 +935,15 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags", ] @@ -820,6 +959,23 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rust-mcp-filesystem" version = "0.1.9" @@ -832,6 +988,7 @@ dependencies = [ "dirs", "futures", "glob", + "grep", "rust-mcp-sdk", "serde", "serde_json", @@ -845,9 +1002,9 @@ dependencies = [ [[package]] name = "rust-mcp-macros" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b5dbc170eee5a6ba6ea4fd4175846a49fdf8cdcdb60bccd55f94a92294af32d" +checksum = "375eba9388d9d5ffc8951e5f25296f9739be4e5700375ac7c74ad957a903b92b" dependencies = [ "proc-macro2", "quote", @@ -858,9 +1015,9 @@ dependencies = [ [[package]] name = "rust-mcp-schema" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c9966340f5104a8d22b6c2db8901f8626a0f737820a385db3ffbb29b1f6ae0f" +checksum = "a794de25669a2d21c5074ec5082f74f5e88863a112339fe90264d9e480b0ee8b" dependencies = [ "serde", "serde_json", @@ -868,9 +1025,9 @@ dependencies = [ [[package]] name = "rust-mcp-sdk" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aee75ac978738a3ce3ad666665dbbbb9d97331ceb0badb483969acef2024374c" +checksum = "6bed508dfa638d89b1a797ddfe1b4b4022db834036b3f1bcdf2909cea5be14a2" dependencies = [ "async-trait", "futures", @@ -887,9 +1044,9 @@ dependencies = [ [[package]] name = "rust-mcp-transport" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c975ce25df3b395a0cca763e17fb51978462a29bb816d8f6936ecd8a4412eac" +checksum = "6318216799507090512e6ffa7ea7fd26c6f2098c655cb72942dd5e221cfdea3d" dependencies = [ "async-trait", "bytes", @@ -905,9 +1062,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustix" @@ -1004,18 +1161,15 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" @@ -1035,9 +1189,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.101" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", @@ -1057,6 +1211,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1164,9 +1327,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", @@ -1175,9 +1338,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -1212,9 +1375,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -1329,9 +1492,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-result" @@ -1357,7 +1520,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1366,7 +1529,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", ] [[package]] @@ -1375,14 +1547,30 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -1391,48 +1579,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index e3bde76..f1980f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ eula = false rust-mcp-sdk = { version = "0.4", default-features = false, features = [ "server", "macros", + "2025_03_26", ] } thiserror = { version = "2.0" } @@ -35,6 +36,7 @@ async-trait = "0.1" futures = "0.3" tokio-util = "0.7" async_zip = { version = "0.0", features = ["full"] } +grep = "0.3" [dev-dependencies] tempfile = "3.2" diff --git a/src/error.rs b/src/error.rs index cf2601d..07ee74e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -25,6 +25,8 @@ pub enum ServiceError { #[error("{0}")] SerdeJsonError(#[from] serde_json::Error), #[error("{0}")] + ContentSearchError(#[from] grep::regex::Error), + #[error("{0}")] McpSdkError(#[from] McpSdkError), #[error("{0}")] ZipError(#[from] ZipError), diff --git a/src/fs_service.rs b/src/fs_service.rs index 7677b1f..a9c6ad0 100644 --- a/src/fs_service.rs +++ b/src/fs_service.rs @@ -1,7 +1,11 @@ pub mod file_info; pub mod utils; - use file_info::FileInfo; +use grep::{ + matcher::{Match, Matcher}, + regex::RegexMatcherBuilder, + searcher::{sinks::UTF8, BinaryDetection, Searcher}, +}; use std::{ env, @@ -29,10 +33,33 @@ use crate::{ tools::EditOperation, }; +const SNIPPET_MAX_LENGTH: usize = 200; +const SNIPPET_BACKWARD_CHARS: usize = 30; + pub struct FileSystemService { allowed_path: Vec, } +/// Represents a single match found in a file's content. +#[derive(Debug, Clone)] +pub struct ContentMatchResult { + /// The line number where the match occurred (1-based). + pub line_number: u64, + pub start_pos: usize, + /// The line of text containing the match. + /// If the line exceeds 255 characters (excluding the search term), only a truncated portion will be shown. + pub line_text: String, +} + +/// Represents all matches found in a specific file. +#[derive(Debug, Clone)] +pub struct FileSearchResult { + /// The path to the file where matches were found. + pub file_path: PathBuf, + /// All individual match results within the file. + pub matches: Vec, +} + impl FileSystemService { pub fn try_new(allowed_directories: &[String]) -> ServiceResult { let normalized_dirs: Vec = allowed_directories @@ -376,19 +403,59 @@ impl FileSystemService { Ok(()) } + /// Searches for files in the directory tree starting at `root_path` that match the given `pattern`, + /// excluding paths that match any of the `exclude_patterns`. + /// + /// # Arguments + /// * `root_path` - The root directory to start the search from. + /// * `pattern` - A glob pattern to match file names (case-insensitive). If no wildcards are provided, + /// the pattern is wrapped in '*' for partial matching. + /// * `exclude_patterns` - A list of glob patterns to exclude paths (case-sensitive). + /// + /// # Returns + /// A `ServiceResult` containing a vector of`walkdir::DirEntry` objects for matching files, + /// or a `ServiceError` if an error occurs. pub fn search_files( &self, - // root_path: impl Into, root_path: &Path, pattern: String, exclude_patterns: Vec, ) -> ServiceResult> { + let result = self.search_files_iter(root_path, pattern, exclude_patterns)?; + Ok(result.collect::>()) + } + + /// Returns an iterator over files in the directory tree starting at `root_path` that match + /// the given `pattern`, excluding paths that match any of the `exclude_patterns`. + /// + /// # Arguments + /// * `root_path` - The root directory to start the search from. + /// * `pattern` - A glob pattern to match file names. If no wildcards are provided, the pattern is wrapped in `**/*{pattern}*` for partial matching. + /// * `exclude_patterns` - A list of glob patterns to exclude paths (case-sensitive). + /// + /// # Returns + /// A `ServiceResult` containing an iterator yielding `walkdir::DirEntry` objects for matching files, + /// or a `ServiceError` if an error occurs. + pub fn search_files_iter<'a>( + &'a self, + // root_path: impl Into, + root_path: &'a Path, + pattern: String, + exclude_patterns: Vec, + ) -> ServiceResult + 'a> { let valid_path = self.validate_path(root_path)?; + let updated_pattern = if pattern.contains('*') { + pattern.to_lowercase() + } else { + format!("**/*{}*", &pattern.to_lowercase()) + }; + let glob_pattern = Pattern::new(&updated_pattern); + let result = WalkDir::new(valid_path) .follow_links(true) .into_iter() - .filter_entry(|dir_entry| { + .filter_entry(move |dir_entry| { let full_path = dir_entry.path(); // Validate each path before processing @@ -415,18 +482,9 @@ impl FileSystemService { }); !should_exclude - }); - - let updated_pattern = if pattern.contains('*') { - pattern.to_lowercase() - } else { - format!("**/*{}*", &pattern.to_lowercase()) - }; - let glob_pattern = Pattern::new(&updated_pattern); - let final_result = result - .into_iter() + }) .filter_map(|v| v.ok()) - .filter(|entry| { + .filter(move |entry| { if root_path == entry.path() { return false; } @@ -437,11 +495,10 @@ impl FileSystemService { glob.matches(&entry.file_name().to_str().unwrap_or("").to_lowercase()) }) .unwrap_or(false); - is_match - }) - .collect::>(); - Ok(final_result) + }); + + Ok(result) } pub fn create_unified_diff( @@ -631,4 +688,140 @@ impl FileSystemService { Ok(formatted_diff) } + + pub fn escape_regex(&self, text: &str) -> String { + // Covers special characters in regex engines (RE2, PCRE, JS, Python) + const SPECIAL_CHARS: &[char] = &[ + '.', '^', '$', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '|', '/', + ]; + + let mut escaped = String::with_capacity(text.len()); + + for ch in text.chars() { + if SPECIAL_CHARS.contains(&ch) { + escaped.push('\\'); + } + escaped.push(ch); + } + + escaped + } + + // Searches the content of a file for occurrences of the given query string. + /// + /// This method searches the file specified by `file_path` for lines matching the `query`. + /// The search can be performed as a regular expression or as a literal string, + /// depending on the `is_regex` flag. + /// + /// If matched line is larger than 255 characters, a snippet will be extracted around the matched text. + /// + pub fn content_search( + &self, + query: &str, + file_path: impl AsRef, + is_regex: Option, + ) -> ServiceResult> { + let query = if is_regex.unwrap_or_default() { + query.to_string() + } else { + self.escape_regex(query) + }; + + let matcher = RegexMatcherBuilder::new() + .case_insensitive(true) + .build(query.as_str())?; + + let mut searcher = Searcher::new(); + let mut result = FileSearchResult { + file_path: file_path.as_ref().to_path_buf(), + matches: vec![], + }; + + searcher.set_binary_detection(BinaryDetection::quit(b'\x00')); + + searcher.search_path( + &matcher, + file_path, + UTF8(|line_number, line| { + let actual_match = matcher.find(line.as_bytes())?.unwrap(); + + result.matches.push(ContentMatchResult { + line_number, + start_pos: actual_match.start(), + line_text: self.extract_snippet(line, actual_match, None, None), + }); + Ok(true) + }), + )?; + + if result.matches.is_empty() { + return Ok(None); + } + + Ok(Some(result)) + } + + /// Extracts a snippet from a given line of text around a match. + /// + /// It extracts a substring starting a fixed number of characters (`SNIPPET_BACKWARD_CHARS`) + /// before the start position of the `match`, and extends up to `max_length` characters + /// If the snippet does not include the beginning or end of the original line, ellipses (`"..."`) are added + /// to indicate the truncation. + pub fn extract_snippet( + &self, + line: &str, + match_result: Match, + max_length: Option, + backward_chars: Option, + ) -> String { + let max_length = max_length.unwrap_or(SNIPPET_MAX_LENGTH); + let backward_chars = backward_chars.unwrap_or(SNIPPET_BACKWARD_CHARS); + + let start_pos = line.len() - line.trim_start().len(); + + let line = line.trim(); + + // Start SNIPPET_BACKWARD_CHARS characters before match (or at 0) + let snippet_start = (match_result.start() - start_pos).saturating_sub(backward_chars); + + // Get up to SNIPPET_MAX_LENGTH characters from snippet_start + let snippet_end = (snippet_start + max_length).min(line.len()); + + let snippet = &line[snippet_start..snippet_end]; + + // Add ellipses if line was truncated + let mut result = String::new(); + if snippet_start > 0 { + result.push_str("..."); + } + result.push_str(snippet); + if snippet_end < line.len() { + result.push_str("..."); + } + result + } + + pub fn search_files_content( + &self, + root_path: impl AsRef, + pattern: &str, + query: &str, + is_regex: bool, + exclude_patterns: Option>, + ) -> ServiceResult> { + let files_iter = self.search_files_iter( + root_path.as_ref(), + pattern.to_string(), + exclude_patterns.to_owned().unwrap_or_default(), + )?; + + let results: Vec = files_iter + .filter_map(|entry| { + self.content_search(query, entry.path(), Some(is_regex)) + .ok() + .and_then(|v| v) + }) + .collect(); + Ok(results) + } } diff --git a/src/handler.rs b/src/handler.rs index d871c6f..f8637a7 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -147,6 +147,9 @@ impl ServerHandler for MyServerHandler { FileSystemTools::ZipDirectoryTool(params) => { ZipDirectoryTool::run_tool(params, &self.fs_service).await } + FileSystemTools::SearchFilesContentTool(params) => { + SearchFilesContentTool::run_tool(params, &self.fs_service).await + } } } } diff --git a/src/tools.rs b/src/tools.rs index 7d540f2..3fbab26 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -8,6 +8,7 @@ mod move_file; mod read_files; mod read_multiple_files; mod search_file; +mod search_files_content; mod write_file; mod zip_unzip; @@ -22,6 +23,7 @@ pub use read_files::ReadFileTool; pub use read_multiple_files::ReadMultipleFilesTool; pub use rust_mcp_sdk::tool_box; pub use search_file::SearchFilesTool; +pub use search_files_content::SearchFilesContentTool; pub use write_file::WriteFileTool; pub use zip_unzip::{UnzipFileTool, ZipDirectoryTool, ZipFilesTool}; @@ -42,7 +44,8 @@ tool_box!( WriteFileTool, ZipFilesTool, UnzipFileTool, - ZipDirectoryTool + ZipDirectoryTool, + SearchFilesContentTool ] ); @@ -58,13 +61,13 @@ impl FileSystemTools { | FileSystemTools::ZipFilesTool(_) | FileSystemTools::UnzipFileTool(_) | FileSystemTools::ZipDirectoryTool(_) => true, - FileSystemTools::ReadFileTool(_) | FileSystemTools::DirectoryTreeTool(_) | FileSystemTools::GetFileInfoTool(_) | FileSystemTools::ListAllowedDirectoriesTool(_) | FileSystemTools::ListDirectoryTool(_) | FileSystemTools::ReadMultipleFilesTool(_) + | FileSystemTools::SearchFilesContentTool(_) | FileSystemTools::SearchFilesTool(_) => false, } } diff --git a/src/tools/search_files_content.rs b/src/tools/search_files_content.rs new file mode 100644 index 0000000..a8911ef --- /dev/null +++ b/src/tools/search_files_content.rs @@ -0,0 +1,87 @@ +use crate::error::ServiceError; +use crate::fs_service::{FileSearchResult, FileSystemService}; +use rust_mcp_sdk::macros::{mcp_tool, JsonSchema}; +use rust_mcp_sdk::schema::{schema_utils::CallToolError, CallToolResult}; +use std::fmt::Write; +#[mcp_tool( + name = "search_files_content", + description = concat!("Searches for text or regex patterns in the content of files matching matching a GLOB pattern.", + "Returns detailed matches with file path, line number, column number and a preview of matched text.", + "By default, it performs a literal text search; if the 'is_regex' parameter is set to true, it performs a regular expression (regex) search instead.", + "Ideal for finding specific code, comments, or text when you don’t know their exact location."), + destructive_hint = false, + idempotent_hint = false, + open_world_hint = false, + read_only_hint = true +)] +#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug, JsonSchema)] + +/// A tool for searching content of one or more files based on a path and pattern. +pub struct SearchFilesContentTool { + /// The file or directory path to search in. + pub path: String, + /// The file glob pattern to match (e.g., "*.rs"). + pub pattern: String, + /// Text or regex pattern to find in file contents (e.g., 'TODO' or '^function\\s+'). + pub query: String, + /// Whether the query is a regular expression. If false, the query as plain text. (Default : false) + pub is_regex: Option, + #[serde(rename = "excludePatterns")] + /// Optional list of patterns to exclude from the search. + pub exclude_patterns: Option>, +} + +impl SearchFilesContentTool { + fn format_result(&self, results: Vec) -> String { + // TODO: improve capacity estimation + let estimated_capacity = 2048; + + let mut output = String::with_capacity(estimated_capacity); + + for file_result in results { + // Push file path + let _ = writeln!(output, "{}", file_result.file_path.display()); + + // Push each match line + for m in &file_result.matches { + // Format: " line:col: text snippet" + let _ = writeln!( + output, + " {}:{}: {}", + m.line_number, m.start_pos, m.line_text + ); + } + + // double spacing + output.push('\n'); + } + + output + } + pub async fn run_tool( + params: Self, + context: &FileSystemService, + ) -> std::result::Result { + let is_regex = params.is_regex.unwrap_or_default(); + match context.search_files_content( + ¶ms.path, + ¶ms.pattern, + ¶ms.query, + is_regex, + params.exclude_patterns.to_owned(), + ) { + Ok(results) => { + if results.is_empty() { + return Ok(CallToolResult::with_error(CallToolError::new( + ServiceError::FromString("No matches found in the files content.".into()), + ))); + } + Ok(CallToolResult::text_content( + params.format_result(results), + None, + )) + } + Err(err) => Ok(CallToolResult::with_error(CallToolError::new(err))), + } + } +} diff --git a/tests/test_fs_service.rs b/tests/test_fs_service.rs index cd8ae1c..ecffd18 100644 --- a/tests/test_fs_service.rs +++ b/tests/test_fs_service.rs @@ -8,6 +8,7 @@ use common::create_temp_file_info; use common::get_temp_dir; use common::setup_service; use dirs::home_dir; +use grep::matcher::Match; use rust_mcp_filesystem::error::ServiceError; use rust_mcp_filesystem::fs_service::file_info::FileInfo; use rust_mcp_filesystem::fs_service::utils::*; @@ -923,3 +924,174 @@ async fn test_panic_on_out_of_bounds_edit() { // It should panic without the fix, or return an error after applying the fix assert!(result.is_err()); } + +#[tokio::test] +async fn test_content_search() { + let (temp_dir, service) = setup_service(vec!["dir_search".to_string()]); + let file = create_temp_file( + &temp_dir.as_path().join("dir_search"), + "file_to_search.txt", + r#"For the Doctor Watsons of this world, as opposed to the Sherlock + Holmeses, success in the province of detective work must always + be, to a very large extent, the result of luck. Sherlock Holmes + can extract a clew from a wisp of straw or a flake of cigar ash; + but Doctor Watso2n has to have it taken out for him and dusted, + and exhibited clearly, with Watso\d*n a label attached."#, + ); + + let query = r#"Watso\d*n"#; + + // search as regex + let result = service.content_search(query, &file, Some(true)).unwrap(); + + assert!(result.is_some()); + let result = result.unwrap(); + + assert_eq!(result.file_path, file); + assert_eq!(result.matches.len(), 2); + assert_eq!(result.matches[0].line_number, 1); + assert_eq!(result.matches[1].line_number, 5); + assert_eq!( + result.matches[0].line_text.trim(), + "For the Doctor Watsons of this world, as opposed to the Sherlock" + ); + assert_eq!( + result.matches[1].line_text.trim(), + "but Doctor Watso2n has to have it taken out for him and dusted," + ); + + // search as literal + let result = service.content_search(query, &file, Some(false)).unwrap(); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.matches.len(), 1); + assert_eq!(result.matches[0].line_number, 6); + assert_eq!( + result.matches[0].line_text.trim(), + "and exhibited clearly, with Watso\\d*n a label attached." + ); +} + +#[test] +fn test_match_near_start_short_line() { + let (_, service) = setup_service(vec!["dir_search".to_string()]); + + let line = "match this text"; + let m = Match::new(0, 5); + let result = service.extract_snippet(line, m, Some(20), Some(5)); + + // Start at 0, should not prepend ... + // Full line is shorter than SNIPPET_MAX_LENGTH + assert_eq!(result, "match this text"); +} + +#[tokio::test] +async fn test_snippet_back_chars() { + let (_, service) = setup_service(vec!["dir_search".to_string()]); + let line = "this is a long enough line for testing match in middle"; + let m = Match::new(40, 45); + let result = service.extract_snippet(line, m, Some(20), Some(5)); + + assert!(result.starts_with("...")); + assert!(!result.ends_with("...")); + assert!(result.contains("match")); + + // larger text, truncates at the end + let line = "this is a long enough line for testing match in middles ."; + let m = Match::new(40, 45); + let result = service.extract_snippet(line, m, Some(20), Some(5)); + assert!(result.starts_with("...")); + assert!(result.ends_with("...")); + assert!(result.contains("match")); +} + +#[test] +fn test_match_triggers_only_end_ellipsis() { + let (_, service) = setup_service(vec!["dir_search".to_string()]); + + let line = "match is at start but line is long"; + let m = Match::new(0, 5); + + let result = service.extract_snippet(line, m, Some(10), Some(5)); + + // Only ends in ellipsis + assert!(!result.starts_with("...")); + assert!(result.ends_with("...")); +} + +#[test] +fn test_match_triggers_only_start_ellipsis() { + let (_, service) = setup_service(vec!["dir_search".to_string()]); + + let line = "line is long and match is near end"; + let m = Match::new(31, 36); + let result = service.extract_snippet(line, m, Some(10), Some(5)); + // Only starts with ellipsis + assert!(result.starts_with("...")); + assert!(!result.ends_with("...")); +} + +#[test] +fn test_trim_applied() { + let (_, service) = setup_service(vec!["dir_search".to_string()]); + + let line = " match here with spaces "; + let m = Match::new(5, 10); + + let result = service.extract_snippet(line, m, Some(10), Some(5)); + + // Ensure whitespace is trimmed before slicing + assert!(!result.contains(" ")); + assert!(result.contains("match")); +} + +#[test] +fn test_exact_snippet_end() { + let (_, service) = setup_service(vec!["dir_search".to_string()]); + let line = "some content with match inside"; + let m = Match::new(18, 23); + let result = service.extract_snippet(line, m, Some(line.len()), Some(18)); + // Full trimmed line, no ellipses + assert_eq!(result, "some content with match inside"); +} + +#[test] +fn search_files_content() { + let (temp_dir, service) = setup_service(vec!["dir_search".to_string()]); + + create_temp_file( + &temp_dir.as_path().join("dir_search"), + "file1.txt", + r#"For the Doctor Watsons of this world, as opposed to the Sherlock + Holmeses, success in the province of detective work must always + be, to a very large extent, the result of luck. Sherlock Holmes + can extract a clew from a wisp of straw or a flake of cigar ash; + but Doctor Watso2n has to have it taken out for him and dusted, + and exhibited clearly, with Watso\d*n a label attached."#, + ); + create_temp_file( + &temp_dir.as_path().join("dir_search"), + "file2.txt", + r#"For the Doctor Watsons of this world, as opposed to the Sherlock + Holmeses, success in the province of detective work must always + be, to a very large extent, the result of luck. Sherlock Holmes + can extract a clew from a wisp of straw or a flake of cigar ash; + but Doctor Watso2n has to have it taken out for him and dusted, + and exhibited clearly, with Watso\d*n a label attached."#, + ); + + let query = r#"Watso\d*n"#; + + let results = service + .search_files_content( + temp_dir.as_path().join("dir_search"), + "*.txt", + query, + true, + None, + ) + .unwrap(); + assert_eq!(results.len(), 2); + assert_eq!(results[0].matches.len(), 2); + assert_eq!(results[1].matches.len(), 2); +}