From 484c9a900c78a41d50e54ce3ff3fc94c98beb868 Mon Sep 17 00:00:00 2001 From: Liu-Cheng Xu Date: Thu, 18 Apr 2024 17:32:54 +0800 Subject: [PATCH] Optimize grep search performance (#1071) * Initial markdown preview * Move toc into maple_markdown crate * Update docs * Replace markdown with pulldown-cmark * Improve grep performance hyperfine shows 20%+ perf enhancement. * Update CHANGELOG.md --- CHANGELOG.md | 1 + Cargo.lock | 442 +++++++++++++++++- Cargo.toml | 5 +- crates/cli/src/command/grep/mod.rs | 18 +- crates/maple_config/src/lib.rs | 4 + crates/maple_core/Cargo.toml | 2 +- .../src/searcher/grep/stoppable_searcher.rs | 20 +- .../src/stdio_server/plugin/markdown.rs | 282 ++++------- .../src/stdio_server/provider/impls/blines.rs | 3 + .../src/stdio_server/provider/impls/grep.rs | 39 +- crates/maple_markdown/Cargo.toml | 23 + crates/maple_markdown/js/index.html | 80 ++++ crates/maple_markdown/src/lib.rs | 146 ++++++ crates/maple_markdown/src/toc.rs | 193 ++++++++ docs/src/plugins/plugins.md | 1 + plugin/clap.vim | 2 +- 16 files changed, 1010 insertions(+), 251 deletions(-) create mode 100644 crates/maple_markdown/Cargo.toml create mode 100644 crates/maple_markdown/js/index.html create mode 100644 crates/maple_markdown/src/lib.rs create mode 100644 crates/maple_markdown/src/toc.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e688cc1f8..50e9f9093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add `multi_select` property explicitly in provider, useful for the provider lsp. - Add `:ClapAction diagnostics.{first,last,next,prev}` for navigating between all kinds of the diagnostics. - Add `:ClapAction diagnostics.{firstHint,lastHint,nextHint,prevHint}` for navigating between the Hint diagnostics. +- Optimize the grep search performance by 20+%. ## [0.52] 2024-2-29 diff --git a/Cargo.lock b/Cargo.lock index 4d113eba1..d2c56a520 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,87 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.21.7", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper 1.0.0", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -172,6 +253,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.9.1" @@ -205,6 +295,12 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.5.0" @@ -492,6 +588,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.0" @@ -577,6 +682,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.20.8" @@ -612,6 +727,12 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + [[package]] name = "deranged" version = "0.3.11" @@ -621,6 +742,16 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "directories" version = "4.0.1" @@ -877,6 +1008,16 @@ dependencies = [ "thread_local", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -887,6 +1028,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.12" @@ -993,7 +1143,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap", "slab", "tokio", @@ -1017,6 +1167,30 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http 1.1.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.1.0", +] + [[package]] name = "heck" version = "0.4.1" @@ -1049,6 +1223,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -1056,7 +1241,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -1083,8 +1291,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -1096,6 +1304,25 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -1103,13 +1330,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", - "hyper", + "http 0.2.12", + "hyper 0.14.28", "rustls", "tokio", "tokio-rustls", ] +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.2.0", + "pin-project-lite", + "socket2", + "tokio", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -1460,12 +1703,12 @@ dependencies = [ "maple_config", "maple_derive", "maple_lsp", + "maple_markdown", "matcher", "once_cell", "parking_lot", "paths", "pattern", - "percent-encoding", "printer", "rayon", "regex", @@ -1519,6 +1762,23 @@ dependencies = [ "which", ] +[[package]] +name = "maple_markdown" +version = "0.1.52" +dependencies = [ + "axum", + "axum-extra", + "once_cell", + "percent-encoding", + "pulldown-cmark", + "regex", + "serde_json", + "tokio", + "tracing", + "utils", + "webbrowser", +] + [[package]] name = "matcher" version = "0.1.52" @@ -1541,6 +1801,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.7.1" @@ -1785,6 +2051,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1882,6 +2168,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0530d13d87d1f549b66a3e8d0c688952abe5994e204ed62615baaf25dc029c" +dependencies = [ + "bitflags 2.4.2", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d8f9aa0e3cbcfaf8bf00300004ee3b72f74770f9cbac93f6928771f613276b" + [[package]] name = "quick-xml" version = "0.31.0" @@ -2047,9 +2352,9 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-rustls", "ipnet", "js-sys", @@ -2063,7 +2368,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-rustls", @@ -2158,6 +2463,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.17" @@ -2235,6 +2546,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.18" @@ -2258,6 +2579,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2382,6 +2714,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c" + [[package]] name = "syntect" version = "5.2.0" @@ -2534,6 +2872,7 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -2562,6 +2901,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -2602,6 +2953,28 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -2614,6 +2987,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2866,6 +3240,31 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "types" version = "0.1.52" @@ -2874,6 +3273,15 @@ dependencies = [ "pattern", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2929,6 +3337,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.1" @@ -2956,6 +3370,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index a675cb4e9..cd7d0bc85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "crates/maple_core", "crates/maple_derive", "crates/maple_lsp", + "crates/maple_markdown", "crates/matcher", "crates/matcher/extracted_fzy", "crates/paths", @@ -64,8 +65,9 @@ lsp = { package = "lsp-types", version = "0.94" } memchr = "2.5" num_cpus = "1.13" once_cell = "1.7" -percent-encoding = "2.2.0" parking_lot = "0.12" +percent-encoding = "2.2.0" +pulldown-cmark = "0.10.2" rayon = "1.5" regex = "1" rgb2ansi256 = "0.1.1" @@ -96,6 +98,7 @@ maple_config = { path = "crates/maple_config" } maple_core = { path = "crates/maple_core" } maple_derive = { path = "crates/maple_derive" } maple_lsp = { path = "crates/maple_lsp" } +maple_markdown = { path = "crates/maple_markdown" } matcher = { path = "crates/matcher" } paths = { path = "crates/paths" } pattern = { path = "crates/pattern" } diff --git a/crates/cli/src/command/grep/mod.rs b/crates/cli/src/command/grep/mod.rs index 9603d477f..ca4c9a13b 100644 --- a/crates/cli/src/command/grep/mod.rs +++ b/crates/cli/src/command/grep/mod.rs @@ -24,23 +24,31 @@ pub struct Grep { #[clap(index = 1)] grep_query: String, - /// Read input from a cached grep tempfile, only absolute file path is supported. + /// Read input from a cached grep tempfile. + /// + /// Only absolute file path is supported. #[clap(long, value_parser)] input: Option, - /// Specify the working directory of CMD + /// Specify the working directory of CMD. #[clap(long, value_parser)] cmd_dir: Option, - /// Recreate the cache, only intended for the test purpose. + /// Recreate the grep cache. + /// + /// Only intended for the test purpose. #[clap(long)] refresh_cache: bool, + /// Run the filter in parallel. + /// + /// Deprecated. #[clap(long)] par_run: bool, + /// Use the builtin searching implementation on top of libripgrep instead of the rg executable. #[clap(long)] - ripgrep: bool, + lib_ripgrep: bool, } impl Grep { @@ -55,7 +63,7 @@ impl Grep { return Ok(()); } - if self.ripgrep { + if self.lib_ripgrep { let dir = match self.cmd_dir { Some(ref dir) => dir.clone(), None => std::env::current_dir()?, diff --git a/crates/maple_config/src/lib.rs b/crates/maple_config/src/lib.rs index 2bc258766..aa1055358 100644 --- a/crates/maple_config/src/lib.rs +++ b/crates/maple_config/src/lib.rs @@ -162,6 +162,10 @@ impl Default for CursorWordConfig { pub struct MarkdownPluginConfig { /// Whether to enable this plugin. pub enable: bool, + /// Specify the port number for the preview page in browser. + /// + /// A random port will be used each time if not specified. + pub preview_port: usize, } #[derive(Serialize, Deserialize, Debug, Default, PartialEq)] diff --git a/crates/maple_core/Cargo.toml b/crates/maple_core/Cargo.toml index 567e0b4b4..37ed13bd6 100644 --- a/crates/maple_core/Cargo.toml +++ b/crates/maple_core/Cargo.toml @@ -25,7 +25,6 @@ itertools = { workspace = true } tokio = { workspace = true, features = ["fs", "rt", "process", "macros", "rt-multi-thread", "sync", "time"] } once_cell = { workspace = true } parking_lot = { workspace = true } -percent-encoding = { workspace = true } rayon = { workspace = true } regex = { workspace = true } rgb2ansi256 = { workspace = true } @@ -42,6 +41,7 @@ code_tools = { workspace = true } dirs = { workspace = true } maple_config = { workspace = true } maple_derive = { workspace = true } +maple_markdown = { workspace = true } filter = { workspace = true } icon = { workspace = true } maple_lsp = { workspace = true } diff --git a/crates/maple_core/src/searcher/grep/stoppable_searcher.rs b/crates/maple_core/src/searcher/grep/stoppable_searcher.rs index 76510ce95..e0d5bd15c 100644 --- a/crates/maple_core/src/searcher/grep/stoppable_searcher.rs +++ b/crates/maple_core/src/searcher/grep/stoppable_searcher.rs @@ -96,9 +96,8 @@ impl StoppableSearchImpl { return WalkState::Quit; } - let entry = match entry { - Ok(entry) => entry, - Err(_) => return WalkState::Continue, + let Ok(entry) = entry else { + return WalkState::Continue; }; // TODO: Add search syntax for filtering path @@ -109,6 +108,11 @@ impl StoppableSearchImpl { _ => return WalkState::Continue, }; + let relative_path = entry + .path() + .strip_prefix(&search_root) + .unwrap_or_else(|_| entry.path()); + let result = searcher.search_path( &MatchEverything, entry.path(), @@ -118,14 +122,10 @@ impl StoppableSearchImpl { return Ok(sender.send(SearcherMessage::ProcessedOne).is_ok()); } - let path = entry - .path() - .strip_prefix(&search_root) - .unwrap_or_else(|_| entry.path()); let line = line.trim(); let maybe_file_result = matcher - .match_file_result(path, line) + .match_file_result(relative_path, line) .map(|matched| FileResult { path: entry.path().to_path_buf(), line_number, @@ -147,7 +147,7 @@ impl StoppableSearchImpl { ); if let Err(err) = result { - tracing::error!("Global search error: {}, {}", entry.path().display(), err); + tracing::error!(?err, path = ?entry.path(), "Global search error"); } WalkState::Continue @@ -289,7 +289,7 @@ pub async fn search(query: String, matcher: Matcher, search_context: SearchConte .expect("Max capacity is non-zero; qed"); let new = file_result; - if let std::cmp::Ordering::Greater = new.rank.cmp(&last.rank) { + if new.rank > last.rank { *last = new; best_results.sort(); } diff --git a/crates/maple_core/src/stdio_server/plugin/markdown.rs b/crates/maple_core/src/stdio_server/plugin/markdown.rs index ceed565c9..eff3eee1f 100644 --- a/crates/maple_core/src/stdio_server/plugin/markdown.rs +++ b/crates/maple_core/src/stdio_server/plugin/markdown.rs @@ -3,205 +3,23 @@ use crate::stdio_server::input::{AutocmdEvent, AutocmdEventType, PluginAction}; use crate::stdio_server::plugin::{ClapPlugin, PluginError, Toggle}; use crate::stdio_server::vim::Vim; -use once_cell::sync::Lazy; -use percent_encoding::{percent_encode, CONTROLS}; -use regex::Regex; +use maple_markdown::toc::{find_toc_range, generate_toc}; +use maple_markdown::Message; use serde_json::json; -use std::collections::VecDeque; -use std::path::Path; -use std::str::FromStr; - -fn slugify(text: &str) -> String { - percent_encode(text.replace(' ', "-").to_lowercase().as_bytes(), CONTROLS).to_string() -} - -#[derive(Debug)] -pub struct TocConfig { - pub bullet: String, - pub indent: usize, - pub max_depth: Option, - pub min_depth: usize, - pub header: Option, - pub no_link: bool, -} - -impl Default for TocConfig { - fn default() -> Self { - TocConfig { - bullet: String::from("*"), - indent: 4, - max_depth: None, - min_depth: 1, - no_link: false, - header: Some(String::from("## Table of Contents")), - } - } -} - -#[derive(Debug)] -pub struct Heading { - pub depth: usize, - pub title: String, -} - -impl FromStr for Heading { - type Err = (); - - fn from_str(s: &str) -> Result { - let trimmed = s.trim_end(); - if trimmed.starts_with('#') { - let mut depth = 0usize; - let title = trimmed - .chars() - .skip_while(|c| { - if *c == '#' { - depth += 1; - true - } else { - false - } - }) - .collect::() - .trim_start() - .to_owned(); - Ok(Heading { - depth: depth - 1, - title, - }) - } else { - Err(()) - } - } -} - -static MARKDOWN_LINK: Lazy = Lazy::new(|| Regex::new(r"^\[(.*)\](.*)").unwrap()); - -impl Heading { - fn format(&self, config: &TocConfig) -> Option { - if self.depth >= config.min_depth - && config.max_depth.map(|d| self.depth <= d).unwrap_or(true) - { - let Self { depth, title } = self; - let indent_before_bullet = " " - .repeat(config.indent) - .repeat(depth.saturating_sub(config.min_depth)); - let bullet = &config.bullet; - let indent_after_bullet = " ".repeat(config.indent.saturating_sub(1)); - - if config.no_link { - Some(format!( - "{indent_before_bullet}{bullet}{indent_after_bullet}{title}" - )) - } else if let Some(cap) = MARKDOWN_LINK.captures(title) { - let title = cap.get(1).map(|x| x.as_str())?; - Some(format!( - "{indent_before_bullet}{bullet}{indent_after_bullet}[{title}](#{})", - slugify(title) - )) - } else { - Some(format!( - "{indent_before_bullet}{bullet}{indent_after_bullet}[{title}](#{})", - slugify(title) - )) - } - } else { - None - } - } -} - -enum CodeBlockStart { - Backticks, - Tides, -} - -fn parse_toc( - input_file: &Path, - toc_config: &TocConfig, - line_start: usize, -) -> std::io::Result> { - let mut code_fence = None; - Ok(utils::read_lines(input_file)? - .skip(line_start) - .filter_map(Result::ok) - .filter(|line| match &code_fence { - None => { - if line.starts_with("```") { - code_fence.replace(CodeBlockStart::Backticks); - false - } else if line.starts_with("~~~") { - code_fence.replace(CodeBlockStart::Tides); - false - } else { - true - } - } - Some(code_block_start) => { - match code_block_start { - CodeBlockStart::Backticks if line.starts_with("```") => { - code_fence.take(); - } - CodeBlockStart::Tides if line.starts_with("~~~") => { - code_fence.take(); - } - _ => {} - } - false - } - }) - .filter_map(|line| { - line.parse::() - .ok() - .and_then(|heading| heading.format(toc_config)) - }) - .collect()) -} - -fn generate_toc( - input_file: impl AsRef, - line_start: usize, - shiftwidth: usize, -) -> std::io::Result> { - let toc_config = TocConfig { - indent: shiftwidth, - ..Default::default() - }; - let toc = parse_toc(input_file.as_ref(), &toc_config, line_start)?; - - let mut full_toc = Vec::with_capacity(toc.len() + 4); - full_toc.push("".to_string()); - full_toc.push(Default::default()); - full_toc.extend(toc); - full_toc.push(Default::default()); - full_toc.push("".to_string()); - - Ok(full_toc.into()) -} - -fn find_toc_range(input_file: impl AsRef) -> std::io::Result> { - let mut start = 0; - - for (idx, line) in utils::read_lines(input_file)? - .map_while(Result::ok) - .enumerate() - { - let line = line.trim(); - if line == "" { - start = idx; - } else if line == "" { - return Ok(Some((start, idx))); - } else { - continue; - } - } - - Ok(None) -} - -#[derive(Debug, Clone, maple_derive::ClapPlugin)] -#[clap_plugin(id = "markdown", actions = ["generateToc", "updateToc", "deleteToc"])] +use std::collections::HashMap; + +#[derive(Debug, maple_derive::ClapPlugin)] +#[clap_plugin( + id = "markdown", + actions = [ + "generateToc", + "updateToc", + "deleteToc", + "previewInBrowser", +])] pub struct Markdown { vim: Vim, + bufs: HashMap>, toggle: Toggle, } @@ -209,7 +27,8 @@ impl Markdown { pub fn new(vim: Vim) -> Self { Self { vim, - toggle: Toggle::Off, + bufs: HashMap::new(), + toggle: Toggle::On, } } @@ -228,15 +47,52 @@ impl Markdown { #[async_trait::async_trait] impl ClapPlugin for Markdown { - fn subscriptions(&self) -> &[AutocmdEventType] { - &[] - } + #[maple_derive::subscriptions] + async fn handle_autocmd(&mut self, autocmd: AutocmdEvent) -> Result<(), PluginError> { + use AutocmdEventType::{BufDelete, BufWritePost, CursorMoved, TextChangedI}; - async fn handle_autocmd(&mut self, _autocmd: AutocmdEvent) -> Result<(), PluginError> { if self.toggle.is_off() { return Ok(()); } + if self.bufs.is_empty() { + return Ok(()); + } + + let (event_type, params) = autocmd; + let bufnr = params.parse_bufnr()?; + + match event_type { + CursorMoved => { + let scroll_persent = self.vim.line(".").await? * 100 / self.vim.line("$").await?; + if let Some(msg_tx) = self.bufs.get(&bufnr) { + msg_tx.send_replace(Message::Scroll(scroll_persent)); + } + } + BufWritePost => { + for (bufnr, msg_tx) in self.bufs.iter() { + let path = self.vim.bufabspath(bufnr).await?; + msg_tx.send_replace(Message::FileChanged(path)); + } + } + TextChangedI => { + // TODO: incremental update? + let lines = self.vim.getbufline(bufnr, 1, "$").await?; + let markdown_content = lines.join("\n"); + let html = maple_markdown::to_html(&markdown_content)?; + if let Some(msg_tx) = self.bufs.get(&bufnr) { + msg_tx.send_replace(Message::UpdateContent(html)); + } + } + BufDelete => { + if let Some(msg_tx) = self.bufs.remove(&bufnr) { + // Drop the markdown worker message sender to exit the markdown preview task. + drop(msg_tx); + } + } + event => return Err(PluginError::UnhandledEvent(event)), + } + Ok(()) } @@ -266,6 +122,30 @@ impl ClapPlugin for Markdown { self.vim.deletebufline(bufnr, start + 1, end + 1).await?; } } + MarkdownAction::PreviewInBrowser => { + let (msg_tx, msg_rx) = + tokio::sync::watch::channel(Message::UpdateContent(String::new())); + + let port = maple_config::config().plugin.markdown.preview_port; + let addr = format!("127.0.0.1:{port}"); + let listener = tokio::net::TcpListener::bind(&addr).await?; + + let bufnr = self.vim.bufnr("").await?; + + tokio::spawn(async move { + if let Err(err) = + maple_markdown::open_preview_in_browser(listener, msg_rx).await + { + tracing::error!(?err, "Failed to open markdown preview"); + } + tracing::debug!(bufnr, "markdown preview exited"); + }); + + let path = self.vim.bufabspath(bufnr).await?; + msg_tx.send_replace(Message::FileChanged(path)); + + self.bufs.insert(bufnr, msg_tx); + } } Ok(()) diff --git a/crates/maple_core/src/stdio_server/provider/impls/blines.rs b/crates/maple_core/src/stdio_server/provider/impls/blines.rs index f01f7facb..361fe5feb 100644 --- a/crates/maple_core/src/stdio_server/provider/impls/blines.rs +++ b/crates/maple_core/src/stdio_server/provider/impls/blines.rs @@ -103,13 +103,16 @@ impl BlinesProvider { let new_control = { let stop_signal = Arc::new(AtomicBool::new(false)); + let vim = ctx.vim.clone(); let join_handle = { let search_context = ctx.search_context(stop_signal.clone()); tokio::spawn(async move { + let _ = vim.bare_exec("clap#spinner#set_busy"); crate::searcher::file::search(query, source_file, matcher, search_context) .await; + let _ = vim.bare_exec("clap#spinner#set_idle"); }) }; diff --git a/crates/maple_core/src/stdio_server/provider/impls/grep.rs b/crates/maple_core/src/stdio_server/provider/impls/grep.rs index df73f3787..907c39bea 100644 --- a/crates/maple_core/src/stdio_server/provider/impls/grep.rs +++ b/crates/maple_core/src/stdio_server/provider/impls/grep.rs @@ -48,27 +48,24 @@ impl GrepProvider { .match_scope(MatchScope::Full) // Force using MatchScope::Full. .build(Query::from(&query)); - let new_control = { - let stop_signal = Arc::new(AtomicBool::new(false)); - - let vim = ctx.vim.clone(); - let mut search_context = ctx.search_context(stop_signal.clone()); - // cwd + extra paths - if self.args.base.no_cwd { - search_context.paths = self.args.paths.clone(); - } else { - search_context.paths.extend_from_slice(&self.args.paths); - } - let join_handle = tokio::spawn(async move { - let _ = vim.bare_exec("clap#spinner#set_busy"); - crate::searcher::grep::search(query, matcher, search_context).await; - let _ = vim.bare_exec("clap#spinner#set_idle"); - }); - - SearcherControl { - stop_signal, - join_handle, - } + let vim = ctx.vim.clone(); + let stop_signal = Arc::new(AtomicBool::new(false)); + let mut search_context = ctx.search_context(stop_signal.clone()); + // cwd + extra paths + if self.args.base.no_cwd { + search_context.paths = self.args.paths.clone(); + } else { + search_context.paths.extend_from_slice(&self.args.paths); + } + let join_handle = tokio::spawn(async move { + let _ = vim.bare_exec("clap#spinner#set_busy"); + crate::searcher::grep::search(query, matcher, search_context).await; + let _ = vim.bare_exec("clap#spinner#set_idle"); + }); + + let new_control = SearcherControl { + stop_signal, + join_handle, }; self.searcher_control.replace(new_control); diff --git a/crates/maple_markdown/Cargo.toml b/crates/maple_markdown/Cargo.toml new file mode 100644 index 000000000..1f3234cc3 --- /dev/null +++ b/crates/maple_markdown/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "maple_markdown" +authors.workspace = true +version.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +axum = { version = "0.7.5", features = ["ws"] } +axum-extra = { version = "0.9.3", features = ["typed-header"] } +once_cell = { workspace = true } +percent-encoding = { workspace = true } +pulldown-cmark = { workspace = true } +regex = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } +webbrowser = { workspace = true } + +utils = { workspace = true } diff --git a/crates/maple_markdown/js/index.html b/crates/maple_markdown/js/index.html new file mode 100644 index 000000000..6c2b379fb --- /dev/null +++ b/crates/maple_markdown/js/index.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + Markdown Preview + + +
+ + + + diff --git a/crates/maple_markdown/src/lib.rs b/crates/maple_markdown/src/lib.rs new file mode 100644 index 000000000..121c2034a --- /dev/null +++ b/crates/maple_markdown/src/lib.rs @@ -0,0 +1,146 @@ +pub mod toc; + +use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}; +use axum::extract::Extension; +use axum::http::StatusCode; +use axum::response::{Html, IntoResponse}; +use axum::routing::get; +use axum::Router; +use std::net::SocketAddr; +use tokio::sync::watch::Receiver; + +type Error = Box; + +/// The handler for the HTTP request (this gets called when the HTTP GET lands at the start +/// of websocket negotiation). After this completes, the actual switching from HTTP to +/// websocket protocol will occur. +/// This is the last point where we can extract TCP/IP metadata such as IP address of the client +/// as well as things from HTTP headers such as user-agent of the browser etc. +async fn ws_handler( + ws: Option, + Extension(msg_rx): Extension>, +) -> impl IntoResponse { + if let Some(ws) = ws { + ws.on_upgrade(|ws| async move { handle_websocket(ws, msg_rx).await }) + } else { + let html = include_str!("../js/index.html"); + (StatusCode::OK, Html(html)).into_response() + } +} + +async fn handle_websocket(mut socket: WebSocket, mut msg_rx: Receiver) { + while msg_rx.changed().await.is_ok() { + let msg = msg_rx.borrow().clone(); + + let Ok(text) = process_message(msg) else { + break; + }; + + if socket + .send(WsMessage::Text(text.to_string())) + .await + .is_err() + { + break; + } + } + + let _ = socket.send(WsMessage::Close(None)).await; +} + +pub fn to_html(markdown_content: &str) -> Result { + let parser = pulldown_cmark::Parser::new(markdown_content); + + let mut html_output = String::new(); + pulldown_cmark::html::push_html(&mut html_output, parser); + + Ok(html_output) +} + +fn process_message(msg: Message) -> Result { + let res = match msg { + Message::FileChanged(path) => { + let markdown_content = std::fs::read_to_string(path)?; + let html = to_html(&markdown_content)?; + serde_json::json!({ + "type": "update_content", + "data": html, + }) + } + Message::UpdateContent(content) => { + serde_json::json!({ + "type": "update_content", + "data": content, + }) + } + Message::Scroll(position) => { + serde_json::json!({ + "type": "scroll", + "data": position, + }) + } + }; + Ok(res) +} + +// Worker message that the websocket server deals with. +#[derive(Debug, Clone)] +pub enum Message { + /// Markdown file was modified. + FileChanged(String), + /// Refresh the page with given html content. + UpdateContent(String), + /// Scroll to the given position specified in a percent to the window height. + Scroll(usize), +} + +pub async fn open_preview_in_browser( + listener: tokio::net::TcpListener, + msg_rx: Receiver, +) -> Result<(), Error> { + let app = Router::new() + .route("/", get(ws_handler)) + .layer(Extension(msg_rx)); + + let port = listener.local_addr()?.port(); + + webbrowser::open(&format!("http://127.0.0.1:{port}"))?; + + tracing::debug!("Listening on {listener:?}"); + + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + #[ignore] + async fn it_works() { + let (msg_tx, msg_rx) = tokio::sync::watch::channel(Message::UpdateContent(String::new())); + + tokio::spawn(async move { + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1)); + for _i in 0..10 { + interval.tick().await; + let html = format!("Current time: {:?}", std::time::Instant::now()); + msg_tx.send_replace(Message::UpdateContent(html)); + } + }); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") + .await + .unwrap(); + + open_preview_in_browser(listener, msg_rx) + .await + .expect("Failed to open markdown preview"); + } +} diff --git a/crates/maple_markdown/src/toc.rs b/crates/maple_markdown/src/toc.rs new file mode 100644 index 000000000..33d94379d --- /dev/null +++ b/crates/maple_markdown/src/toc.rs @@ -0,0 +1,193 @@ +use once_cell::sync::Lazy; +use percent_encoding::{percent_encode, CONTROLS}; +use regex::Regex; +use std::collections::VecDeque; +use std::path::Path; +use std::str::FromStr; + +fn slugify(text: &str) -> String { + percent_encode(text.replace(' ', "-").to_lowercase().as_bytes(), CONTROLS).to_string() +} + +#[derive(Debug)] +pub struct TocConfig { + pub bullet: String, + pub indent: usize, + pub max_depth: Option, + pub min_depth: usize, + pub header: Option, + pub no_link: bool, +} + +impl Default for TocConfig { + fn default() -> Self { + Self { + bullet: String::from("*"), + indent: 4, + max_depth: None, + min_depth: 1, + no_link: false, + header: Some(String::from("## Table of Contents")), + } + } +} + +#[derive(Debug)] +pub struct Heading { + pub depth: usize, + pub title: String, +} + +impl FromStr for Heading { + type Err = (); + + fn from_str(s: &str) -> Result { + let trimmed = s.trim_end(); + if trimmed.starts_with('#') { + let mut depth = 0usize; + let title = trimmed + .chars() + .skip_while(|c| { + if *c == '#' { + depth += 1; + true + } else { + false + } + }) + .collect::() + .trim_start() + .to_owned(); + Ok(Heading { + depth: depth - 1, + title, + }) + } else { + Err(()) + } + } +} + +static MARKDOWN_LINK: Lazy = Lazy::new(|| Regex::new(r"^\[(.*)\](.*)").unwrap()); + +impl Heading { + fn format(&self, config: &TocConfig) -> Option { + if self.depth >= config.min_depth + && config.max_depth.map(|d| self.depth <= d).unwrap_or(true) + { + let Self { depth, title } = self; + let indent_before_bullet = " " + .repeat(config.indent) + .repeat(depth.saturating_sub(config.min_depth)); + let bullet = &config.bullet; + let indent_after_bullet = " ".repeat(config.indent.saturating_sub(1)); + + if config.no_link { + Some(format!( + "{indent_before_bullet}{bullet}{indent_after_bullet}{title}" + )) + } else if let Some(cap) = MARKDOWN_LINK.captures(title) { + let title = cap.get(1).map(|x| x.as_str())?; + Some(format!( + "{indent_before_bullet}{bullet}{indent_after_bullet}[{title}](#{})", + slugify(title) + )) + } else { + Some(format!( + "{indent_before_bullet}{bullet}{indent_after_bullet}[{title}](#{})", + slugify(title) + )) + } + } else { + None + } + } +} + +enum CodeBlockStart { + Backticks, + Tides, +} + +fn parse_toc( + input_file: &Path, + toc_config: &TocConfig, + line_start: usize, +) -> std::io::Result> { + let mut code_fence = None; + Ok(utils::read_lines(input_file)? + .skip(line_start) + .filter_map(Result::ok) + .filter(|line| match &code_fence { + None => { + if line.starts_with("```") { + code_fence.replace(CodeBlockStart::Backticks); + false + } else if line.starts_with("~~~") { + code_fence.replace(CodeBlockStart::Tides); + false + } else { + true + } + } + Some(code_block_start) => { + match code_block_start { + CodeBlockStart::Backticks if line.starts_with("```") => { + code_fence.take(); + } + CodeBlockStart::Tides if line.starts_with("~~~") => { + code_fence.take(); + } + _ => {} + } + false + } + }) + .filter_map(|line| { + line.parse::() + .ok() + .and_then(|heading| heading.format(toc_config)) + }) + .collect()) +} + +pub fn generate_toc( + input_file: impl AsRef, + line_start: usize, + shiftwidth: usize, +) -> std::io::Result> { + let toc_config = TocConfig { + indent: shiftwidth, + ..Default::default() + }; + let toc = parse_toc(input_file.as_ref(), &toc_config, line_start)?; + + let mut full_toc = Vec::with_capacity(toc.len() + 4); + full_toc.push("".to_string()); + full_toc.push(Default::default()); + full_toc.extend(toc); + full_toc.push(Default::default()); + full_toc.push("".to_string()); + + Ok(full_toc.into()) +} + +pub fn find_toc_range(input_file: impl AsRef) -> std::io::Result> { + let mut start = 0; + + for (idx, line) in utils::read_lines(input_file)? + .map_while(Result::ok) + .enumerate() + { + let line = line.trim(); + if line == "" { + start = idx; + } else if line == "" { + return Ok(Some((start, idx))); + } else { + continue; + } + } + + Ok(None) +} diff --git a/docs/src/plugins/plugins.md b/docs/src/plugins/plugins.md index 91f7b52af..a000d2f47 100644 --- a/docs/src/plugins/plugins.md +++ b/docs/src/plugins/plugins.md @@ -114,6 +114,7 @@ enable = true - Features - Generate/Update/Delete toc + - Open preview in browser ## syntax diff --git a/plugin/clap.vim b/plugin/clap.vim index ff7939f1b..d2db3899a 100644 --- a/plugin/clap.vim +++ b/plugin/clap.vim @@ -66,7 +66,7 @@ augroup VimClap autocmd BufWinLeave * call clap#client#notify('BufWinLeave', [+expand('')]) " Are these really needed? " autocmd TextChanged * call clap#client#notify('TextChanged', [+expand('')]) - " autocmd TextChangedI * call clap#client#notify('TextChangedI', [+expand('')]) + autocmd TextChangedI * call clap#client#notify('TextChangedI', [+expand('')]) " Create `clap_actions` provider so that it's convenient to interact with the plugins later. let g:clap_provider_clap_actions = get(g:, 'clap_provider_clap_actions', {