Skip to content

Commit 0f00599

Browse files
author
Yogthos
committed
LSP Phase 8: feature gate + Channels struct refactor
Two related changes — splitting into separate commits would have required disentangling overlapping diffs in the same files. ## Refactor: build_channels returns Channels struct 10-tuple → `struct Channels { ... }`. Same fields, just named. build_channels returns Channels; main.rs destructures the same locals it already used. No behavior change, but the call site is no longer a positional 10-tuple unpacking that breaks every time we add a slot. Doc-noted in main.rs: `compile_lsp_commands` currently ignores the `extensions` field on per-server overrides. The claimed-extensions list lives in the static `builtin_servers()` registry; making it instance-overridable requires plumbing a per-session server set down through LspManager. Follow-up; users who need new extensions today must edit `server.rs`. ## Phase 8: feature gate Adds `feature = "lsp"` to Cargo.toml's default set. With it off: - `lsp` module is not compiled; `lsp-types` dep stays optional + skipped. - `Channels.lsp_manager` field gated out (also gated out of destructure in main.rs). - `build_agent` / `build_agent_inner` / `run_interactive` drop their `lsp_manager` arg via `#[cfg(feature = "lsp")]` on the param. - Read/Write/Edit tools drop their `lsp_manager` field + integration call. - LspTool not registered; `--no-lsp` CLI flag not exposed. - `LspConfig` / `LspServerConfig` types gated; `cfg.lsp` field gated. - ACP / slash sub-rebuilds / plan-switch all use `#[cfg(feature = "lsp")]` None args. Verified: - `cargo build --no-default-features --features 'loop git-worktree mcp'` → clean (no lsp deps pulled in). - `cargo build` (default) → clean, all 4 LSP servers wired. - `cargo test --no-default-features --features 'loop git-worktree mcp'` → 305 passing (LSP module's 121 tests correctly excluded). - `cargo test` (default) → 426 passing.
1 parent 68aef91 commit 0f00599

13 files changed

Lines changed: 125 additions & 51 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ readme = "README.md"
1111
keywords = ["ai", "cli", "dev"]
1212

1313
[features]
14-
default = ['loop', 'git-worktree', 'mcp']
14+
default = ['loop', 'git-worktree', 'mcp', 'lsp']
1515
loop = []
1616
git-worktree = []
1717
mcp = [
@@ -26,6 +26,7 @@ semantic-ts = ["semantic", "dep:tree-sitter-typescript"]
2626
semantic-python = ["semantic", "dep:tree-sitter-python"]
2727
semantic-bash = ["semantic", "dep:tree-sitter-bash"]
2828
plugin = ["dep:janetrs"]
29+
lsp = ["dep:lsp-types"]
2930

3031
[dependencies]
3132
rig = { version = "0.37", features = ["rmcp"] }
@@ -66,7 +67,7 @@ streaming-iterator = { version = "0.1", optional = true }
6667
janetrs = { version = "0.8", optional = true }
6768
html2text = "0.17"
6869
indexmap = "2"
69-
lsp-types = "0.97"
70+
lsp-types = { version = "0.97", optional = true }
7071

7172
[dev-dependencies]
7273
tokio = { version = "1", features = ["test-util"] }

src/agent/builder.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub async fn build_agent_inner<M: CompletionModel + 'static>(
3737
question_tx: Option<QuestionSender>,
3838
plan_tx: Option<PlanSwitchSender>,
3939
bg_store: Option<BackgroundStore>,
40-
lsp_manager: Option<std::sync::Arc<crate::lsp::manager::LspManager>>,
40+
#[cfg(feature = "lsp")] lsp_manager: Option<std::sync::Arc<crate::lsp::manager::LspManager>>,
4141
sandbox: Sandbox,
4242
parent_model: Option<AnyModel>,
4343
#[cfg(feature = "mcp")] mcp_manager: Option<&McpClientManager>,
@@ -141,20 +141,23 @@ pub async fn build_agent_inner<M: CompletionModel + 'static>(
141141
permission.clone(),
142142
ask_tx.clone(),
143143
cache.clone(),
144+
#[cfg(feature = "lsp")]
144145
lsp_manager.clone(),
145146
)),
146147
Box::new(tools::WriteTool::with_cache(
147148
permission.clone(),
148149
ask_tx.clone(),
149150
plan_file.clone(),
150151
cache.clone(),
152+
#[cfg(feature = "lsp")]
151153
lsp_manager.clone(),
152154
)),
153155
Box::new(tools::EditTool::with_cache(
154156
permission.clone(),
155157
ask_tx.clone(),
156158
plan_file.clone(),
157159
cache.clone(),
160+
#[cfg(feature = "lsp")]
158161
lsp_manager.clone(),
159162
)),
160163
Box::new(tools::BashTool::with_cache(
@@ -262,6 +265,7 @@ pub async fn build_agent_inner<M: CompletionModel + 'static>(
262265
builder = builder.tools(vec![task_tool, status_tool]);
263266
}
264267

268+
#[cfg(feature = "lsp")]
265269
if let Some(manager) = &lsp_manager {
266270
let cwd = std::env::current_dir().unwrap_or_else(|_| ".".into());
267271
let lsp_tool = Box::new(tools::LspTool::new(

src/agent/tools/edit.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use std::path::PathBuf;
2+
#[cfg(feature = "lsp")]
23
use std::sync::Arc;
34

45
use rig::completion::ToolDefinition;
56
use rig::tool::Tool;
67

78
use crate::agent::tools::cache::ToolCache;
89
use crate::agent::tools::{AskSender, EditArgs, PermCheck, ToolError, check_perm_path};
10+
#[cfg(feature = "lsp")]
911
use crate::lsp::manager::LspManager;
1012

1113
pub struct EditTool {
@@ -16,6 +18,7 @@ pub struct EditTool {
1618
/// When set, the tool touches the edited file on the LSP server and
1719
/// appends any diagnostic block to its output. `None` reproduces the
1820
/// pre-LSP behaviour.
21+
#[cfg(feature = "lsp")]
1922
lsp_manager: Option<Arc<LspManager>>,
2023
}
2124

@@ -31,6 +34,7 @@ impl EditTool {
3134
ask_tx,
3235
plan_file,
3336
cache: None,
37+
#[cfg(feature = "lsp")]
3438
lsp_manager: None,
3539
}
3640
}
@@ -40,13 +44,14 @@ impl EditTool {
4044
ask_tx: Option<AskSender>,
4145
plan_file: Option<PathBuf>,
4246
cache: ToolCache,
43-
lsp_manager: Option<Arc<LspManager>>,
47+
#[cfg(feature = "lsp")] lsp_manager: Option<Arc<LspManager>>,
4448
) -> Self {
4549
EditTool {
4650
permission,
4751
ask_tx,
4852
plan_file,
4953
cache: Some(cache),
54+
#[cfg(feature = "lsp")]
5055
lsp_manager,
5156
}
5257
}
@@ -212,6 +217,7 @@ impl Tool for EditTool {
212217
new_content
213218
};
214219

220+
#[cfg(feature = "lsp")]
215221
let write_at = std::time::Instant::now();
216222
tokio::fs::write(&args.path, &output).await?;
217223
// File mutated → invalidate cached reads/greps/listings for this turn.
@@ -236,15 +242,18 @@ impl Tool for EditTool {
236242
));
237243
}
238244

239-
let path = std::path::Path::new(&args.path);
240-
result.push_str(
241-
&crate::agent::tools::write::append_lsp_block(
242-
self.lsp_manager.as_ref(),
243-
path,
244-
write_at,
245-
)
246-
.await,
247-
);
245+
#[cfg(feature = "lsp")]
246+
{
247+
let path = std::path::Path::new(&args.path);
248+
result.push_str(
249+
&crate::agent::tools::write::append_lsp_block(
250+
self.lsp_manager.as_ref(),
251+
path,
252+
write_at,
253+
)
254+
.await,
255+
);
256+
}
248257
Ok(result)
249258
}
250259
}

src/agent/tools/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod find_files;
77
mod glob;
88
mod grep;
99
mod list_dir;
10+
#[cfg(feature = "lsp")]
1011
mod lsp;
1112
mod memory;
1213
pub(crate) mod plan;
@@ -30,6 +31,7 @@ pub use find_files::FindFilesTool;
3031
pub use glob::GlobTool;
3132
pub use grep::GrepTool;
3233
pub use list_dir::ListDirTool;
34+
#[cfg(feature = "lsp")]
3335
pub use lsp::LspTool;
3436
pub use memory::MemoryTool;
3537
pub use plan::{PlanEnterTool, PlanExitTool};

src/agent/tools/read.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
#[cfg(feature = "lsp")]
12
use std::sync::Arc;
23

34
use rig::completion::ToolDefinition;
45
use rig::tool::Tool;
56

67
use crate::agent::tools::cache::ToolCache;
78
use crate::agent::tools::{AskSender, PermCheck, ReadArgs, ToolError, check_perm_path};
9+
#[cfg(feature = "lsp")]
810
use crate::lsp::manager::{LspManager, TouchMode};
911

1012
pub struct ReadTool {
@@ -14,6 +16,7 @@ pub struct ReadTool {
1416
/// When set, the tool fires off a `touch_file` to warm the LSP server
1517
/// so subsequent edits surface diagnostics quickly. Fire-and-forget:
1618
/// the read tool does not wait or surface diagnostics in its output.
19+
#[cfg(feature = "lsp")]
1720
pub lsp_manager: Option<Arc<LspManager>>,
1821
}
1922

@@ -24,6 +27,7 @@ impl ReadTool {
2427
permission,
2528
ask_tx,
2629
cache: None,
30+
#[cfg(feature = "lsp")]
2731
lsp_manager: None,
2832
}
2933
}
@@ -32,12 +36,13 @@ impl ReadTool {
3236
permission: Option<PermCheck>,
3337
ask_tx: Option<AskSender>,
3438
cache: ToolCache,
35-
lsp_manager: Option<Arc<LspManager>>,
39+
#[cfg(feature = "lsp")] lsp_manager: Option<Arc<LspManager>>,
3640
) -> Self {
3741
ReadTool {
3842
permission,
3943
ask_tx,
4044
cache: Some(cache),
45+
#[cfg(feature = "lsp")]
4146
lsp_manager,
4247
}
4348
}
@@ -122,6 +127,7 @@ impl Tool for ReadTool {
122127
// Fire-and-forget LSP warmup so the server already has the file
123128
// open by the time the agent edits it (and we can wait_for_push
124129
// quickly). No diagnostic surfacing on read.
130+
#[cfg(feature = "lsp")]
125131
if let Some(manager) = self.lsp_manager.clone() {
126132
let path = std::path::PathBuf::from(&args.path);
127133
tokio::spawn(async move {

src/agent/tools/write.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
use std::path::{Path, PathBuf};
2+
#[cfg(feature = "lsp")]
23
use std::sync::Arc;
4+
#[cfg(feature = "lsp")]
35
use std::time::{Duration, Instant};
46

57
use rig::completion::ToolDefinition;
68
use rig::tool::Tool;
79

810
use crate::agent::tools::cache::ToolCache;
911
use crate::agent::tools::{AskSender, PermCheck, ToolError, WriteArgs, check_perm_path};
12+
#[cfg(feature = "lsp")]
1013
use crate::lsp::diagnostic;
14+
#[cfg(feature = "lsp")]
1115
use crate::lsp::manager::{LspManager, TouchMode};
1216

1317
/// How long to wait for the LSP server to publish fresh diagnostics after
1418
/// a write. Matches opencode's `DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS`. Bounded
1519
/// so a stuck server doesn't hold up the agent's turn.
20+
#[cfg(feature = "lsp")]
1621
const DIAGNOSTIC_WAIT: Duration = Duration::from_secs(10);
1722

1823
pub struct WriteTool {
@@ -23,6 +28,7 @@ pub struct WriteTool {
2328
/// When set, the tool touches the file on the LSP server after writing
2429
/// and appends any resulting diagnostic block to its output. `None`
2530
/// reproduces the pre-LSP behaviour exactly.
31+
#[cfg(feature = "lsp")]
2632
lsp_manager: Option<Arc<LspManager>>,
2733
}
2834

@@ -38,6 +44,7 @@ impl WriteTool {
3844
ask_tx,
3945
plan_file,
4046
cache: None,
47+
#[cfg(feature = "lsp")]
4148
lsp_manager: None,
4249
}
4350
}
@@ -47,13 +54,14 @@ impl WriteTool {
4754
ask_tx: Option<AskSender>,
4855
plan_file: Option<PathBuf>,
4956
cache: ToolCache,
50-
lsp_manager: Option<Arc<LspManager>>,
57+
#[cfg(feature = "lsp")] lsp_manager: Option<Arc<LspManager>>,
5158
) -> Self {
5259
WriteTool {
5360
permission,
5461
ask_tx,
5562
plan_file,
5663
cache: Some(cache),
64+
#[cfg(feature = "lsp")]
5765
lsp_manager,
5866
}
5967
}
@@ -106,14 +114,17 @@ impl Tool for WriteTool {
106114
tokio::fs::create_dir_all(parent).await?;
107115
}
108116
let bytes = args.content.len();
117+
#[cfg(feature = "lsp")]
109118
let write_at = Instant::now();
110119
tokio::fs::write(path, &args.content).await?;
111120
// File mutated → invalidate cached reads/greps/listings for this turn.
112121
if let Some(ref cache) = self.cache {
113122
cache.clear();
114123
}
115124

125+
#[allow(unused_mut)]
116126
let mut output = format!("Written {} bytes to {}", bytes, args.path);
127+
#[cfg(feature = "lsp")]
117128
output.push_str(&append_lsp_block(self.lsp_manager.as_ref(), path, write_at).await);
118129
Ok(output)
119130
}
@@ -124,6 +135,7 @@ impl Tool for WriteTool {
124135
/// Errors during touch/wait are intentionally swallowed — diagnostic
125136
/// surfacing is a side-effect; the write tool's primary contract is
126137
/// "wrote the file".
138+
#[cfg(feature = "lsp")]
127139
pub(crate) async fn append_lsp_block(
128140
manager: Option<&Arc<LspManager>>,
129141
path: &Path,
@@ -145,7 +157,7 @@ pub(crate) async fn append_lsp_block(
145157
diagnostic::build_report_block(path, &diagnostics)
146158
}
147159

148-
#[cfg(test)]
160+
#[cfg(all(test, feature = "lsp"))]
149161
mod tests {
150162
use super::*;
151163
use crate::agent::tools::cache::ToolCache;

src/cli.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub struct Cli {
4848
#[arg(long = "no-tools", help = "Disable all tools")]
4949
pub no_tools: bool,
5050

51+
#[cfg(feature = "lsp")]
5152
#[arg(
5253
long = "no-lsp",
5354
help = "Disable LSP integration (no diagnostics on edit/write, no `lsp` agent tool)"
@@ -167,6 +168,7 @@ impl Cli {
167168
self.no_tools || cfg.no_tools.unwrap_or(false)
168169
}
169170

171+
#[cfg(feature = "lsp")]
170172
pub fn resolve_lsp_enabled(&self, cfg: &config::Config) -> bool {
171173
if self.no_lsp || self.no_tools {
172174
return false;

src/config/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub struct ToolsConfig {
3232
/// - `{ "disabled": true }` to turn off a built-in server entirely.
3333
/// - any subset of `{ command, extensions, env, initialization, disabled }`
3434
/// to override pieces of the default.
35+
#[cfg(feature = "lsp")]
3536
#[derive(Debug, Default, Clone, Deserialize)]
3637
#[serde(default)]
3738
pub struct LspServerConfig {
@@ -46,13 +47,15 @@ pub struct LspServerConfig {
4647
/// `lsp = false` → disable LSP entirely.
4748
/// `lsp = { server-id = { … } }` → enable defaults, overriding the named
4849
/// servers with the provided config.
50+
#[cfg(feature = "lsp")]
4951
#[derive(Debug, Clone, Deserialize)]
5052
#[serde(untagged)]
5153
pub enum LspConfig {
5254
Enabled(bool),
5355
Servers(HashMap<String, LspServerConfig>),
5456
}
5557

58+
#[cfg(feature = "lsp")]
5659
impl LspConfig {
5760
/// `true` when LSP should be on. Defaults to enabled.
5861
pub fn is_enabled(&self) -> bool {
@@ -102,6 +105,7 @@ pub struct Config {
102105
pub tool_result_max_chars: Option<usize>,
103106
pub default_prompt: Option<String>,
104107
pub tools: Option<ToolsConfig>,
108+
#[cfg(feature = "lsp")]
105109
pub lsp: Option<LspConfig>,
106110
#[cfg(feature = "mcp")]
107111
pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
@@ -194,7 +198,7 @@ pub fn load() -> Config {
194198
cfg
195199
}
196200

197-
#[cfg(test)]
201+
#[cfg(all(test, feature = "lsp"))]
198202
mod tests {
199203
use super::*;
200204

src/extras/acp/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ async fn run_prompt(
170170
None,
171171
None,
172172
None,
173+
#[cfg(feature = "lsp")]
173174
None,
174175
sandbox,
175176
#[cfg(feature = "mcp")]

0 commit comments

Comments
 (0)