Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions extension.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ languages = ["Ruby"]
name = "Steep"
languages = ["Ruby"]

[language_servers.sorbet]
name = "Sorbet"
languages = ["Ruby"]

[grammars.ruby]
repository = "https://github.com/tree-sitter/tree-sitter-ruby"
commit = "71bd32fb7607035768799732addba884a37a6210"
Expand Down
72 changes: 55 additions & 17 deletions src/language_servers/language_server.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[cfg(test)]
use std::collections::HashMap;

use crate::{bundler::Bundler, command_executor::RealCommandExecutor, gemset::Gemset};
use zed_extension_api::{self as zed};

Expand All @@ -8,35 +11,52 @@ pub struct LanguageServerBinary {
pub env: Option<Vec<(String, String)>>,
}

#[derive(Clone, Debug, Default)]
pub struct LspBinarySettings {
#[allow(dead_code)]
pub path: Option<String>,
pub arguments: Option<Vec<String>>,
}

pub trait WorktreeLike {
#[allow(dead_code)]
fn root_path(&self) -> String;

#[allow(dead_code)]
fn shell_env(&self) -> Vec<(String, String)>;

#[allow(dead_code)]
fn read_text_file(&self, path: &str) -> Result<String, String>;
fn lsp_binary_settings(&self, server_id: &str) -> Result<Option<LspBinarySettings>, String>;
}

impl WorktreeLike for zed::Worktree {
fn root_path(&self) -> String {
self.root_path()
zed::Worktree::root_path(self)
}

fn shell_env(&self) -> Vec<(String, String)> {
self.shell_env()
zed::Worktree::shell_env(self)
}

fn read_text_file(&self, path: &str) -> Result<String, String> {
self.read_text_file(path)
zed::Worktree::read_text_file(self, path)
}

fn lsp_binary_settings(&self, server_id: &str) -> Result<Option<LspBinarySettings>, String> {
match zed::settings::LspSettings::for_worktree(server_id, self) {
Ok(lsp_settings) => Ok(lsp_settings.binary.map(|b| LspBinarySettings {
path: b.path,
arguments: b.arguments,
})),
Err(e) => Err(e),
}
}
}

#[cfg(test)]
pub struct FakeWorktree {
root_path: String,
shell_env: Vec<(String, String)>,
files: HashMap<String, Result<String, String>>,
lsp_binary_settings_map: HashMap<String, Result<Option<LspBinarySettings>, String>>,
}

#[cfg(test)]
Expand All @@ -45,11 +65,21 @@ impl FakeWorktree {
FakeWorktree {
root_path,
shell_env: Vec::new(),
files: HashMap::new(),
lsp_binary_settings_map: HashMap::new(),
}
}

fn read_text_file(&self, _path: &str) -> Result<String, String> {
Ok(String::new())
pub fn add_file(&mut self, path: String, content: Result<String, String>) {
self.files.insert(path, content);
}

pub fn add_lsp_binary_setting(
&mut self,
server_id: String,
settings: Result<Option<LspBinarySettings>, String>,
) {
self.lsp_binary_settings_map.insert(server_id, settings);
}
}

Expand All @@ -64,7 +94,17 @@ impl WorktreeLike for FakeWorktree {
}

fn read_text_file(&self, path: &str) -> Result<String, String> {
self.read_text_file(path)
self.files
.get(path)
.cloned()
.unwrap_or_else(|| Err(format!("File not found in mock: {}", path)))
}

fn lsp_binary_settings(&self, server_id: &str) -> Result<Option<LspBinarySettings>, String> {
self.lsp_binary_settings_map
.get(server_id)
.cloned()
.unwrap_or(Ok(None))
}
}

Expand Down Expand Up @@ -104,11 +144,11 @@ pub trait LanguageServer {
let lsp_settings =
zed::settings::LspSettings::for_worktree(language_server_id.as_ref(), worktree)?;

if let Some(binary_settings) = lsp_settings.binary {
if let Some(path) = binary_settings.path {
if let Some(binary_settings) = &lsp_settings.binary {
if let Some(path) = &binary_settings.path {
return Ok(LanguageServerBinary {
path,
args: binary_settings.arguments,
path: path.clone(),
args: binary_settings.arguments.clone(),
env: Some(worktree.shell_env()),
});
}
Expand All @@ -133,7 +173,7 @@ pub trait LanguageServer {
Ok(_version) => {
let bundle_path = worktree
.which("bundle")
.ok_or("Unable to find 'bundle' command: e")?;
.ok_or_else(|| "Unable to find 'bundle' command".to_string())?;

Ok(LanguageServerBinary {
path: bundle_path,
Expand Down Expand Up @@ -236,9 +276,7 @@ pub trait LanguageServer {

#[cfg(test)]
mod tests {
use crate::language_servers::language_server::FakeWorktree;

use super::{LanguageServer, WorktreeLike};
use super::{FakeWorktree, LanguageServer, WorktreeLike};

struct TestServer {}

Expand Down
2 changes: 2 additions & 0 deletions src/language_servers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ mod language_server;
mod rubocop;
mod ruby_lsp;
mod solargraph;
mod sorbet;
mod steep;

pub use language_server::LanguageServer;
pub use rubocop::*;
pub use ruby_lsp::*;
pub use solargraph::*;
pub use sorbet::*;
pub use steep::*;
155 changes: 155 additions & 0 deletions src/language_servers/sorbet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use super::{language_server::WorktreeLike, LanguageServer};

pub struct Sorbet {}

impl LanguageServer for Sorbet {
const SERVER_ID: &str = "sorbet";
const EXECUTABLE_NAME: &str = "srb";
const GEM_NAME: &str = "sorbet";

fn get_executable_args<T: WorktreeLike>(&self, worktree: &T) -> Vec<String> {
let binary_settings = worktree
.lsp_binary_settings(Self::SERVER_ID)
.unwrap_or_default();

let default_args = vec![
"tc".to_string(),
"--lsp".to_string(),
"--enable-experimental-lsp-document-highlight".to_string(),
];

// test if sorbet/config is present
match worktree.read_text_file("sorbet/config") {
Ok(_) => {
// Config file exists, prefer custom arguments if available.
binary_settings
.and_then(|bs| bs.arguments)
.unwrap_or(default_args)
}
Err(_) => {
// gross, but avoid sorbet errors in a non-sorbet
// environment by using an empty config
vec![
"tc".to_string(),
"--lsp".to_string(),
"--dir".to_string(),
"./".to_string(),
]
}
}
}
}

impl Sorbet {
pub fn new() -> Self {
Self {}
}
}

#[cfg(test)]
mod tests {
use crate::language_servers::{
language_server::{FakeWorktree, LspBinarySettings},
LanguageServer, Sorbet,
};

#[test]
fn test_server_id() {
assert_eq!(Sorbet::SERVER_ID, "sorbet");
}

#[test]
fn test_executable_name() {
assert_eq!(Sorbet::EXECUTABLE_NAME, "srb");
}

#[test]
fn test_executable_args_no_config_file() {
let sorbet = Sorbet::new();
let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string());

fake_worktree.add_file(
"sorbet/config".to_string(),
Err("File not found".to_string()),
);
fake_worktree.add_lsp_binary_setting(Sorbet::SERVER_ID.to_string(), Ok(None));

let expected_args_no_config = vec![
"tc".to_string(),
"--lsp".to_string(),
"--dir".to_string(),
"./".to_string(),
];
assert_eq!(
sorbet.get_executable_args(&fake_worktree),
expected_args_no_config,
"Should use fallback arguments when sorbet/config is not found"
);
}

#[test]
fn test_executable_args_with_config_and_custom_settings() {
let sorbet = Sorbet::new();
let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string());

fake_worktree.add_file("sorbet/config".to_string(), Ok("--dir\n.".to_string()));

let custom_args = vec!["--custom-arg1".to_string(), "value1".to_string()];
fake_worktree.add_lsp_binary_setting(
Sorbet::SERVER_ID.to_string(),
Ok(Some(LspBinarySettings {
path: None,
arguments: Some(custom_args.clone()),
})),
);

assert_eq!(
sorbet.get_executable_args(&fake_worktree),
custom_args,
"Should use custom arguments when config and settings are present"
);
}

#[test]
fn test_executable_args_with_config_no_custom_settings() {
let sorbet = Sorbet::new();
let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string());

fake_worktree.add_file("sorbet/config".to_string(), Ok("--dir\n.".to_string()));
fake_worktree.add_lsp_binary_setting(Sorbet::SERVER_ID.to_string(), Ok(None));

let expected_default_args = vec![
"tc".to_string(),
"--lsp".to_string(),
"--enable-experimental-lsp-document-highlight".to_string(),
];
assert_eq!(
sorbet.get_executable_args(&fake_worktree),
expected_default_args,
"Should use default arguments when config is present but no custom settings"
);
}

#[test]
fn test_executable_args_with_config_lsp_settings_is_empty_struct() {
let sorbet = Sorbet::new();
let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string());

fake_worktree.add_file("sorbet/config".to_string(), Ok("--dir\n.".to_string()));
fake_worktree.add_lsp_binary_setting(
Sorbet::SERVER_ID.to_string(),
Ok(Some(LspBinarySettings::default())),
);

let expected_default_args = vec![
"tc".to_string(),
"--lsp".to_string(),
"--enable-experimental-lsp-document-highlight".to_string(),
];
assert_eq!(
sorbet.get_executable_args(&fake_worktree),
expected_default_args,
"Should use default arguments when config is present and LSP settings have no arguments"
);
}
}
7 changes: 6 additions & 1 deletion src/ruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ mod command_executor;
mod gemset;
mod language_servers;

use language_servers::{LanguageServer, Rubocop, RubyLsp, Solargraph, Steep};
use language_servers::{LanguageServer, Rubocop, RubyLsp, Solargraph, Sorbet, Steep};
use zed_extension_api::{self as zed};

#[derive(Default)]
struct RubyExtension {
solargraph: Option<Solargraph>,
ruby_lsp: Option<RubyLsp>,
rubocop: Option<Rubocop>,
sorbet: Option<Sorbet>,
steep: Option<Steep>,
}

Expand All @@ -37,6 +38,10 @@ impl zed::Extension for RubyExtension {
let rubocop = self.rubocop.get_or_insert_with(Rubocop::new);
rubocop.language_server_command(language_server_id, worktree)
}
Sorbet::SERVER_ID => {
let sorbet = self.sorbet.get_or_insert_with(Sorbet::new);
sorbet.language_server_command(language_server_id, worktree)
}
Steep::SERVER_ID => {
let steep = self.steep.get_or_insert_with(Steep::new);
steep.language_server_command(language_server_id, worktree)
Expand Down