boxxkite_client/lsp.rs
1//! Agent-invokable language-server completions: `POST /v1/sandboxes/{id}/lsp/start`
2//! and its `/{lsp_id}/open`, `/{lsp_id}/completion`, `/{lsp_id}/stop`
3//! siblings. Mirrors `sdk-python`'s `lsp_start`/`lsp_open`/`lsp_completion`/
4//! `lsp_stop` (`sdk-go` does not wrap these).
5
6use reqwest::Method;
7use serde::{Deserialize, Serialize};
8
9use crate::client::Client;
10use crate::error::BoxxkiteError;
11
12/// `POST /v1/sandboxes/{id}/lsp/start`'s response. `lsp_id` is an opaque
13/// handle to pass to the other LSP calls.
14#[derive(Debug, Clone, Deserialize)]
15pub struct LspStartResult {
16 pub lsp_id: String,
17}
18
19/// `POST /v1/sandboxes/{id}/lsp/{lsp_id}/open`'s response.
20#[derive(Debug, Clone, Deserialize)]
21pub struct LspOpenResult {
22 pub status: String,
23}
24
25/// `POST /v1/sandboxes/{id}/lsp/{lsp_id}/completion`'s response. Each item is
26/// a raw LSP `CompletionItem` object -- left as raw JSON rather than an
27/// over-fitted struct, since the shape is the language server's, not this
28/// API's (matching how `ls`/`glob` leave their entries untyped).
29#[derive(Debug, Clone, Deserialize)]
30pub struct LspCompletionResult {
31 #[serde(default)]
32 pub items: Vec<serde_json::Value>,
33}
34
35/// `POST /v1/sandboxes/{id}/lsp/{lsp_id}/stop`'s response.
36#[derive(Debug, Clone, Deserialize)]
37pub struct LspStopResult {
38 pub status: String,
39}
40
41impl Client {
42 /// `POST /v1/sandboxes/{session_id}/lsp/start` -- start a persistent
43 /// language server (`pyright` for `"python"`,
44 /// `typescript-language-server` for `"typescript"`/`"javascript"`).
45 /// Returns the opaque `lsp_id` handle for the other LSP calls.
46 pub async fn lsp_start(
47 &self,
48 session_id: &str,
49 language: &str,
50 ) -> Result<LspStartResult, BoxxkiteError> {
51 #[derive(Serialize)]
52 struct Body<'a> {
53 language: &'a str,
54 }
55 let builder = self
56 .request(
57 Method::POST,
58 &format!("/v1/sandboxes/{session_id}/lsp/start"),
59 )
60 .json(&Body { language });
61 self.send(builder).await
62 }
63
64 /// `POST /v1/sandboxes/{session_id}/lsp/{lsp_id}/open` -- open (or
65 /// full-document-replace) a document on a running language server.
66 pub async fn lsp_open(
67 &self,
68 session_id: &str,
69 lsp_id: &str,
70 path: &str,
71 content: &str,
72 ) -> Result<LspOpenResult, BoxxkiteError> {
73 #[derive(Serialize)]
74 struct Body<'a> {
75 path: &'a str,
76 content: &'a str,
77 }
78 let builder = self
79 .request(
80 Method::POST,
81 &format!("/v1/sandboxes/{session_id}/lsp/{lsp_id}/open"),
82 )
83 .json(&Body { path, content });
84 self.send(builder).await
85 }
86
87 /// `POST /v1/sandboxes/{session_id}/lsp/{lsp_id}/completion` -- request
88 /// completions at a 0-indexed `(line, character)` position. `path` must
89 /// already be open on this handle (see [`Client::lsp_open`]).
90 pub async fn lsp_completion(
91 &self,
92 session_id: &str,
93 lsp_id: &str,
94 path: &str,
95 line: u32,
96 character: u32,
97 ) -> Result<LspCompletionResult, BoxxkiteError> {
98 #[derive(Serialize)]
99 struct Body<'a> {
100 path: &'a str,
101 line: u32,
102 character: u32,
103 }
104 let builder = self
105 .request(
106 Method::POST,
107 &format!("/v1/sandboxes/{session_id}/lsp/{lsp_id}/completion"),
108 )
109 .json(&Body {
110 path,
111 line,
112 character,
113 });
114 self.send(builder).await
115 }
116
117 /// `POST /v1/sandboxes/{session_id}/lsp/{lsp_id}/stop` -- gracefully shut
118 /// down a running language server.
119 pub async fn lsp_stop(
120 &self,
121 session_id: &str,
122 lsp_id: &str,
123 ) -> Result<LspStopResult, BoxxkiteError> {
124 let builder = self.request(
125 Method::POST,
126 &format!("/v1/sandboxes/{session_id}/lsp/{lsp_id}/stop"),
127 );
128 self.send(builder).await
129 }
130}