Add settings to remote servers, use XDG paths on remote, and enable node LSPs (#19176)

Supersedes https://github.com/zed-industries/zed/pull/19166

TODO:
- [x] Update basic zed paths
- [x] update create_state_directory
- [x] Use this with `NodeRuntime`
- [x] Add server settings
- [x] Add an 'open server settings command'
- [x] Make sure it all works


Release Notes:

- Updated the actions `zed::OpenLocalSettings` and `zed::OpenLocalTasks`
to `zed::OpenProjectSettings` and `zed::OpenProjectTasks`.

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
This commit is contained in:
Mikayla Maki 2024-10-15 23:32:44 -07:00 committed by GitHub
parent 1dda039f38
commit f944ebc4cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 804 additions and 218 deletions

View file

@ -22,6 +22,7 @@ debug-embed = ["dep:rust-embed"]
test-support = ["fs/test-support"]
[dependencies]
async-watch.workspace = true
anyhow.workspace = true
backtrace = "0.3"
clap.workspace = true
@ -30,13 +31,16 @@ env_logger.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
language.workspace = true
languages.workspace = true
log.workspace = true
lsp.workspace = true
node_runtime.workspace = true
project.workspace = true
paths = { workspace = true }
remote.workspace = true
reqwest_client.workspace = true
rpc.workspace = true
rust-embed = { workspace = true, optional = true, features = ["debug-embed"] }
serde.workspace = true
@ -66,4 +70,4 @@ cargo_toml.workspace = true
toml.workspace = true
[package.metadata.cargo-machete]
ignored = ["rust-embed"]
ignored = ["rust-embed", "paths"]

View file

@ -1,6 +1,7 @@
use anyhow::{anyhow, Result};
use fs::Fs;
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
use http_client::HttpClient;
use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
use node_runtime::NodeRuntime;
use project::{
@ -16,6 +17,8 @@ use rpc::{
proto::{self, SSH_PEER_ID, SSH_PROJECT_ID},
AnyProtoClient, TypedEnvelope,
};
use settings::initial_server_settings_content;
use smol::stream::StreamExt;
use std::{
path::{Path, PathBuf},
@ -36,6 +39,14 @@ pub struct HeadlessProject {
pub languages: Arc<LanguageRegistry>,
}
pub struct HeadlessAppState {
pub session: Arc<ChannelClient>,
pub fs: Arc<dyn Fs>,
pub http_client: Arc<dyn HttpClient>,
pub node_runtime: NodeRuntime,
pub languages: Arc<LanguageRegistry>,
}
impl HeadlessProject {
pub fn init(cx: &mut AppContext) {
settings::init(cx);
@ -43,11 +54,16 @@ impl HeadlessProject {
project::Project::init_settings(cx);
}
pub fn new(session: Arc<ChannelClient>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
let node_runtime = NodeRuntime::unavailable();
pub fn new(
HeadlessAppState {
session,
fs,
http_client,
node_runtime,
languages,
}: HeadlessAppState,
cx: &mut ModelContext<Self>,
) -> Self {
languages::init(languages.clone(), node_runtime.clone(), cx);
let worktree_store = cx.new_model(|cx| {
@ -99,7 +115,7 @@ impl HeadlessProject {
prettier_store.clone(),
environment,
languages.clone(),
None,
http_client,
fs.clone(),
cx,
);
@ -139,6 +155,7 @@ impl HeadlessProject {
client.add_model_request_handler(Self::handle_open_buffer_by_path);
client.add_model_request_handler(Self::handle_find_search_candidates);
client.add_model_request_handler(Self::handle_open_server_settings);
client.add_model_request_handler(BufferStore::handle_update_buffer);
client.add_model_message_handler(BufferStore::handle_close_buffer);
@ -203,6 +220,15 @@ impl HeadlessProject {
})
.log_err();
}
LspStoreEvent::Notification(message) => {
self.session
.send(proto::Toast {
project_id: SSH_PROJECT_ID,
notification_id: "lsp".to_string(),
message: message.clone(),
})
.log_err();
}
LspStoreEvent::LanguageServerLog(language_server_id, log_type, message) => {
self.session
.send(proto::LanguageServerLog {
@ -336,6 +362,59 @@ impl HeadlessProject {
})
}
pub async fn handle_open_server_settings(
this: Model<Self>,
_: TypedEnvelope<proto::OpenServerSettings>,
mut cx: AsyncAppContext,
) -> Result<proto::OpenBufferResponse> {
let settings_path = paths::settings_file();
let (worktree, path) = this
.update(&mut cx, |this, cx| {
this.worktree_store.update(cx, |worktree_store, cx| {
worktree_store.find_or_create_worktree(settings_path, false, cx)
})
})?
.await?;
let (buffer, buffer_store) = this.update(&mut cx, |this, cx| {
let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
buffer_store.open_buffer(
ProjectPath {
worktree_id: worktree.read(cx).id(),
path: path.into(),
},
cx,
)
});
(buffer, this.buffer_store.clone())
})?;
let buffer = buffer.await?;
let buffer_id = cx.update(|cx| {
if buffer.read(cx).is_empty() {
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, initial_server_settings_content())], None, cx)
});
}
let buffer_id = buffer.read_with(cx, |b, _| b.remote_id());
buffer_store.update(cx, |buffer_store, cx| {
buffer_store
.create_buffer_for_peer(&buffer, SSH_PEER_ID, cx)
.detach_and_log_err(cx);
});
buffer_id
})?;
Ok(proto::OpenBufferResponse {
buffer_id: buffer_id.to_proto(),
})
}
pub async fn handle_find_search_candidates(
this: Model<Self>,
envelope: TypedEnvelope<proto::FindSearchCandidates>,

View file

@ -3,7 +3,7 @@ use client::{Client, UserStore};
use clock::FakeSystemClock;
use fs::{FakeFs, Fs};
use gpui::{Context, Model, TestAppContext};
use http_client::FakeHttpClient;
use http_client::{BlockedHttpClient, FakeHttpClient};
use language::{
language_settings::{all_language_settings, AllLanguageSettings},
Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName,
@ -17,7 +17,7 @@ use project::{
};
use remote::SshRemoteClient;
use serde_json::json;
use settings::{Settings, SettingsLocation, SettingsStore};
use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
use smol::stream::StreamExt;
use std::{
path::{Path, PathBuf},
@ -197,7 +197,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
cx.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store.set_user_settings(
r#"{"languages":{"Rust":{"language_servers":["custom-rust-analyzer"]}}}"#,
r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
cx,
)
})
@ -210,7 +210,27 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
AllLanguageSettings::get_global(cx)
.language(Some(&"Rust".into()))
.language_servers,
["custom-rust-analyzer".to_string()]
["from-local-settings".to_string()]
)
});
server_cx
.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store.set_server_settings(
r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
cx,
)
})
.unwrap();
cx.run_until_parked();
server_cx.read(|cx| {
assert_eq!(
AllLanguageSettings::get_global(cx)
.language(Some(&"Rust".into()))
.language_servers,
["from-server-settings".to_string()]
)
});
@ -606,6 +626,21 @@ async fn test_adding_then_removing_then_adding_worktrees(
})
}
#[gpui::test]
async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let (project, _headless, _fs) = init_test(cx, server_cx).await;
let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
cx.executor().run_until_parked();
let buffer = buffer.await.unwrap();
cx.update(|cx| {
assert_eq!(
buffer.read(cx).text(),
initial_server_settings_content().to_string()
)
})
}
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok();
@ -642,8 +677,23 @@ async fn init_test(
);
server_cx.update(HeadlessProject::init);
let headless =
server_cx.new_model(|cx| HeadlessProject::new(ssh_server_client, fs.clone(), cx));
let http_client = Arc::new(BlockedHttpClient);
let node_runtime = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(cx.executor()));
let headless = server_cx.new_model(|cx| {
client::init_settings(cx);
HeadlessProject::new(
crate::HeadlessAppState {
session: ssh_server_client,
fs: fs.clone(),
http_client,
node_runtime,
languages,
},
cx,
)
});
let project = build_project(ssh_remote_client, cx);
project

View file

@ -6,4 +6,4 @@ pub mod unix;
#[cfg(test)]
mod remote_editing_tests;
pub use headless_project::HeadlessProject;
pub use headless_project::{HeadlessAppState, HeadlessProject};

View file

@ -1,27 +1,37 @@
use crate::headless_project::HeadlessAppState;
use crate::HeadlessProject;
use anyhow::{anyhow, Context, Result};
use fs::RealFs;
use client::ProxySettings;
use fs::{Fs, RealFs};
use futures::channel::mpsc;
use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt};
use gpui::{AppContext, Context as _};
use gpui::{AppContext, Context as _, ModelContext, UpdateGlobal as _};
use http_client::{read_proxy_from_env, Uri};
use language::LanguageRegistry;
use node_runtime::{NodeBinaryOptions, NodeRuntime};
use paths::logs_dir;
use project::project_settings::ProjectSettings;
use remote::proxy::ProxyLaunchError;
use remote::ssh_session::ChannelClient;
use remote::{
json_log::LogRecord,
protocol::{read_message, write_message},
};
use rpc::proto::Envelope;
use reqwest_client::ReqwestClient;
use rpc::proto::{self, Envelope, SSH_PROJECT_ID};
use settings::{watch_config_file, Settings, SettingsStore};
use smol::channel::{Receiver, Sender};
use smol::io::AsyncReadExt;
use smol::Async;
use smol::{net::unix::UnixListener, stream::StreamExt as _};
use std::{
env,
io::Write,
mem,
path::{Path, PathBuf},
sync::Arc,
};
use util::ResultExt;
fn init_logging_proxy() {
env_logger::builder()
@ -266,6 +276,22 @@ fn start_server(
ChannelClient::new(incoming_rx, outgoing_tx, cx)
}
fn init_paths() -> anyhow::Result<()> {
for path in [
paths::config_dir(),
paths::extensions_dir(),
paths::languages_dir(),
paths::logs_dir(),
paths::temp_dir(),
]
.iter()
{
std::fs::create_dir_all(path)
.map_err(|e| anyhow!("Could not create directory {:?}: {}", path, e))?;
}
Ok(())
}
pub fn execute_run(
log_file: PathBuf,
pid_file: PathBuf,
@ -275,6 +301,7 @@ pub fn execute_run(
) -> Result<()> {
let log_rx = init_logging_server(log_file)?;
init_panic_hook();
init_paths()?;
log::info!(
"starting up. pid_file: {:?}, stdin_socket: {:?}, stdout_socket: {:?}, stderr_socket: {:?}",
@ -297,8 +324,43 @@ pub fn execute_run(
log::info!("gpui app started, initializing server");
let session = start_server(listeners, log_rx, cx);
client::init_settings(cx);
let project = cx.new_model(|cx| {
HeadlessProject::new(session, Arc::new(RealFs::new(Default::default(), None)), cx)
let fs = Arc::new(RealFs::new(Default::default(), None));
let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx);
let proxy_url = read_proxy_settings(cx);
let http_client = Arc::new(
ReqwestClient::proxy_and_user_agent(
proxy_url,
&format!(
"Zed-Server/{} ({}; {})",
env!("CARGO_PKG_VERSION"),
std::env::consts::OS,
std::env::consts::ARCH
),
)
.expect("Could not start HTTP client"),
);
let node_runtime = NodeRuntime::new(http_client.clone(), node_settings_rx);
let mut languages = LanguageRegistry::new(cx.background_executor().clone());
languages.set_language_server_download_dir(paths::languages_dir().clone());
let languages = Arc::new(languages);
HeadlessProject::new(
HeadlessAppState {
session,
fs,
http_client,
node_runtime,
languages,
},
cx,
)
});
mem::forget(project);
@ -318,13 +380,15 @@ struct ServerPaths {
impl ServerPaths {
fn new(identifier: &str) -> Result<Self> {
let project_dir = create_state_directory(identifier)?;
let server_dir = paths::remote_server_state_dir().join(identifier);
std::fs::create_dir_all(&server_dir)?;
std::fs::create_dir_all(&logs_dir())?;
let pid_file = project_dir.join("server.pid");
let stdin_socket = project_dir.join("stdin.sock");
let stdout_socket = project_dir.join("stdout.sock");
let stderr_socket = project_dir.join("stderr.sock");
let log_file = project_dir.join("server.log");
let pid_file = server_dir.join("server.pid");
let stdin_socket = server_dir.join("stdin.sock");
let stdout_socket = server_dir.join("stdout.sock");
let stderr_socket = server_dir.join("stderr.sock");
let log_file = logs_dir().join(format!("server-{}.log", identifier));
Ok(Self {
pid_file,
@ -358,7 +422,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
}
spawn_server(&server_paths)?;
}
};
let stdin_task = smol::spawn(async move {
let stdin = Async::new(std::io::stdin())?;
@ -409,19 +473,6 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
Ok(())
}
fn create_state_directory(identifier: &str) -> Result<PathBuf> {
let home_dir = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let server_dir = PathBuf::from(home_dir)
.join(".local")
.join("state")
.join("zed-remote-server")
.join(identifier);
std::fs::create_dir_all(&server_dir)?;
Ok(server_dir)
}
fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> {
log::info!("killing existing server with PID {}", pid);
std::process::Command::new("kill")
@ -453,7 +504,7 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
}
let binary_name = std::env::current_exe()?;
let server_process = std::process::Command::new(binary_name)
let server_process = smol::process::Command::new(binary_name)
.arg("run")
.arg("--log-file")
.arg(&paths.log_file)
@ -484,6 +535,7 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
"server ready to accept connections. total time waited: {:?}",
total_time_waited
);
Ok(())
}
@ -556,3 +608,118 @@ async fn write_size_prefixed_buffer<S: AsyncWrite + Unpin>(
stream.write_all(buffer).await?;
Ok(())
}
fn initialize_settings(
session: Arc<ChannelClient>,
fs: Arc<dyn Fs>,
cx: &mut AppContext,
) -> async_watch::Receiver<Option<NodeBinaryOptions>> {
let user_settings_file_rx = watch_config_file(
&cx.background_executor(),
fs,
paths::settings_file().clone(),
);
handle_settings_file_changes(user_settings_file_rx, cx, {
let session = session.clone();
move |err, _cx| {
if let Some(e) = err {
log::info!("Server settings failed to change: {}", e);
session
.send(proto::Toast {
project_id: SSH_PROJECT_ID,
notification_id: "server-settings-failed".to_string(),
message: format!(
"Error in settings on remote host {:?}: {}",
paths::settings_file(),
e
),
})
.log_err();
} else {
session
.send(proto::HideToast {
project_id: SSH_PROJECT_ID,
notification_id: "server-settings-failed".to_string(),
})
.log_err();
}
}
});
let (tx, rx) = async_watch::channel(None);
cx.observe_global::<SettingsStore>(move |cx| {
let settings = &ProjectSettings::get_global(cx).node;
log::info!("Got new node settings: {:?}", settings);
let options = NodeBinaryOptions {
allow_path_lookup: !settings.ignore_system_version.unwrap_or_default(),
// TODO: Implement this setting
allow_binary_download: true,
use_paths: settings.path.as_ref().map(|node_path| {
let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref());
let npm_path = settings
.npm_path
.as_ref()
.map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref()));
(
node_path.clone(),
npm_path.unwrap_or_else(|| {
let base_path = PathBuf::new();
node_path.parent().unwrap_or(&base_path).join("npm")
}),
)
}),
};
tx.send(Some(options)).log_err();
})
.detach();
rx
}
pub fn handle_settings_file_changes(
mut server_settings_file: mpsc::UnboundedReceiver<String>,
cx: &mut AppContext,
settings_changed: impl Fn(Option<anyhow::Error>, &mut AppContext) + 'static,
) {
let server_settings_content = cx
.background_executor()
.block(server_settings_file.next())
.unwrap();
SettingsStore::update_global(cx, |store, cx| {
store
.set_server_settings(&server_settings_content, cx)
.log_err();
});
cx.spawn(move |cx| async move {
while let Some(server_settings_content) = server_settings_file.next().await {
let result = cx.update_global(|store: &mut SettingsStore, cx| {
let result = store.set_server_settings(&server_settings_content, cx);
if let Err(err) = &result {
log::error!("Failed to load server settings: {err}");
}
settings_changed(result.err(), cx);
cx.refresh();
});
if result.is_err() {
break; // App dropped
}
}
})
.detach();
}
fn read_proxy_settings(cx: &mut ModelContext<'_, HeadlessProject>) -> Option<Uri> {
let proxy_str = ProxySettings::get_global(cx).proxy.to_owned();
let proxy_url = proxy_str
.as_ref()
.and_then(|input: &String| {
input
.parse::<Uri>()
.inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
.ok()
})
.or_else(read_proxy_from_env);
proxy_url
}