Skip to repository content322 lines · 10.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:34:21.713Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
bash.rs
1use anyhow::Result;
2use async_trait::async_trait;
3use collections::HashMap;
4use language::{LanguageServerName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
5use lsp::LanguageServerBinary;
6use node_runtime::{NodeRuntime, VersionStrategy};
7use project::ContextProviderWithTasks;
8use semver::Version;
9use std::{future::Future, path::PathBuf, sync::Arc, vec};
10use task::{TaskTemplate, TaskTemplates, VariableName};
11use util::{ResultExt, maybe};
12
13pub(super) fn bash_task_context() -> ContextProviderWithTasks {
14 ContextProviderWithTasks::new(TaskTemplates(vec![
15 TaskTemplate {
16 label: "execute selection".to_owned(),
17 command: VariableName::SelectedText.template_value(),
18 ..TaskTemplate::default()
19 },
20 TaskTemplate {
21 label: format!("run '{}'", VariableName::File.template_value()),
22 command: VariableName::File.template_value(),
23 tags: vec!["bash-script".to_owned()],
24 ..TaskTemplate::default()
25 },
26 ]))
27}
28
29pub struct BashLspAdapter {
30 node: NodeRuntime,
31}
32
33impl BashLspAdapter {
34 const PACKAGE_NAME: &str = "bash-language-server";
35 const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "bash-language-server/out/cli.js";
36
37 pub fn new(node: NodeRuntime) -> Self {
38 Self { node }
39 }
40
41 async fn get_cached_server_binary(
42 container_dir: PathBuf,
43 env: HashMap<String, String>,
44 node: &NodeRuntime,
45 ) -> Option<lsp::LanguageServerBinary> {
46 maybe!(async {
47 let server_path = container_dir
48 .join("node_modules")
49 .join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
50 anyhow::ensure!(
51 server_path.exists(),
52 "missing executable in directory {server_path:?}"
53 );
54 Ok(LanguageServerBinary {
55 path: node.binary_path().await?,
56 env: Some(env),
57 arguments: vec![server_path.into(), "start".into()],
58 })
59 })
60 .await
61 .log_err()
62 }
63}
64
65impl LspInstaller for BashLspAdapter {
66 type BinaryVersion = Version;
67
68 async fn cached_server_binary(
69 &self,
70 container_dir: std::path::PathBuf,
71 delegate: &dyn LspAdapterDelegate,
72 ) -> Option<lsp::LanguageServerBinary> {
73 let env = delegate.shell_env().await;
74 Self::get_cached_server_binary(container_dir, env, &self.node).await
75 }
76
77 async fn check_if_user_installed(
78 &self,
79 delegate: &Arc<dyn LspAdapterDelegate>,
80 _: Option<Toolchain>,
81 _: &gpui::AsyncApp,
82 ) -> Option<lsp::LanguageServerBinary> {
83 let path = delegate.which(Self::PACKAGE_NAME.as_ref()).await?;
84 let env = delegate.shell_env().await;
85
86 Some(LanguageServerBinary {
87 path,
88 env: Some(env),
89 arguments: vec!["start".into()],
90 })
91 }
92
93 fn check_if_version_installed(
94 &self,
95 version: &Self::BinaryVersion,
96 container_dir: &PathBuf,
97 delegate: &Arc<dyn LspAdapterDelegate>,
98 ) -> impl Send + Future<Output = Option<lsp::LanguageServerBinary>> + use<> {
99 let node = self.node.clone();
100 let version = version.clone();
101 let container_dir = container_dir.clone();
102 let delegate = delegate.clone();
103
104 async move {
105 let server_path = container_dir
106 .join("node_modules")
107 .join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
108
109 let should_install_language_server = node
110 .should_install_npm_package(
111 Self::PACKAGE_NAME,
112 &server_path,
113 &container_dir,
114 VersionStrategy::Latest(&version),
115 )
116 .await;
117
118 if should_install_language_server {
119 None
120 } else {
121 let env = delegate.shell_env().await;
122 Some(LanguageServerBinary {
123 path: node.binary_path().await.ok()?,
124 env: Some(env),
125 arguments: vec![server_path.into(), "start".into()],
126 })
127 }
128 }
129 }
130
131 async fn fetch_latest_server_version(
132 &self,
133 _: &Arc<dyn LspAdapterDelegate>,
134 _: bool,
135 _: &mut gpui::AsyncApp,
136 ) -> Result<Self::BinaryVersion> {
137 self.node
138 .npm_package_latest_version(Self::PACKAGE_NAME)
139 .await
140 }
141
142 fn fetch_server_binary(
143 &self,
144 _latest_version: Self::BinaryVersion,
145 container_dir: std::path::PathBuf,
146 delegate: &Arc<dyn LspAdapterDelegate>,
147 ) -> impl Send + Future<Output = Result<lsp::LanguageServerBinary>> + use<> {
148 let node = self.node.clone();
149 let delegate = delegate.clone();
150
151 async move {
152 let server_path = container_dir
153 .join("node_modules")
154 .join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
155
156 node.npm_install_latest_packages(&container_dir, &[Self::PACKAGE_NAME])
157 .await?;
158
159 let env = delegate.shell_env().await;
160 Ok(LanguageServerBinary {
161 path: node.binary_path().await?,
162 env: Some(env),
163 arguments: vec![server_path.into(), "start".into()],
164 })
165 }
166 }
167}
168
169#[async_trait(?Send)]
170impl LspAdapter for BashLspAdapter {
171 fn name(&self) -> LanguageServerName {
172 LanguageServerName::new_static(Self::PACKAGE_NAME)
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
179 use language::{AutoindentMode, Buffer};
180 use settings::SettingsStore;
181 use std::num::NonZeroU32;
182 use unindent::Unindent;
183 use util::test::marked_text_offsets;
184
185 #[gpui::test]
186 async fn test_bash_autoindent(cx: &mut TestAppContext) {
187 cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
188 let language = crate::language("bash", tree_sitter_bash::LANGUAGE.into());
189 cx.update(|cx| {
190 let test_settings = SettingsStore::test(cx);
191 cx.set_global(test_settings);
192 cx.update_global::<SettingsStore, _>(|store, cx| {
193 store.update_user_settings(cx, |s| {
194 s.project.all_languages.defaults.tab_size = NonZeroU32::new(2)
195 });
196 });
197 });
198
199 cx.new(|cx| {
200 let mut buffer = Buffer::local("", cx).with_language(language, cx);
201
202 let expect_indents_to =
203 |buffer: &mut Buffer, cx: &mut Context<Buffer>, input: &str, expected: &str| {
204 buffer.edit(
205 [(0..buffer.len(), input)],
206 Some(AutoindentMode::EachLine),
207 cx,
208 );
209 assert_eq!(buffer.text(), expected);
210 };
211
212 // Do not indent after shebang
213 expect_indents_to(
214 &mut buffer,
215 cx,
216 "#!/usr/bin/env bash\n#",
217 "#!/usr/bin/env bash\n#",
218 );
219
220 // indent function correctly
221 expect_indents_to(
222 &mut buffer,
223 cx,
224 "function name() {\necho \"Hello, World!\"\n}",
225 "function name() {\n echo \"Hello, World!\"\n}",
226 );
227
228 // indent if-else correctly
229 expect_indents_to(
230 &mut buffer,
231 cx,
232 "if true;then\nfoo\nelse\nbar\nfi",
233 "if true;then\n foo\nelse\n bar\nfi",
234 );
235
236 // indent if-elif-else correctly
237 expect_indents_to(
238 &mut buffer,
239 cx,
240 "if true;then\nfoo\nelif true;then\nbar\nelse\nbar\nfi",
241 "if true;then\n foo\nelif true;then\n bar\nelse\n bar\nfi",
242 );
243
244 // indent case-when-else correctly
245 expect_indents_to(
246 &mut buffer,
247 cx,
248 "case $1 in\nfoo) echo \"Hello, World!\";;\n*) echo \"Unknown argument\";;\nesac",
249 "case $1 in\n foo) echo \"Hello, World!\";;\n *) echo \"Unknown argument\";;\nesac",
250 );
251
252 // indent for-loop correctly
253 expect_indents_to(
254 &mut buffer,
255 cx,
256 "for i in {1..10};do\nfoo\ndone",
257 "for i in {1..10};do\n foo\ndone",
258 );
259
260 // indent while-loop correctly
261 expect_indents_to(
262 &mut buffer,
263 cx,
264 "while true; do\nfoo\ndone",
265 "while true; do\n foo\ndone",
266 );
267
268 // indent array correctly
269 expect_indents_to(
270 &mut buffer,
271 cx,
272 "array=(\n1\n2\n3\n)",
273 "array=(\n 1\n 2\n 3\n)",
274 );
275
276 // indents non-"function" function correctly
277 expect_indents_to(
278 &mut buffer,
279 cx,
280 "foo() {\necho \"Hello, World!\"\n}",
281 "foo() {\n echo \"Hello, World!\"\n}",
282 );
283
284 let (input, offsets) = marked_text_offsets(
285 &r#"
286 if foo; then
287 1ˇ
288 else
289 3
290 fi
291 "#
292 .unindent(),
293 );
294
295 buffer.edit([(0..buffer.len(), input)], None, cx);
296 buffer.edit(
297 [(offsets[0]..offsets[0], "\n")],
298 Some(AutoindentMode::EachLine),
299 cx,
300 );
301 buffer.edit(
302 [(offsets[0] + 3..offsets[0] + 3, "elif")],
303 Some(AutoindentMode::EachLine),
304 cx,
305 );
306 let expected = r#"
307 if foo; then
308 1
309 elif
310 else
311 3
312 fi
313 "#
314 .unindent();
315
316 pretty_assertions::assert_eq!(buffer.text(), expected);
317
318 buffer
319 });
320 }
321}
322