use collections::{HashMap, VecDeque};use copilot::Copilot;use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll};use futures::{StreamExt, channel::mpsc};use gpui::{ AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,};use language::{LanguageServerId, language_settings::SoftWrap};use lsp::{ IoKind, LanguageServer, LanguageServerName, MessageType, SetTraceParams, TraceValue, notification::SetTrace,};use project::{Project, WorktreeId, search::SearchQuery};use std::{any::TypeId, borrow::Cow, sync::Arc};use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*};use workspace::{ SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, item::{Item, ItemHandle}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},}; const SEND_LINE: &str = "// Send:\n";const RECEIVE_LINE: &str = "// Receive:\n";const MAX_STORED_LOG_ENTRIES: usize = 2000; pub struct LogStore { projects: HashMap<WeakEntity<Project>, ProjectState>, language_servers: HashMap<LanguageServerId, LanguageServerState>, copilot_log_subscription: Option<lsp::Subscription>, _copilot_subscription: Option<gpui::Subscription>, io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>,} struct ProjectState { _subscriptions: [gpui::Subscription; 2],} trait Message: AsRef<str> { type Level: Copy + std::fmt::Debug; fn should_include(&self, _: Self::Level) -> bool { true }} struct LogMessage { message: String, typ: MessageType,} impl AsRef<str> for LogMessage { fn as_ref(&self) -> &str { &self.message }} impl Message for LogMessage { type Level = MessageType; fn should_include(&self, level: Self::Level) -> bool { match (self.typ, level) { (MessageType::ERROR, _) => true, (_, MessageType::ERROR) => false, (MessageType::WARNING, _) => true, (_, MessageType::WARNING) => false, (MessageType::INFO, _) => true, (_, MessageType::INFO) => false, _ => true, } }} struct TraceMessage { message: String,} impl AsRef<str> for TraceMessage { fn as_ref(&self) -> &str { &self.message }} impl Message for TraceMessage { type Level = ();} struct RpcMessage { message: String,} impl AsRef<str> for RpcMessage { fn as_ref(&self) -> &str { &self.message }} impl Message for RpcMessage { type Level = ();} struct LanguageServerState { name: Option<LanguageServerName>, worktree_id: Option<WorktreeId>, kind: LanguageServerKind, log_messages: VecDeque<LogMessage>, trace_messages: VecDeque<TraceMessage>, rpc_state: Option<LanguageServerRpcState>, trace_level: TraceValue, log_level: MessageType, io_logs_subscription: Option<lsp::Subscription>,} #[derive(PartialEq, Clone)]pub enum LanguageServerKind { Local { project: WeakEntity<Project> }, Remote { project: WeakEntity<Project> }, Global,} impl LanguageServerKind { fn is_remote(&self) -> bool { matches!(self, LanguageServerKind::Remote { .. }) }} impl std::fmt::Debug for LanguageServerKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"), LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"), LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"), } }} impl LanguageServerKind { fn project(&self) -> Option<&WeakEntity<Project>> { match self { Self::Local { project } => Some(project), Self::Remote { project } => Some(project), Self::Global { .. } => None, } }} struct LanguageServerRpcState { rpc_messages: VecDeque<RpcMessage>, last_message_kind: Option<MessageKind>,} pub struct LspLogView { pub(crate) editor: Entity<Editor>, editor_subscriptions: Vec<Subscription>, log_store: Entity<LogStore>, current_server_id: Option<LanguageServerId>, active_entry_kind: LogKind, project: Entity<Project>, focus_handle: FocusHandle, _log_store_subscriptions: Vec<Subscription>,} pub struct LspLogToolbarItemView { log_view: Option<Entity<LspLogView>>, _log_view_subscription: Option<Subscription>,} #[derive(Copy, Clone, PartialEq, Eq)]enum MessageKind { Send, Receive,} #[derive(Clone, Copy, Debug, Default, PartialEq)]pub enum LogKind { Rpc, Trace, #[default] Logs, ServerInfo,} impl LogKind { fn label(&self) -> &'static str { match self { LogKind::Rpc => RPC_MESSAGES, LogKind::Trace => SERVER_TRACE, LogKind::Logs => SERVER_LOGS, LogKind::ServerInfo => SERVER_INFO, } }} #[derive(Clone, Debug, PartialEq)]pub(crate) struct LogMenuItem { pub server_id: LanguageServerId, pub server_name: LanguageServerName, pub worktree_root_name: String, pub rpc_trace_enabled: bool, pub selected_entry: LogKind, pub trace_level: lsp::TraceValue, pub server_kind: LanguageServerKind,} actions!(dev, [OpenLanguageServerLogs]); pub fn init(cx: &mut App) { let log_store = cx.new(LogStore::new); cx.observe_new(move |workspace: &mut Workspace, _, cx| { let project = workspace.project(); if project.read(cx).is_local() || project.read(cx).is_via_ssh() { log_store.update(cx, |store, cx| { store.add_project(project, cx); }); } let log_store = log_store.clone(); workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| { let project = workspace.project().read(cx); if project.is_local() || project.is_via_ssh() { workspace.split_item( SplitDirection::Right, Box::new(cx.new(|cx| { LspLogView::new(workspace.project().clone(), log_store.clone(), window, cx) })), window, cx, ); } }); }) .detach();} impl LogStore { pub fn new(cx: &mut Context<Self>) -> Self { let (io_tx, mut io_rx) = mpsc::unbounded(); let copilot_subscription = Copilot::global(cx).map(|copilot| { let copilot = &copilot; cx.subscribe(copilot, |this, copilot, inline_completion_event, cx| { if let copilot::Event::CopilotLanguageServerStarted = inline_completion_event { if let Some(server) = copilot.read(cx).language_server() { let server_id = server.server_id(); let weak_this = cx.weak_entity(); this.copilot_log_subscription = Some(server.on_notification::<copilot::request::LogMessage, _>( move |params, cx| { weak_this .update(cx, |this, cx| { this.add_language_server_log( server_id, MessageType::LOG, ¶ms.message, cx, ); }) .ok(); }, )); let name = LanguageServerName::new_static("copilot"); this.add_language_server( LanguageServerKind::Global, server.server_id(), Some(name), None, Some(server.clone()), cx, ); } } }) }); let this = Self { copilot_log_subscription: None, _copilot_subscription: copilot_subscription, projects: HashMap::default(), language_servers: HashMap::default(), io_tx, }; cx.spawn(async move |this, cx| { while let Some((server_id, io_kind, message)) = io_rx.next().await { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { this.on_io(server_id, io_kind, &message, cx); })?; } } anyhow::Ok(()) }) .detach_and_log_err(cx); this } pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) { let weak_project = project.downgrade(); self.projects.insert( project.downgrade(), ProjectState { _subscriptions: [ cx.observe_release(project, move |this, _, _| { this.projects.remove(&weak_project); this.language_servers .retain(|_, state| state.kind.project() != Some(&weak_project)); }), cx.subscribe(project, |this, project, event, cx| { let server_kind = if project.read(cx).is_via_ssh() { LanguageServerKind::Remote { project: project.downgrade(), } } else { LanguageServerKind::Local { project: project.downgrade(), } }; match event { project::Event::LanguageServerAdded(id, name, worktree_id) => { this.add_language_server( server_kind, *id, Some(name.clone()), *worktree_id, project .read(cx) .lsp_store() .read(cx) .language_server_for_id(*id), cx, ); } project::Event::LanguageServerRemoved(id) => { this.remove_language_server(*id, cx); } project::Event::LanguageServerLog(id, typ, message) => { this.add_language_server(server_kind, *id, None, None, None, cx); match typ { project::LanguageServerLogType::Log(typ) => { this.add_language_server_log(*id, *typ, message, cx); } project::LanguageServerLogType::Trace(_) => { this.add_language_server_trace(*id, message, cx); } } } _ => {} } }), ], }, ); } fn get_language_server_state( &mut self, id: LanguageServerId, ) -> Option<&mut LanguageServerState> { self.language_servers.get_mut(&id) } fn add_language_server( &mut self, kind: LanguageServerKind, server_id: LanguageServerId, name: Option<LanguageServerName>, worktree_id: Option<WorktreeId>, server: Option<Arc<LanguageServer>>, cx: &mut Context<Self>, ) -> Option<&mut LanguageServerState> { let server_state = self.language_servers.entry(server_id).or_insert_with(|| { cx.notify(); LanguageServerState { name: None, worktree_id: None, kind, rpc_state: None, log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), trace_level: TraceValue::Off, log_level: MessageType::LOG, io_logs_subscription: None, } }); if let Some(name) = name { server_state.name = Some(name); } if let Some(worktree_id) = worktree_id { server_state.worktree_id = Some(worktree_id); } if let Some(server) = server .clone() .filter(|_| server_state.io_logs_subscription.is_none()) { let io_tx = self.io_tx.clone(); let server_id = server.server_id(); server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { io_tx .unbounded_send((server_id, io_kind, message.to_string())) .ok(); })); } Some(server_state) } fn add_language_server_log( &mut self, id: LanguageServerId, typ: MessageType, message: &str, cx: &mut Context<Self>, ) -> Option<()> { let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.log_messages; Self::add_language_server_message( log_lines, id, LogMessage { message: message.trim_end().to_string(), typ, }, language_server_state.log_level, LogKind::Logs, cx, ); Some(()) } fn add_language_server_trace( &mut self, id: LanguageServerId, message: &str, cx: &mut Context<Self>, ) -> Option<()> { let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.trace_messages; Self::add_language_server_message( log_lines, id, TraceMessage { message: message.trim_end().to_string(), }, (), LogKind::Trace, cx, ); Some(()) } fn add_language_server_message<T: Message>( log_lines: &mut VecDeque<T>, id: LanguageServerId, message: T, current_severity: <T as Message>::Level, kind: LogKind, cx: &mut Context<Self>, ) { while log_lines.len() >= MAX_STORED_LOG_ENTRIES { log_lines.pop_front(); } let entry = format!("{}\n", message.as_ref().trim()); let visible = message.should_include(current_severity); log_lines.push_back(message); if visible { cx.emit(Event::NewServerLogEntry { id, entry, kind }); cx.notify(); } } fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) { self.language_servers.remove(&id); cx.notify(); } fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> { Some(&self.language_servers.get(&server_id)?.log_messages) } fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<TraceMessage>> { Some(&self.language_servers.get(&server_id)?.trace_messages) } fn server_ids_for_project<'a>( &'a self, lookup_project: &'a WeakEntity<Project>, ) -> impl Iterator<Item = LanguageServerId> + 'a { self.language_servers .iter() .filter_map(move |(id, state)| match &state.kind { LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => { if project == lookup_project { Some(*id) } else {Lines 1–500 of 1608. Load more
use collections::{HashMap, VecDeque}; use copilot::Copilot; use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll}; use futures::{StreamExt, channel::mpsc}; use gpui::{ AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div, }; use language::{LanguageServerId, language_settings::SoftWrap}; use lsp::{ IoKind, LanguageServer, LanguageServerName, MessageType, SetTraceParams, TraceValue, notification::SetTrace, }; use project::{Project, WorktreeId, search::SearchQuery}; use std::{any::TypeId, borrow::Cow, sync::Arc}; use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*}; use workspace::{ SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, item::{Item, ItemHandle}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; const SEND_LINE: &str = "// Send:\n"; const RECEIVE_LINE: &str = "// Receive:\n"; const MAX_STORED_LOG_ENTRIES: usize = 2000; pub struct LogStore { projects: HashMap<WeakEntity<Project>, ProjectState>, language_servers: HashMap<LanguageServerId, LanguageServerState>, copilot_log_subscription: Option<lsp::Subscription>, _copilot_subscription: Option<gpui::Subscription>, io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>, } struct ProjectState { _subscriptions: [gpui::Subscription; 2], } trait Message: AsRef<str> { type Level: Copy + std::fmt::Debug; fn should_include(&self, _: Self::Level) -> bool { true } } struct LogMessage { message: String, typ: MessageType, } impl AsRef<str> for LogMessage { fn as_ref(&self) -> &str { &self.message } } impl Message for LogMessage { type Level = MessageType; fn should_include(&self, level: Self::Level) -> bool { match (self.typ, level) { (MessageType::ERROR, _) => true, (_, MessageType::ERROR) => false, (MessageType::WARNING, _) => true, (_, MessageType::WARNING) => false, (MessageType::INFO, _) => true, (_, MessageType::INFO) => false, _ => true, } } } struct TraceMessage { message: String, } impl AsRef<str> for TraceMessage { fn as_ref(&self) -> &str { &self.message } } impl Message for TraceMessage { type Level = (); } struct RpcMessage { message: String, } impl AsRef<str> for RpcMessage { fn as_ref(&self) -> &str { &self.message } } impl Message for RpcMessage { type Level = (); } struct LanguageServerState { name: Option<LanguageServerName>, worktree_id: Option<WorktreeId>, kind: LanguageServerKind, log_messages: VecDeque<LogMessage>, trace_messages: VecDeque<TraceMessage>, rpc_state: Option<LanguageServerRpcState>, trace_level: TraceValue, log_level: MessageType, io_logs_subscription: Option<lsp::Subscription>, } #[derive(PartialEq, Clone)] pub enum LanguageServerKind { Local { project: WeakEntity<Project> }, Remote { project: WeakEntity<Project> }, Global, } impl LanguageServerKind { fn is_remote(&self) -> bool { matches!(self, LanguageServerKind::Remote { .. }) } } impl std::fmt::Debug for LanguageServerKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"), LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"), LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"), } } } impl LanguageServerKind { fn project(&self) -> Option<&WeakEntity<Project>> { match self { Self::Local { project } => Some(project), Self::Remote { project } => Some(project), Self::Global { .. } => None, } } } struct LanguageServerRpcState { rpc_messages: VecDeque<RpcMessage>, last_message_kind: Option<MessageKind>, } pub struct LspLogView { pub(crate) editor: Entity<Editor>, editor_subscriptions: Vec<Subscription>, log_store: Entity<LogStore>, current_server_id: Option<LanguageServerId>, active_entry_kind: LogKind, project: Entity<Project>, focus_handle: FocusHandle, _log_store_subscriptions: Vec<Subscription>, } pub struct LspLogToolbarItemView { log_view: Option<Entity<LspLogView>>, _log_view_subscription: Option<Subscription>, } #[derive(Copy, Clone, PartialEq, Eq)] enum MessageKind { Send, Receive, } #[derive(Clone, Copy, Debug, Default, PartialEq)] pub enum LogKind { Rpc, Trace, #[default] Logs, ServerInfo, } impl LogKind { fn label(&self) -> &'static str { match self { LogKind::Rpc => RPC_MESSAGES, LogKind::Trace => SERVER_TRACE, LogKind::Logs => SERVER_LOGS, LogKind::ServerInfo => SERVER_INFO, } } } #[derive(Clone, Debug, PartialEq)] pub(crate) struct LogMenuItem { pub server_id: LanguageServerId, pub server_name: LanguageServerName, pub worktree_root_name: String, pub rpc_trace_enabled: bool, pub selected_entry: LogKind, pub trace_level: lsp::TraceValue, pub server_kind: LanguageServerKind, } actions!(dev, [OpenLanguageServerLogs]); pub fn init(cx: &mut App) { let log_store = cx.new(LogStore::new); cx.observe_new(move |workspace: &mut Workspace, _, cx| { let project = workspace.project(); if project.read(cx).is_local() || project.read(cx).is_via_ssh() { log_store.update(cx, |store, cx| { store.add_project(project, cx); }); } let log_store = log_store.clone(); workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| { let project = workspace.project().read(cx); if project.is_local() || project.is_via_ssh() { workspace.split_item( SplitDirection::Right, Box::new(cx.new(|cx| { LspLogView::new(workspace.project().clone(), log_store.clone(), window, cx) })), window, cx, ); } }); }) .detach(); } impl LogStore { pub fn new(cx: &mut Context<Self>) -> Self { let (io_tx, mut io_rx) = mpsc::unbounded(); let copilot_subscription = Copilot::global(cx).map(|copilot| { let copilot = &copilot; cx.subscribe(copilot, |this, copilot, inline_completion_event, cx| { if let copilot::Event::CopilotLanguageServerStarted = inline_completion_event { if let Some(server) = copilot.read(cx).language_server() { let server_id = server.server_id(); let weak_this = cx.weak_entity(); this.copilot_log_subscription = Some(server.on_notification::<copilot::request::LogMessage, _>( move |params, cx| { weak_this .update(cx, |this, cx| { this.add_language_server_log( server_id, MessageType::LOG, ¶ms.message, cx, ); }) .ok(); }, )); let name = LanguageServerName::new_static("copilot"); this.add_language_server( LanguageServerKind::Global, server.server_id(), Some(name), None, Some(server.clone()), cx, ); } } }) }); let this = Self { copilot_log_subscription: None, _copilot_subscription: copilot_subscription, projects: HashMap::default(), language_servers: HashMap::default(), io_tx, }; cx.spawn(async move |this, cx| { while let Some((server_id, io_kind, message)) = io_rx.next().await { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { this.on_io(server_id, io_kind, &message, cx); })?; } } anyhow::Ok(()) }) .detach_and_log_err(cx); this } pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) { let weak_project = project.downgrade(); self.projects.insert( project.downgrade(), ProjectState { _subscriptions: [ cx.observe_release(project, move |this, _, _| { this.projects.remove(&weak_project); this.language_servers .retain(|_, state| state.kind.project() != Some(&weak_project)); }), cx.subscribe(project, |this, project, event, cx| { let server_kind = if project.read(cx).is_via_ssh() { LanguageServerKind::Remote { project: project.downgrade(), } } else { LanguageServerKind::Local { project: project.downgrade(), } }; match event { project::Event::LanguageServerAdded(id, name, worktree_id) => { this.add_language_server( server_kind, *id, Some(name.clone()), *worktree_id, project .read(cx) .lsp_store() .read(cx) .language_server_for_id(*id), cx, ); } project::Event::LanguageServerRemoved(id) => { this.remove_language_server(*id, cx); } project::Event::LanguageServerLog(id, typ, message) => { this.add_language_server(server_kind, *id, None, None, None, cx); match typ { project::LanguageServerLogType::Log(typ) => { this.add_language_server_log(*id, *typ, message, cx); } project::LanguageServerLogType::Trace(_) => { this.add_language_server_trace(*id, message, cx); } } } _ => {} } }), ], }, ); } fn get_language_server_state( &mut self, id: LanguageServerId, ) -> Option<&mut LanguageServerState> { self.language_servers.get_mut(&id) } fn add_language_server( &mut self, kind: LanguageServerKind, server_id: LanguageServerId, name: Option<LanguageServerName>, worktree_id: Option<WorktreeId>, server: Option<Arc<LanguageServer>>, cx: &mut Context<Self>, ) -> Option<&mut LanguageServerState> { let server_state = self.language_servers.entry(server_id).or_insert_with(|| { cx.notify(); LanguageServerState { name: None, worktree_id: None, kind, rpc_state: None, log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), trace_level: TraceValue::Off, log_level: MessageType::LOG, io_logs_subscription: None, } }); if let Some(name) = name { server_state.name = Some(name); } if let Some(worktree_id) = worktree_id { server_state.worktree_id = Some(worktree_id); } if let Some(server) = server .clone() .filter(|_| server_state.io_logs_subscription.is_none()) { let io_tx = self.io_tx.clone(); let server_id = server.server_id(); server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { io_tx .unbounded_send((server_id, io_kind, message.to_string())) .ok(); })); } Some(server_state) } fn add_language_server_log( &mut self, id: LanguageServerId, typ: MessageType, message: &str, cx: &mut Context<Self>, ) -> Option<()> { let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.log_messages; Self::add_language_server_message( log_lines, id, LogMessage { message: message.trim_end().to_string(), typ, }, language_server_state.log_level, LogKind::Logs, cx, ); Some(()) } fn add_language_server_trace( &mut self, id: LanguageServerId, message: &str, cx: &mut Context<Self>, ) -> Option<()> { let language_server_state = self.get_language_server_state(id)?; let log_lines = &mut language_server_state.trace_messages; Self::add_language_server_message( log_lines, id, TraceMessage { message: message.trim_end().to_string(), }, (), LogKind::Trace, cx, ); Some(()) } fn add_language_server_message<T: Message>( log_lines: &mut VecDeque<T>, id: LanguageServerId, message: T, current_severity: <T as Message>::Level, kind: LogKind, cx: &mut Context<Self>, ) { while log_lines.len() >= MAX_STORED_LOG_ENTRIES { log_lines.pop_front(); } let entry = format!("{}\n", message.as_ref().trim()); let visible = message.should_include(current_severity); log_lines.push_back(message); if visible { cx.emit(Event::NewServerLogEntry { id, entry, kind }); cx.notify(); } } fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) { self.language_servers.remove(&id); cx.notify(); } fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> { Some(&self.language_servers.get(&server_id)?.log_messages) } fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<TraceMessage>> { Some(&self.language_servers.get(&server_id)?.trace_messages) } fn server_ids_for_project<'a>( &'a self, lookup_project: &'a WeakEntity<Project>, ) -> impl Iterator<Item = LanguageServerId> + 'a { self.language_servers .iter() .filter_map(move |(id, state)| match &state.kind { LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => { if project == lookup_project { Some(*id) } else {