Merge pull request #2407 from zed-industries/lsp-log-view

Add a simple language server log view
This commit is contained in:
Max Brunsfeld 2023-04-24 10:15:29 -07:00 committed by GitHub
commit d2ba1ec275
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 780 additions and 92 deletions

23
Cargo.lock generated
View file

@ -3612,6 +3612,26 @@ dependencies = [
"url",
]
[[package]]
name = "lsp_log"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"editor",
"futures 0.3.25",
"gpui",
"language",
"lsp",
"project",
"serde",
"settings",
"theme",
"unindent",
"util",
"workspace",
]
[[package]]
name = "mach"
version = "0.3.2"
@ -7239,7 +7259,7 @@ dependencies = [
[[package]]
name = "tree-sitter-json"
version = "0.20.0"
source = "git+https://github.com/tree-sitter/tree-sitter-json?rev=137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8#137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8"
source = "git+https://github.com/tree-sitter/tree-sitter-json?rev=40a81c01a40ac48744e0c8ccabbaba1920441199#40a81c01a40ac48744e0c8ccabbaba1920441199"
dependencies = [
"cc",
"tree-sitter",
@ -8571,6 +8591,7 @@ dependencies = [
"libc",
"log",
"lsp",
"lsp_log",
"node_runtime",
"num_cpus",
"outline",

View file

@ -35,6 +35,7 @@ members = [
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/lsp_log",
"crates/media",
"crates/menu",
"crates/node_runtime",
@ -77,6 +78,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
rand = { version = "0.8" }
postage = { version = "0.5", features = ["futures-traits"] }
smallvec = { version = "1.6", features = ["union"] }
futures = { version = "0.3" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }

View file

@ -17,5 +17,5 @@ project = { path = "../project" }
settings = { path = "../settings" }
util = { path = "../util" }
workspace = { path = "../workspace" }
futures = "0.3"
futures = { workspace = true }
smallvec = { workspace = true }

View file

@ -33,7 +33,7 @@ util = { path = "../util" }
anyhow = "1.0.38"
async-broadcast = "0.4"
futures = "0.3"
futures = { workspace = true }
postage = { workspace = true }
[dev-dependencies]

View file

@ -22,7 +22,7 @@ sum_tree = { path = "../sum_tree" }
anyhow = "1.0.38"
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-tls"] }
futures = "0.3"
futures = { workspace = true }
image = "0.23"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }

View file

@ -27,7 +27,7 @@ base64 = "0.13"
clap = { version = "3.1", features = ["derive"], optional = true }
dashmap = "5.4"
envy = "0.4.2"
futures = "0.3"
futures = { workspace = true }
hyper = "0.14"
lazy_static = "1.4"
lipsum = { version = "0.8", optional = true }

View file

@ -40,7 +40,7 @@ theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
futures = { workspace = true }
log = "0.4"
postage = { workspace = true }
serde = { workspace = true }

View file

@ -35,7 +35,7 @@ log = "0.4"
serde = { workspace = true }
serde_derive = { workspace = true }
smol = "1.2.5"
futures = "0.3"
futures = { workspace = true }
[dev-dependencies]
clock = { path = "../clock" }

View file

@ -19,4 +19,4 @@ util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
smol = "1.2.5"
futures = "0.3"
futures = { workspace = true }

View file

@ -47,7 +47,7 @@ workspace = { path = "../workspace" }
aho-corasick = "0.7"
anyhow = "1.0"
futures = "0.3"
futures = { workspace = true }
indoc = "1.0.4"
itertools = "0.10"
lazy_static = "1.4"

View file

@ -511,6 +511,7 @@ pub struct Editor {
workspace_id: Option<WorkspaceId>,
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
input_enabled: bool,
read_only: bool,
leader_replica_id: Option<u16>,
remote_id: Option<ViewId>,
hover_state: HoverState,
@ -1283,6 +1284,7 @@ impl Editor {
workspace_id: None,
keymap_context_layers: Default::default(),
input_enabled: true,
read_only: false,
leader_replica_id: None,
remote_id: None,
hover_state: Default::default(),
@ -1425,6 +1427,10 @@ impl Editor {
self.input_enabled = input_enabled;
}
pub fn set_read_only(&mut self, read_only: bool) {
self.read_only = read_only;
}
fn selections_did_change(
&mut self,
local: bool,
@ -1533,6 +1539,10 @@ impl Editor {
S: ToOffset,
T: Into<Arc<str>>,
{
if self.read_only {
return;
}
self.buffer
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
}
@ -1543,6 +1553,10 @@ impl Editor {
S: ToOffset,
T: Into<Arc<str>>,
{
if self.read_only {
return;
}
self.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, Some(AutoindentMode::EachLine), cx)
});
@ -1897,6 +1911,9 @@ impl Editor {
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
let text: Arc<str> = text.into();
if self.read_only {
return;
}
if !self.input_enabled {
cx.emit(Event::InputIgnored { text });
return;
@ -2282,6 +2299,10 @@ impl Editor {
autoindent_mode: Option<AutoindentMode>,
cx: &mut ViewContext<Self>,
) {
if self.read_only {
return;
}
let text: Arc<str> = text.into();
self.transact(cx, |this, cx| {
let old_selections = this.selections.all_adjusted(cx);

View file

@ -16,7 +16,7 @@ client = { path = "../client" }
editor = { path = "../editor" }
language = { path = "../language" }
log = "0.4"
futures = "0.3"
futures = { workspace = true }
gpui = { path = "../gpui" }
human_bytes = "0.4.1"
isahc = "1.7"

View file

@ -15,7 +15,7 @@ rope = { path = "../rope" }
util = { path = "../util" }
anyhow = "1.0.57"
async-trait = "0.1"
futures = "0.3"
futures = { workspace = true }
tempfile = "3"
fsevent = { path = "../fsevent" }
lazy_static = "1.4.0"

View file

@ -19,7 +19,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
smol = "1.2"
parking_lot = "0.11.1"
async-trait = "0.1"
futures = "0.3"
futures = { workspace = true }
git2 = { version = "0.15", default-features = false }
[dev-dependencies]

View file

@ -25,7 +25,7 @@ ctor = "0.1"
dhat = { version = "0.3", optional = true }
env_logger = { version = "0.9", optional = true }
etagere = "0.2"
futures = "0.3"
futures = { workspace = true }
image = "0.23"
itertools = "0.10"
lazy_static = "1.4.0"

View file

@ -39,7 +39,7 @@ util = { path = "../util" }
anyhow = "1.0.38"
async-broadcast = "0.4"
async-trait = "0.1"
futures = "0.3"
futures = { workspace = true }
lazy_static = "1.4"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"

View file

@ -32,7 +32,7 @@ anyhow = "1.0.38"
async-broadcast = "0.4"
core-foundation = "0.9.3"
core-graphics = "0.22.3"
futures = "0.3"
futures = { workspace = true }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
postage = { workspace = true }
@ -56,7 +56,7 @@ cocoa = "0.24"
core-foundation = "0.9.3"
core-graphics = "0.22.3"
foreign-types = "0.3"
futures = "0.3"
futures = { workspace = true }
hmac = "0.12"
jwt = "0.16"
lazy_static = "1.4"

View file

@ -12,7 +12,7 @@ doctest = false
[dependencies]
anyhow = "1.0.38"
async-trait = "0.1"
futures = "0.3"
futures = { workspace = true }
hmac = "0.12"
log = "0.4"
jwt = "0.16"

View file

@ -17,7 +17,7 @@ gpui = { path = "../gpui" }
util = { path = "../util" }
anyhow = "1.0"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
futures = "0.3"
futures = { workspace = true }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
lsp-types = "0.91"
parking_lot = "0.11"

View file

@ -20,10 +20,10 @@ use std::{
future::Future,
io::Write,
path::PathBuf,
str::FromStr,
str::{self, FromStr as _},
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
Arc, Weak,
},
};
use std::{path::Path, process::Stdio};
@ -34,16 +34,18 @@ const CONTENT_LEN_HEADER: &str = "Content-Length: ";
type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
pub struct LanguageServer {
server_id: LanguageServerId,
next_id: AtomicUsize,
outbound_tx: channel::Sender<Vec<u8>>,
outbound_tx: channel::Sender<String>,
name: String,
capabilities: ServerCapabilities,
code_action_kinds: Option<Vec<CodeActionKind>>,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
executor: Arc<executor::Background>,
#[allow(clippy::type_complexity)]
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
@ -56,9 +58,15 @@ pub struct LanguageServer {
#[repr(transparent)]
pub struct LanguageServerId(pub usize);
pub struct Subscription {
method: &'static str,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
pub enum Subscription {
Notification {
method: &'static str,
notification_handlers: Option<Arc<Mutex<HashMap<&'static str, NotificationHandler>>>>,
},
Io {
id: usize,
io_handlers: Option<Weak<Mutex<HashMap<usize, IoHandler>>>>,
},
}
#[derive(Serialize, Deserialize)]
@ -177,33 +185,40 @@ impl LanguageServer {
Stdout: AsyncRead + Unpin + Send + 'static,
F: FnMut(AnyNotification) + 'static + Send,
{
let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
let (output_done_tx, output_done_rx) = barrier::channel();
let notification_handlers =
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
let response_handlers =
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
let io_handlers = Arc::new(Mutex::new(HashMap::default()));
let input_task = cx.spawn(|cx| {
let notification_handlers = notification_handlers.clone();
let response_handlers = response_handlers.clone();
Self::handle_input(
stdout,
on_unhandled_notification,
notification_handlers,
response_handlers,
notification_handlers.clone(),
response_handlers.clone(),
io_handlers.clone(),
cx,
)
.log_err()
});
let (output_done_tx, output_done_rx) = barrier::channel();
let output_task = cx.background().spawn({
let response_handlers = response_handlers.clone();
Self::handle_output(stdin, outbound_rx, output_done_tx, response_handlers).log_err()
Self::handle_output(
stdin,
outbound_rx,
output_done_tx,
response_handlers.clone(),
io_handlers.clone(),
)
.log_err()
});
Self {
server_id,
notification_handlers,
response_handlers,
io_handlers,
name: Default::default(),
capabilities: Default::default(),
code_action_kinds,
@ -226,6 +241,7 @@ impl LanguageServer {
mut on_unhandled_notification: F,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
cx: AsyncAppContext,
) -> anyhow::Result<()>
where
@ -252,7 +268,13 @@ impl LanguageServer {
buffer.resize(message_len, 0);
stdout.read_exact(&mut buffer).await?;
log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer));
if let Ok(message) = str::from_utf8(&buffer) {
log::trace!("incoming message:{}", message);
for handler in io_handlers.lock().values_mut() {
handler(true, message);
}
}
if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
@ -291,9 +313,10 @@ impl LanguageServer {
async fn handle_output<Stdin>(
stdin: Stdin,
outbound_rx: channel::Receiver<Vec<u8>>,
outbound_rx: channel::Receiver<String>,
output_done_tx: barrier::Sender,
response_handlers: Arc<Mutex<Option<HashMap<usize, ResponseHandler>>>>,
io_handlers: Arc<Mutex<HashMap<usize, IoHandler>>>,
) -> anyhow::Result<()>
where
Stdin: AsyncWrite + Unpin + Send + 'static,
@ -307,13 +330,17 @@ impl LanguageServer {
});
let mut content_len_buffer = Vec::new();
while let Ok(message) = outbound_rx.recv().await {
log::trace!("outgoing message:{}", String::from_utf8_lossy(&message));
log::trace!("outgoing message:{}", message);
for handler in io_handlers.lock().values_mut() {
handler(false, &message);
}
content_len_buffer.clear();
write!(content_len_buffer, "{}", message.len()).unwrap();
stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
stdin.write_all(&content_len_buffer).await?;
stdin.write_all("\r\n\r\n".as_bytes()).await?;
stdin.write_all(&message).await?;
stdin.write_all(message.as_bytes()).await?;
stdin.flush().await?;
}
drop(output_done_tx);
@ -464,6 +491,19 @@ impl LanguageServer {
self.on_custom_request(T::METHOD, f)
}
#[must_use]
pub fn on_io<F>(&self, f: F) -> Subscription
where
F: 'static + Send + FnMut(bool, &str),
{
let id = self.next_id.fetch_add(1, SeqCst);
self.io_handlers.lock().insert(id, Box::new(f));
Subscription::Io {
id,
io_handlers: Some(Arc::downgrade(&self.io_handlers)),
}
}
pub fn remove_request_handler<T: request::Request>(&self) {
self.notification_handlers.lock().remove(T::METHOD);
}
@ -490,9 +530,9 @@ impl LanguageServer {
prev_handler.is_none(),
"registered multiple handlers for the same LSP method"
);
Subscription {
Subscription::Notification {
method,
notification_handlers: self.notification_handlers.clone(),
notification_handlers: Some(self.notification_handlers.clone()),
}
}
@ -537,7 +577,7 @@ impl LanguageServer {
},
};
if let Some(response) =
serde_json::to_vec(&response).log_err()
serde_json::to_string(&response).log_err()
{
outbound_tx.try_send(response).ok();
}
@ -560,7 +600,7 @@ impl LanguageServer {
message: error.to_string(),
}),
};
if let Some(response) = serde_json::to_vec(&response).log_err() {
if let Some(response) = serde_json::to_string(&response).log_err() {
outbound_tx.try_send(response).ok();
}
}
@ -572,9 +612,9 @@ impl LanguageServer {
prev_handler.is_none(),
"registered multiple handlers for the same LSP method"
);
Subscription {
Subscription::Notification {
method,
notification_handlers: self.notification_handlers.clone(),
notification_handlers: Some(self.notification_handlers.clone()),
}
}
@ -612,14 +652,14 @@ impl LanguageServer {
fn request_internal<T: request::Request>(
next_id: &AtomicUsize,
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
outbound_tx: &channel::Sender<Vec<u8>>,
outbound_tx: &channel::Sender<String>,
params: T::Params,
) -> impl 'static + Future<Output = Result<T::Result>>
where
T::Result: 'static + Send,
{
let id = next_id.fetch_add(1, SeqCst);
let message = serde_json::to_vec(&Request {
let message = serde_json::to_string(&Request {
jsonrpc: JSON_RPC_VERSION,
id,
method: T::METHOD,
@ -662,10 +702,10 @@ impl LanguageServer {
}
fn notify_internal<T: notification::Notification>(
outbound_tx: &channel::Sender<Vec<u8>>,
outbound_tx: &channel::Sender<String>,
params: T::Params,
) -> Result<()> {
let message = serde_json::to_vec(&Notification {
let message = serde_json::to_string(&Notification {
jsonrpc: JSON_RPC_VERSION,
method: T::METHOD,
params,
@ -685,8 +725,14 @@ impl Drop for LanguageServer {
}
impl Subscription {
pub fn detach(mut self) {
self.method = "";
pub fn detach(&mut self) {
match self {
Subscription::Notification {
notification_handlers,
..
} => *notification_handlers = None,
Subscription::Io { io_handlers, .. } => *io_handlers = None,
}
}
}
@ -698,7 +744,21 @@ impl fmt::Display for LanguageServerId {
impl Drop for Subscription {
fn drop(&mut self) {
self.notification_handlers.lock().remove(self.method);
match self {
Subscription::Notification {
method,
notification_handlers,
} => {
if let Some(handlers) = notification_handlers {
handlers.lock().remove(method);
}
}
Subscription::Io { id, io_handlers } => {
if let Some(io_handlers) = io_handlers.as_ref().and_then(|h| h.upgrade()) {
io_handlers.lock().remove(id);
}
}
}
}
}

29
crates/lsp_log/Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
[package]
name = "lsp_log"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/lsp_log.rs"
doctest = false
[dependencies]
collections = { path = "../collections" }
editor = { path = "../editor" }
settings = { path = "../settings" }
theme = { path = "../theme" }
language = { path = "../language" }
project = { path = "../project" }
workspace = { path = "../workspace" }
gpui = { path = "../gpui" }
util = { path = "../util" }
lsp = { path = "../lsp" }
futures = { workspace = true }
serde = { workspace = true }
anyhow = "1.0"
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
unindent = "0.1.7"

View file

@ -0,0 +1,523 @@
use collections::{hash_map, HashMap};
use editor::Editor;
use futures::{channel::mpsc, StreamExt};
use gpui::{
actions,
elements::{
AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
ParentElement, Stack,
},
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
ViewHandle, WeakModelHandle,
};
use language::{Buffer, LanguageServerId, LanguageServerName};
use project::{Project, WorktreeId};
use settings::Settings;
use std::{borrow::Cow, sync::Arc};
use theme::{ui, Theme};
use workspace::{
item::{Item, ItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace,
};
const SEND_LINE: &str = "// Send:\n";
const RECEIVE_LINE: &str = "// Receive:\n";
struct LogStore {
projects: HashMap<WeakModelHandle<Project>, LogStoreProject>,
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
}
struct LogStoreProject {
servers: HashMap<LanguageServerId, LogStoreLanguageServer>,
_subscription: gpui::Subscription,
}
struct LogStoreLanguageServer {
buffer: ModelHandle<Buffer>,
last_message_kind: Option<MessageKind>,
_subscription: lsp::Subscription,
}
pub struct LspLogView {
log_store: ModelHandle<LogStore>,
current_server_id: Option<LanguageServerId>,
editor: Option<ViewHandle<Editor>>,
project: ModelHandle<Project>,
}
pub struct LspLogToolbarItemView {
log_view: Option<ViewHandle<LspLogView>>,
menu_open: bool,
project: ModelHandle<Project>,
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum MessageKind {
Send,
Receive,
}
actions!(log, [OpenLanguageServerLogs]);
pub fn init(cx: &mut AppContext) {
let log_set = cx.add_model(|cx| LogStore::new(cx));
cx.add_action(
move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
let project = workspace.project().read(cx);
if project.is_local() {
workspace.add_item(
Box::new(cx.add_view(|cx| {
LspLogView::new(workspace.project().clone(), log_set.clone(), cx)
})),
cx,
);
}
},
);
}
impl LogStore {
fn new(cx: &mut ModelContext<Self>) -> Self {
let (io_tx, mut io_rx) = mpsc::unbounded();
let this = Self {
projects: HashMap::default(),
io_tx,
};
cx.spawn_weak(|this, mut cx| async move {
while let Some((project, server_id, is_output, mut message)) = io_rx.next().await {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
message.push('\n');
this.on_io(project, server_id, is_output, &message, cx);
});
}
}
anyhow::Ok(())
})
.detach();
this
}
pub fn has_enabled_logs_for_language_server(
&self,
project: &ModelHandle<Project>,
server_id: LanguageServerId,
) -> bool {
self.projects
.get(&project.downgrade())
.map_or(false, |store| store.servers.contains_key(&server_id))
}
pub fn enable_logs_for_language_server(
&mut self,
project: &ModelHandle<Project>,
server_id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<ModelHandle<Buffer>> {
let server = project.read(cx).language_server_for_id(server_id)?;
let weak_project = project.downgrade();
let project_logs = match self.projects.entry(weak_project) {
hash_map::Entry::Occupied(entry) => entry.into_mut(),
hash_map::Entry::Vacant(entry) => entry.insert(LogStoreProject {
servers: HashMap::default(),
_subscription: cx.observe_release(&project, move |this, _, _| {
this.projects.remove(&weak_project);
}),
}),
};
let server_log_state = project_logs.servers.entry(server_id).or_insert_with(|| {
let io_tx = self.io_tx.clone();
let language = project.read(cx).languages().language_for_name("JSON");
let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
cx.spawn_weak({
let buffer = buffer.clone();
|_, mut cx| async move {
let language = language.await.ok();
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(language, cx);
});
}
})
.detach();
let project = project.downgrade();
LogStoreLanguageServer {
buffer,
last_message_kind: None,
_subscription: server.on_io(move |is_received, json| {
io_tx
.unbounded_send((project, server_id, is_received, json.to_string()))
.ok();
}),
}
});
Some(server_log_state.buffer.clone())
}
pub fn disable_logs_for_language_server(
&mut self,
project: &ModelHandle<Project>,
server_id: LanguageServerId,
_: &mut ModelContext<Self>,
) {
let project = project.downgrade();
if let Some(store) = self.projects.get_mut(&project) {
store.servers.remove(&server_id);
if store.servers.is_empty() {
self.projects.remove(&project);
}
}
}
fn on_io(
&mut self,
project: WeakModelHandle<Project>,
language_server_id: LanguageServerId,
is_received: bool,
message: &str,
cx: &mut AppContext,
) -> Option<()> {
let state = self
.projects
.get_mut(&project)?
.servers
.get_mut(&language_server_id)?;
state.buffer.update(cx, |buffer, cx| {
let kind = if is_received {
MessageKind::Receive
} else {
MessageKind::Send
};
if state.last_message_kind != Some(kind) {
let len = buffer.len();
let line = match kind {
MessageKind::Send => SEND_LINE,
MessageKind::Receive => RECEIVE_LINE,
};
buffer.edit([(len..len, line)], None, cx);
state.last_message_kind = Some(kind);
}
let len = buffer.len();
buffer.edit([(len..len, message)], None, cx);
});
Some(())
}
}
impl LspLogView {
fn new(
project: ModelHandle<Project>,
log_set: ModelHandle<LogStore>,
_: &mut ViewContext<Self>,
) -> Self {
Self {
project,
log_store: log_set,
editor: None,
current_server_id: None,
}
}
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
let buffer = self.log_store.update(cx, |log_set, cx| {
log_set.enable_logs_for_language_server(&self.project, server_id, cx)
});
if let Some(buffer) = buffer {
self.current_server_id = Some(server_id);
self.editor = Some(cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx);
editor.set_read_only(true);
editor.move_to_end(&Default::default(), cx);
editor
}));
cx.notify();
}
}
fn toggle_logging_for_server(
&mut self,
server_id: LanguageServerId,
enabled: bool,
cx: &mut ViewContext<Self>,
) {
self.log_store.update(cx, |log_store, cx| {
if enabled {
log_store.enable_logs_for_language_server(&self.project, server_id, cx);
} else {
log_store.disable_logs_for_language_server(&self.project, server_id, cx);
}
});
}
}
impl View for LspLogView {
fn ui_name() -> &'static str {
"LspLogView"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(editor) = &self.editor {
ChildView::new(&editor, cx).into_any()
} else {
Empty::new().into_any()
}
}
}
impl Item for LspLogView {
fn tab_content<V: View>(
&self,
_: Option<usize>,
style: &theme::Tab,
_: &AppContext,
) -> AnyElement<V> {
Label::new("LSP Logs", style.label.clone()).into_any()
}
}
impl ToolbarItemView for LspLogToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
self.menu_open = false;
if let Some(item) = active_pane_item {
if let Some(log_view) = item.downcast::<LspLogView>() {
self.log_view = Some(log_view.clone());
return ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)),
};
}
}
self.log_view = None;
ToolbarItemLocation::Hidden
}
}
impl View for LspLogToolbarItemView {
fn ui_name() -> &'static str {
"LspLogView"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = cx.global::<Settings>().theme.clone();
let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
let project = self.project.read(cx);
let log_view = log_view.read(cx);
let log_store = log_view.log_store.read(cx);
let mut language_servers = project
.language_servers()
.map(|(id, name, worktree)| {
(
id,
name,
worktree,
log_store.has_enabled_logs_for_language_server(&self.project, id),
)
})
.collect::<Vec<_>>();
language_servers.sort_by_key(|a| (a.0, a.2));
language_servers.dedup_by_key(|a| a.0);
let current_server_id = log_view.current_server_id;
let current_server = current_server_id.and_then(|current_server_id| {
if let Ok(ix) = language_servers.binary_search_by_key(&current_server_id, |e| e.0) {
Some(language_servers[ix].clone())
} else {
None
}
});
enum Menu {}
Stack::new()
.with_child(Self::render_language_server_menu_header(
current_server,
&self.project,
&theme,
cx,
))
.with_children(if self.menu_open {
Some(
Overlay::new(
MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
Flex::column()
.with_children(language_servers.into_iter().filter_map(
|(id, name, worktree_id, logging_enabled)| {
Self::render_language_server_menu_item(
id,
name,
worktree_id,
logging_enabled,
Some(id) == current_server_id,
&self.project,
&theme,
cx,
)
},
))
.contained()
.with_style(theme.context_menu.container)
.constrained()
.with_width(400.)
.with_height(400.)
})
.on_down_out(MouseButton::Left, |_, this, cx| {
this.menu_open = false;
cx.notify()
}),
)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
.aligned()
.bottom()
.left(),
)
} else {
None
})
.aligned()
.left()
.clipped()
.into_any()
}
}
impl LspLogToolbarItemView {
pub fn new(project: ModelHandle<Project>) -> Self {
Self {
menu_open: false,
log_view: None,
project,
}
}
fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
self.menu_open = !self.menu_open;
cx.notify();
}
fn toggle_logging_for_server(
&mut self,
id: LanguageServerId,
enabled: bool,
cx: &mut ViewContext<Self>,
) {
if let Some(log_view) = &self.log_view {
log_view.update(cx, |log_view, cx| {
log_view.toggle_logging_for_server(id, enabled, cx);
if !enabled && Some(id) == log_view.current_server_id {
log_view.current_server_id = None;
log_view.editor = None;
cx.notify();
}
});
}
cx.notify();
}
fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
if let Some(log_view) = &self.log_view {
log_view.update(cx, |log_view, cx| {
log_view.show_logs_for_server(id, cx);
});
self.menu_open = false;
}
cx.notify();
}
fn render_language_server_menu_header(
current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId, bool)>,
project: &ModelHandle<Project>,
theme: &Arc<Theme>,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
enum ToggleMenu {}
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
let project = project.read(cx);
let label: Cow<str> = current_server
.and_then(|(_, server_name, worktree_id, _)| {
let worktree = project.worktree_for_id(worktree_id, cx)?;
let worktree = &worktree.read(cx);
Some(format!("{} - ({})", server_name.0, worktree.root_name()).into())
})
.unwrap_or_else(|| "No server selected".into());
Label::new(
label,
theme
.context_menu
.item
.style_for(state, false)
.label
.clone(),
)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.toggle_menu(cx);
})
}
fn render_language_server_menu_item(
id: LanguageServerId,
name: LanguageServerName,
worktree_id: WorktreeId,
logging_enabled: bool,
is_selected: bool,
project: &ModelHandle<Project>,
theme: &Arc<Theme>,
cx: &mut ViewContext<Self>,
) -> Option<impl Element<Self>> {
enum ActivateLog {}
let project = project.read(cx);
let worktree = project.worktree_for_id(worktree_id, cx)?;
let worktree = &worktree.read(cx);
if !worktree.is_visible() {
return None;
}
let label = format!("{} - ({})", name.0, worktree.root_name());
Some(
MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, cx| {
let item_style = theme.context_menu.item.style_for(state, is_selected);
Flex::row()
.with_child(ui::checkbox_with_label::<Self, _, Self, _>(
Empty::new(),
&theme.welcome.checkbox,
logging_enabled,
id.0,
cx,
move |this, enabled, cx| {
this.toggle_logging_for_server(id, enabled, cx);
},
))
.with_child(Label::new(label, item_style.label.clone()).aligned().left())
.align_children_center()
.contained()
.with_style(item_style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.show_logs_for_server(id, cx);
}),
)
}
}
impl Entity for LogStore {
type Event = ();
}
impl Entity for LspLogView {
type Event = ();
}
impl Entity for LspLogToolbarItemView {
type Event = ();
}

View file

@ -13,7 +13,7 @@ gpui = { path = "../gpui" }
util = { path = "../util" }
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
async-tar = "0.4.2"
futures = "0.3"
futures = { workspace = true }
anyhow = "1.0.38"
parking_lot = "0.11.1"
serde = { workspace = true }

View file

@ -41,7 +41,7 @@ aho-corasick = "0.7"
anyhow = "1.0.57"
async-trait = "0.1"
backtrace = "0.3"
futures = "0.3"
futures = { workspace = true }
ignore = "0.4"
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }

View file

@ -185,6 +185,8 @@ pub struct Collaborator {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
LanguageServerAdded(LanguageServerId),
LanguageServerRemoved(LanguageServerId),
ActiveEntryChanged(Option<ProjectEntryId>),
WorktreeAdded,
WorktreeRemoved(WorktreeId),
@ -1869,7 +1871,7 @@ impl Project {
let next_snapshot = buffer.text_snapshot();
let language_servers: Vec<_> = self
.language_servers_iter_for_buffer(buffer, cx)
.language_servers_for_buffer(buffer, cx)
.map(|i| i.1.clone())
.collect();
@ -6279,7 +6281,25 @@ impl Project {
}
}
pub fn language_servers_iter_for_buffer(
pub fn language_servers(
&self,
) -> impl '_ + Iterator<Item = (LanguageServerId, LanguageServerName, WorktreeId)> {
self.language_server_ids
.iter()
.map(|((worktree_id, server_name), server_id)| {
(*server_id, server_name.clone(), *worktree_id)
})
}
pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
if let LanguageServerState::Running { server, .. } = self.language_servers.get(&id)? {
Some(server.clone())
} else {
None
}
}
pub fn language_servers_for_buffer(
&self,
buffer: &Buffer,
cx: &AppContext,
@ -6299,20 +6319,12 @@ impl Project {
})
}
fn language_servers_for_buffer(
&self,
buffer: &Buffer,
cx: &AppContext,
) -> Vec<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
self.language_servers_iter_for_buffer(buffer, cx).collect()
}
fn primary_language_servers_for_buffer(
&self,
buffer: &Buffer,
cx: &AppContext,
) -> Option<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
self.language_servers_iter_for_buffer(buffer, cx).next()
self.language_servers_for_buffer(buffer, cx).next()
}
fn language_server_for_buffer(
@ -6321,7 +6333,7 @@ impl Project {
server_id: LanguageServerId,
cx: &AppContext,
) -> Option<(&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
self.language_servers_iter_for_buffer(buffer, cx)
self.language_servers_for_buffer(buffer, cx)
.find(|(_, s)| s.server_id() == server_id)
}

View file

@ -20,7 +20,7 @@ theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
postage = { workspace = true }
futures = "0.3"
futures = { workspace = true }
unicase = "2.6"
[dev-dependencies]

View file

@ -24,7 +24,7 @@ postage = { workspace = true }
smol = "1.2"
[dev-dependencies]
futures = "0.3"
futures = { workspace = true }
settings = { path = "../settings", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }

View file

@ -21,7 +21,7 @@ anyhow = "1.0"
async-lock = "2.4"
async-tungstenite = "0.16"
base64 = "0.13"
futures = "0.3"
futures = { workspace = true }
parking_lot = "0.11.1"
prost = "0.8"
rand = "0.8"

View file

@ -20,7 +20,7 @@ theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow = "1.0"
futures = "0.3"
futures = { workspace = true }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
postage = { workspace = true }
serde = { workspace = true }

View file

@ -18,7 +18,7 @@ gpui = { path = "../gpui" }
sqlez = { path = "../sqlez" }
fs = { path = "../fs" }
anyhow = "1.0.38"
futures = "0.3"
futures = { workspace = true }
theme = { path = "../theme" }
staff_mode = { path = "../staff_mode" }
util = { path = "../util" }

View file

@ -14,5 +14,5 @@ smol = "1.2"
thread_local = "1.1.4"
lazy_static = "1.4"
parking_lot = "0.11.1"
futures = "0.3"
uuid = { version = "1.1.2", features = ["v4"] }
futures = { workspace = true }
uuid = { version = "1.1.2", features = ["v4"] }

View file

@ -20,7 +20,7 @@ procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f
smallvec = { workspace = true }
smol = "1.2.5"
mio-extras = "2.0.6"
futures = "0.3"
futures = { workspace = true }
ordered-float = "2.1.1"
itertools = "0.10"
dirs = "4.0.0"

View file

@ -24,7 +24,7 @@ terminal = { path = "../terminal" }
smallvec = { workspace = true }
smol = "1.2.5"
mio-extras = "2.0.6"
futures = "0.3"
futures = { workspace = true }
ordered-float = "2.1.1"
itertools = "0.10"
dirs = "4.0.0"

View file

@ -27,28 +27,40 @@ pub struct CheckboxStyle {
pub hovered_and_checked: ContainerStyle,
}
pub fn checkbox<Tag: 'static, V: View>(
pub fn checkbox<Tag, V, F>(
label: &'static str,
style: &CheckboxStyle,
checked: bool,
id: usize,
cx: &mut ViewContext<V>,
change: fn(checked: bool, cx: &mut EventContext<V>) -> (),
) -> MouseEventHandler<Tag, V> {
change: F,
) -> MouseEventHandler<Tag, V>
where
Tag: 'static,
V: View,
F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
{
let label = Label::new(label, style.label.text.clone())
.contained()
.with_style(style.label.container);
checkbox_with_label(label, style, checked, cx, change)
checkbox_with_label(label, style, checked, id, cx, change)
}
pub fn checkbox_with_label<Tag: 'static, D: Element<V>, V: View>(
pub fn checkbox_with_label<Tag, D, V, F>(
label: D,
style: &CheckboxStyle,
checked: bool,
id: usize,
cx: &mut ViewContext<V>,
change: fn(checked: bool, cx: &mut EventContext<V>) -> (),
) -> MouseEventHandler<Tag, V> {
MouseEventHandler::new(0, cx, |state, _| {
change: F,
) -> MouseEventHandler<Tag, V>
where
Tag: 'static,
D: Element<V>,
V: View,
F: 'static + Fn(&mut V, bool, &mut EventContext<V>),
{
MouseEventHandler::new(id, cx, |state, _| {
let indicator = if checked {
svg(&style.icon)
} else {
@ -75,8 +87,8 @@ pub fn checkbox_with_label<Tag: 'static, D: Element<V>, V: View>(
.with_child(label)
.align_children_center()
})
.on_click(platform::MouseButton::Left, move |_, _, cx| {
change(!checked, cx)
.on_click(platform::MouseButton::Left, move |_, view, cx| {
change(view, !checked, cx)
})
.with_cursor_style(platform::CursorStyle::PointingHand)
}

View file

@ -16,7 +16,7 @@ anyhow = "1.0.38"
backtrace = "0.3"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
lazy_static = "1.4.0"
futures = "0.3"
futures = { workspace = true }
isahc = "1.7"
smol = "1.2.5"
url = "2.2"

View file

@ -126,7 +126,7 @@ impl View for WelcomePage {
.with_child(
Flex::column()
.with_child(
theme::ui::checkbox_with_label::<Metrics, _, Self>(
theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
Flex::column()
.with_child(
Label::new(
@ -146,8 +146,9 @@ impl View for WelcomePage {
),
&theme.welcome.checkbox,
metrics,
0,
cx,
|checked, cx| {
|_, checked, cx| {
SettingsFile::update(cx, move |file| {
file.telemetry.set_metrics(checked)
})
@ -157,12 +158,13 @@ impl View for WelcomePage {
.with_style(theme.welcome.checkbox_container),
)
.with_child(
theme::ui::checkbox::<Diagnostics, Self>(
theme::ui::checkbox::<Diagnostics, Self, _>(
"Send crash reports",
&theme.welcome.checkbox,
diagnostics,
0,
cx,
|checked, cx| {
|_, checked, cx| {
SettingsFile::update(cx, move |file| {
file.telemetry.set_diagnostics(checked)
})

View file

@ -38,7 +38,7 @@ util = { path = "../util" }
async-recursion = "1.0.0"
bincode = "1.2.1"
anyhow = "1.0.38"
futures = "0.3"
futures = { workspace = true }
lazy_static = "1.4"
env_logger = "0.9.1"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }

View file

@ -46,6 +46,7 @@ journal = { path = "../journal" }
language = { path = "../language" }
language_selector = { path = "../language_selector" }
lsp = { path = "../lsp" }
lsp_log = { path = "../lsp_log" }
node_runtime = { path = "../node_runtime" }
outline = { path = "../outline" }
plugin_runtime = { path = "../plugin_runtime" }
@ -76,7 +77,7 @@ chrono = "0.4"
ctor = "0.1.20"
easy-parallel = "3.1.0"
env_logger = "0.9"
futures = "0.3"
futures = { workspace = true }
ignore = "0.4"
image = "0.23"
indexmap = "1.6.2"
@ -109,7 +110,7 @@ tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
tree-sitter-embedded-template = "0.20.0"
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
tree-sitter-rust = "0.20.3"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-python = "0.20.2"

View file

@ -262,6 +262,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
);
activity_indicator::init(cx);
copilot_button::init(cx);
lsp_log::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
settings::KeymapFileContent::load_defaults(cx);
}
@ -273,7 +274,7 @@ pub fn initialize_workspace(
) {
let workspace_handle = cx.handle();
cx.subscribe(&workspace_handle, {
move |_, _, event, cx| {
move |workspace, _, event, cx| {
if let workspace::Event::PaneAdded(pane) = event {
pane.update(cx, |pane, cx| {
pane.toolbar().update(cx, |toolbar, cx| {
@ -287,6 +288,10 @@ pub fn initialize_workspace(
toolbar.add_item(submit_feedback_button, cx);
let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
toolbar.add_item(feedback_info_text, cx);
let lsp_log_item = cx.add_view(|_| {
lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
});
toolbar.add_item(lsp_log_item, cx);
})
});
}