diff --git a/.vscode/launch.json b/.vscode/launch.json index b4f69d35..0a164290 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,11 +2,12 @@ { "version": "1.0.0", "configurations": [ + { "name": "Launch Server (lldb)", "type": "lldb", "request": "launch", - "args": ["--use-tcp"], + "args": ["--use-tcp", "--spy"], "cargo": { "args": [ "build" @@ -33,7 +34,7 @@ "type": "cppvsdbg", "request": "launch", "program": "${workspaceRoot}/server/target/debug/odoo_ls_server.exe", - "args": ["--use-tcp"], + "args": ["--use-tcp", "--spy"], "cwd": "${workspaceFolder}/server", "console": "externalTerminal", "preLaunchTask": "cargo build" diff --git a/server/src/args.rs b/server/src/args.rs index d5109ef1..89af5f8e 100644 --- a/server/src/args.rs +++ b/server/src/args.rs @@ -59,6 +59,9 @@ pub struct Cli { #[arg(long)] pub config_path: Option, + //enable connection on localhost:8072 for odoo-ls-spy debugging tool + #[arg(long)] + pub spy: bool, } #[derive(ValueEnum, Clone, Debug)] diff --git a/server/src/constants.rs b/server/src/constants.rs index 7639b908..90be045b 100644 --- a/server/src/constants.rs +++ b/server/src/constants.rs @@ -97,6 +97,12 @@ impl From for BuildSteps { } } +impl fmt::Display for BuildSteps { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + #[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum BuildStatus { PENDING, @@ -105,6 +111,12 @@ pub enum BuildStatus { DONE } +impl fmt::Display for BuildStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + pub const BUILT_IN_LIBS: &[&str] = &["string", "re", "difflib", "textwrap", "unicodedata", "stringprep", "readline", "rlcompleter", "datetime", "zoneinfo", "calendar", "collections", "heapq", "bisect", "array", "weakref", "types", "copy", "pprint", "reprlib", "enum", "graphlib", "numbers", "math", "cmath", "decimal", "fractions", "random", "statistics", "itertools", diff --git a/server/src/core/model.rs b/server/src/core/model.rs index a386cce4..f36c6db4 100644 --- a/server/src/core/model.rs +++ b/server/src/core/model.rs @@ -87,6 +87,10 @@ impl Model { res } + pub fn get_name(&self) -> String { + self.name.to_string() + } + pub fn add_symbol(&mut self, session: &mut SessionInfo, symbol: Rc>) { if self.symbols.contains(&symbol) { return; diff --git a/server/src/core/symbols/class_symbol.rs b/server/src/core/symbols/class_symbol.rs index c59740bd..ebe2c13a 100644 --- a/server/src/core/symbols/class_symbol.rs +++ b/server/src/core/symbols/class_symbol.rs @@ -1,4 +1,5 @@ use ruff_text_size::{TextRange, TextSize}; +use serde_json::json; use std::collections::HashMap; use std::rc::{Rc, Weak}; use std::cell::RefCell; @@ -122,4 +123,16 @@ impl ClassSymbol { result } + pub fn to_json(&self) -> serde_json::Value { + json!({ + "type": SymType::CLASS.to_string(), + "doc_string": self.doc_string, + "is_external": self.is_external, + "range": json!({ + "start": self.range.start().to_u32(), + "end": self.range.end().to_u32(), + }) + }) + } + } diff --git a/server/src/core/symbols/compiled_symbol.rs b/server/src/core/symbols/compiled_symbol.rs index 03dca25f..7d9705fa 100644 --- a/server/src/core/symbols/compiled_symbol.rs +++ b/server/src/core/symbols/compiled_symbol.rs @@ -1,8 +1,9 @@ use std::{cell::RefCell, collections::HashMap, rc::{Rc, Weak}}; +use serde_json::json; use weak_table::PtrWeakHashSet; -use crate::constants::OYarn; +use crate::constants::{OYarn, SymType}; use super::symbol::Symbol; @@ -35,4 +36,19 @@ impl CompiledSymbol { self.module_symbols.insert(compiled.borrow().name().clone(), compiled.clone()); } + pub fn to_json(&self) -> serde_json::Value { + let module_sym: Vec = self.module_symbols.values().map(|sym| { + json!({ + "name": sym.borrow().name().to_string(), + "type": sym.borrow().typ().to_string(), + }) + }).collect(); + json!({ + "type": SymType::COMPILED.to_string(), + "path": self.path, + "is_external": self.is_external, + "module_symbols": module_sym, + }) + } + } \ No newline at end of file diff --git a/server/src/core/symbols/csv_file_symbol.rs b/server/src/core/symbols/csv_file_symbol.rs index ddee58df..ab97a5b3 100644 --- a/server/src/core/symbols/csv_file_symbol.rs +++ b/server/src/core/symbols/csv_file_symbol.rs @@ -1,6 +1,7 @@ +use serde_json::json; use weak_table::PtrWeakHashSet; -use crate::{constants::{BuildStatus, BuildSteps, OYarn}, core::{file_mgr::NoqaInfo, model::Model, xml_data::OdooData}, oyarn}; +use crate::{constants::{BuildStatus, BuildSteps, OYarn, SymType}, core::{file_mgr::NoqaInfo, model::Model, xml_data::OdooData}, oyarn, tool_api::to_json::{dependencies_to_json, dependents_to_json}}; use std::{cell::RefCell, collections::HashMap, rc::{Rc, Weak}}; use super::{symbol::Symbol, symbol_mgr::SectionRange}; @@ -142,4 +143,50 @@ impl CsvFileSymbol { self.in_workspace } + pub fn to_json(&self) -> serde_json::Value { + json!({ + "type": SymType::CSV_FILE.to_string(), + "path": self.path, + "is_external": self.is_external, + "in_workspace": self.in_workspace, + "arch_status": self.arch_status.to_string(), + "validation_status": self.validation_status.to_string(), + "not_found_paths": self.not_found_paths.iter().map(|(step, paths)| { + json!({ + "step": step.to_string(), + "paths": paths.into_iter().map(|x| x.to_string()).collect::>(), + }) + }).collect::>(), + "self_import": self.self_import, + "model_dependencies": self.model_dependencies.iter().map(|x| json!(x.borrow().get_name())).collect::>(), + "dependencies": dependencies_to_json(&self.dependencies), + "dependents": dependents_to_json(&self.dependents), + "processed_text_hash": self.processed_text_hash, + + "sections": self.sections.iter().map(|x| { + json!({ + "start": x.start, + "index": x.index, + }) + }).collect::>(), + "symbols": self.symbols.iter().map(|(name, sections)| { + json!({ + "name": name.to_string(), + "sections": sections.iter().map(|(section, symbols)| { + json!({ + "section": section, + "symbols": symbols.iter().map(|sym| json!(sym.borrow().name().to_string())).collect::>(), + }) + }).collect::>(), + }) + }).collect::>(), + "ext_symbols": self.ext_symbols.iter().map(|(name, symbols)| { + json!({ + "name": name.to_string(), + "symbols": symbols.iter().map(|sym| json!(sym.borrow().name().to_string())).collect::>(), + }) + }).collect::>(), + }) + } + } \ No newline at end of file diff --git a/server/src/core/symbols/disk_dir_symbol.rs b/server/src/core/symbols/disk_dir_symbol.rs index 8ee923b4..e35be3bb 100644 --- a/server/src/core/symbols/disk_dir_symbol.rs +++ b/server/src/core/symbols/disk_dir_symbol.rs @@ -1,7 +1,9 @@ use std::{cell::RefCell, collections::HashMap, path::PathBuf, rc::{Rc, Weak}}; -use crate::{constants::OYarn, oyarn, utils::PathSanitizer}; +use serde_json::json; + +use crate::{constants::{OYarn, SymType}, oyarn, utils::PathSanitizer}; use super::symbol::Symbol; @@ -40,4 +42,20 @@ impl DiskDirSymbol { /*pub fn load(sesion: &mut SessionInfo, dir: &Rc>) -> Rc> { let path = dir.borrow().as_disk_dir_sym().path.clone(); }*/ + + pub fn to_json(&self) -> serde_json::Value { + let module_sym: Vec = self.module_symbols.values().map(|sym| { + json!({ + "name": sym.borrow().name().to_string(), + "type": sym.borrow().typ().to_string(), + }) + }).collect(); + json!({ + "type": SymType::DISK_DIR.to_string(), + "path": self.path, + "is_external": self.is_external, + "in_workspace": self.in_workspace, + "module_symbols": module_sym, + }) + } } \ No newline at end of file diff --git a/server/src/core/symbols/file_symbol.rs b/server/src/core/symbols/file_symbol.rs index f73727a0..2d76fb4d 100644 --- a/server/src/core/symbols/file_symbol.rs +++ b/server/src/core/symbols/file_symbol.rs @@ -1,6 +1,7 @@ +use serde_json::json; use weak_table::{PtrWeakHashSet, PtrWeakKeyHashMap}; -use crate::{constants::{BuildStatus, BuildSteps, OYarn}, core::{file_mgr::NoqaInfo, model::Model, xml_data::OdooData}, oyarn}; +use crate::{constants::{BuildStatus, BuildSteps, OYarn, SymType}, core::{file_mgr::NoqaInfo, model::Model, xml_data::OdooData}, oyarn, tool_api::to_json::{dependencies_to_json, dependents_to_json}}; use std::{cell::RefCell, collections::HashMap, rc::{Rc, Weak}}; use super::{symbol::Symbol, symbol_mgr::{SectionRange, SymbolMgr}}; @@ -169,4 +170,51 @@ impl FileSymbol { result } + pub fn to_json(&self) -> serde_json::Value { + json!({ + "type": SymType::FILE.to_string(), + "path": self.path, + "is_external": self.is_external, + "in_workspace": self.in_workspace, + "arch_status": self.arch_status.to_string(), + "arch_eval_status": self.arch_eval_status.to_string(), + "validation_status": self.validation_status.to_string(), + "not_found_paths": self.not_found_paths.iter().map(|(step, paths)| { + json!({ + "step": step.to_string(), + "paths": paths.into_iter().map(|x| x.to_string()).collect::>(), + }) + }).collect::>(), + "self_import": self.self_import, + "model_dependencies": self.model_dependencies.iter().map(|x| json!(x.borrow().get_name())).collect::>(), + "dependencies": dependencies_to_json(&self.dependencies), + "dependents": dependents_to_json(&self.dependents), + "processed_text_hash": self.processed_text_hash, + + "sections": self.sections.iter().map(|x| { + json!({ + "start": x.start, + "index": x.index, + }) + }).collect::>(), + "symbols": self.symbols.iter().map(|(name, sections)| { + json!({ + "name": name.to_string(), + "sections": sections.iter().map(|(section, symbols)| { + json!({ + "section": section, + "symbols": symbols.iter().map(|sym| json!(sym.borrow().name().to_string())).collect::>(), + }) + }).collect::>(), + }) + }).collect::>(), + "ext_symbols": self.ext_symbols.iter().map(|(name, symbols)| { + json!({ + "name": name.to_string(), + "symbols": symbols.iter().map(|sym| json!(sym.borrow().name().to_string())).collect::>(), + }) + }).collect::>(), + }) + } + } \ No newline at end of file diff --git a/server/src/core/symbols/function_symbol.rs b/server/src/core/symbols/function_symbol.rs index 213b496d..f961c457 100644 --- a/server/src/core/symbols/function_symbol.rs +++ b/server/src/core/symbols/function_symbol.rs @@ -3,6 +3,7 @@ use std::{cell::RefCell, collections::HashMap, rc::{Rc, Weak}}; use lsp_types::Diagnostic; use ruff_python_ast::{AtomicNodeIndex, Expr, ExprCall}; use ruff_text_size::{TextRange, TextSize}; +use serde_json::json; use weak_table::{PtrWeakHashSet, PtrWeakKeyHashMap}; use crate::{constants::{BuildStatus, BuildSteps, OYarn, SymType}, core::{evaluation::{Context, Evaluation}, file_mgr::NoqaInfo, model::Model}, oyarn, threads::SessionInfo}; @@ -210,4 +211,17 @@ impl FunctionSymbol { } result } + + pub fn to_json(&self) -> serde_json::Value { + json!({ + "type": SymType::FUNCTION.to_string(), + "doc_string": self.doc_string, + "is_external": self.is_external, + "range": json!({ + "start": self.range.start().to_u32(), + "end": self.range.end().to_u32(), + }), + "evaluations": self.evaluations.iter().map(|eval| json!("to implement")).collect::>(), + }) + } } diff --git a/server/src/core/symbols/module_symbol.rs b/server/src/core/symbols/module_symbol.rs index a043407e..e3032eb8 100644 --- a/server/src/core/symbols/module_symbol.rs +++ b/server/src/core/symbols/module_symbol.rs @@ -1,6 +1,7 @@ use lsp_types::{Diagnostic, DiagnosticTag, Position, Range}; use ruff_python_ast::{Expr, Stmt}; use ruff_text_size::{Ranged, TextRange}; +use serde_json::json; use tracing::{error, info}; use weak_table::{PtrWeakHashSet, PtrWeakKeyHashMap}; use std::collections::{HashMap, HashSet}; @@ -589,4 +590,20 @@ impl ModuleSymbol { res } + pub fn to_json(&self) -> serde_json::Value { + let module_sym: Vec = self.module_symbols.values().map(|sym| { + json!({ + "name": sym.borrow().name().to_string(), + "type": sym.borrow().typ().to_string(), + }) + }).collect(); + json!({ + "type": "MODULE_PACKAGE", + "path": self.path, + "is_external": self.is_external, + "in_workspace": self.in_workspace, + "module_symbols": module_sym, + }) + } + } diff --git a/server/src/core/symbols/namespace_symbol.rs b/server/src/core/symbols/namespace_symbol.rs index 82d8a3ca..0413b456 100644 --- a/server/src/core/symbols/namespace_symbol.rs +++ b/server/src/core/symbols/namespace_symbol.rs @@ -1,8 +1,9 @@ +use serde_json::json; use weak_table::PtrWeakHashSet; use std::{cell::RefCell, collections::HashMap, path::PathBuf, rc::{Rc, Weak}}; -use crate::constants::OYarn; +use crate::{constants::{OYarn, SymType}, tool_api::to_json::{dependencies_to_json, dependents_to_json}}; use super::symbol::Symbol; @@ -144,6 +145,30 @@ impl NamespaceSymbol { pub fn is_in_workspace(&self) -> bool { self.in_workspace } + + pub fn to_json(&self) -> serde_json::Value { + let mut directories = vec![]; + for directory in self.directories.iter() { + let module_sym: Vec = directory.module_symbols.values().map(|sym| { + json!({ + "name": sym.borrow().name().to_string(), + "type": sym.borrow().typ().to_string(), + }) + }).collect(); + directories.push(json!({ + "path": directory.path, + "module_symbols": module_sym, + })); + } + json!({ + "type": SymType::NAMESPACE.to_string(), + "is_external": self.is_external, + "in_workspace": self.in_workspace, + "directories": directories, + "dependencies": dependencies_to_json(&self.dependencies), + "dependents": dependents_to_json(&self.dependents), + }) + } pub fn get_ext_symbol(&self, name: &OYarn) -> Vec>> { let mut result = vec![]; diff --git a/server/src/core/symbols/package_symbol.rs b/server/src/core/symbols/package_symbol.rs index 8a1798b8..778ab952 100644 --- a/server/src/core/symbols/package_symbol.rs +++ b/server/src/core/symbols/package_symbol.rs @@ -1,3 +1,4 @@ +use serde_json::json; use weak_table::{PtrWeakHashSet, PtrWeakKeyHashMap}; use crate::{constants::{BuildStatus, BuildSteps, OYarn}, core::{file_mgr::NoqaInfo, model::Model, xml_data::OdooData}, oyarn, threads::SessionInfo, S}; @@ -261,4 +262,20 @@ impl PythonPackageSymbol { result } + pub fn to_json(&self) -> serde_json::Value { + let module_sym: Vec = self.module_symbols.values().map(|sym| { + json!({ + "name": sym.borrow().name().to_string(), + "type": sym.borrow().typ().to_string(), + }) + }).collect(); + json!({ + "type": "PYTHON_PACKAGE", + "path": self.path, + "is_external": self.is_external, + "in_workspace": self.in_workspace, + "module_symbols": module_sym, + }) + } + } \ No newline at end of file diff --git a/server/src/core/symbols/root_symbol.rs b/server/src/core/symbols/root_symbol.rs index f7824c61..cc3610f8 100644 --- a/server/src/core/symbols/root_symbol.rs +++ b/server/src/core/symbols/root_symbol.rs @@ -1,4 +1,6 @@ -use crate::{constants::OYarn, core::entry_point::EntryPoint, oyarn}; +use serde_json::json; + +use crate::{constants::{OYarn, SymType}, core::entry_point::EntryPoint, oyarn}; use std::{cell::RefCell, collections::HashMap, rc::{Rc, Weak}}; use super::symbol::Symbol; @@ -31,4 +33,17 @@ impl RootSymbol { self.module_symbols.insert(file.borrow().name().clone(), file.clone()); } + pub fn to_json(&self) -> serde_json::Value { + let module_sym: Vec = self.module_symbols.values().map(|sym| { + json!({ + "name": sym.borrow().name().to_string(), + "type": sym.borrow().typ().to_string(), + }) + }).collect(); + json!({ + "type": SymType::ROOT.to_string(), + "module_symbols": module_sym, + }) + } + } diff --git a/server/src/core/symbols/symbol.rs b/server/src/core/symbols/symbol.rs index c5bf4dca..a93aa397 100644 --- a/server/src/core/symbols/symbol.rs +++ b/server/src/core/symbols/symbol.rs @@ -495,6 +495,20 @@ impl Symbol { } } + pub fn as_compiled(&self) -> &CompiledSymbol { + match self { + Symbol::Compiled(c) => c, + _ => {panic!("Not a compiled symbol")} + } + } + + pub fn as_compiled_mut(&mut self) -> &mut CompiledSymbol { + match self { + Symbol::Compiled(c) => c, + _ => {panic!("Not a compiled symbol")} + } + } + pub fn as_variable(&self) -> &VariableSymbol { match self { Symbol::Variable(v) => v, diff --git a/server/src/core/symbols/variable_symbol.rs b/server/src/core/symbols/variable_symbol.rs index 0e758e6d..482b9a6b 100644 --- a/server/src/core/symbols/variable_symbol.rs +++ b/server/src/core/symbols/variable_symbol.rs @@ -1,4 +1,5 @@ use ruff_text_size::TextRange; +use serde_json::json; use crate::{constants::{OYarn, SymType}, core::evaluation::{ContextValue, Evaluation}, oyarn, threads::SessionInfo, S}; use std::{cell::RefCell, collections::HashMap, rc::{Rc, Weak}}; @@ -87,4 +88,19 @@ impl VariableSymbol { vec![] } + pub fn to_json(&self) -> serde_json::Value { + json!({ + "type": SymType::VARIABLE.to_string(), + "doc_string": self.doc_string, + "is_external": self.is_external, + "range": json!({ + "start": self.range.start().to_u32(), + "end": self.range.end().to_u32(), + }), + "is_import_variable": self.is_import_variable, + "is_parameter": self.is_parameter, + "evaluations": self.evaluations.iter().map(|eval| json!("to implement")).collect::>(), + }) + } + } \ No newline at end of file diff --git a/server/src/core/symbols/xml_file_symbol.rs b/server/src/core/symbols/xml_file_symbol.rs index 22d2601e..3b813b7d 100644 --- a/server/src/core/symbols/xml_file_symbol.rs +++ b/server/src/core/symbols/xml_file_symbol.rs @@ -1,7 +1,10 @@ use lsp_types::Diagnostic; use roxmltree::Error; +use serde_json::json; use weak_table::PtrWeakHashSet; +use crate::constants::SymType; +use crate::tool_api::to_json::{dependencies_to_json, dependents_to_json}; use crate::{core::diagnostics::DiagnosticCode, threads::SessionInfo}; use crate::{constants::{BuildStatus, BuildSteps, OYarn}, core::{file_mgr::{FileInfo, NoqaInfo}, model::Model, xml_data::OdooData}, oyarn}; use std::{cell::RefCell, collections::HashMap, rc::{Rc, Weak}}; @@ -153,4 +156,50 @@ impl XmlFileSymbol { } } + pub fn to_json(&self) -> serde_json::Value { + json!({ + "type": SymType::XML_FILE.to_string(), + "path": self.path, + "is_external": self.is_external, + "in_workspace": self.in_workspace, + "arch_status": self.arch_status.to_string(), + "validation_status": self.validation_status.to_string(), + "not_found_paths": self.not_found_paths.iter().map(|(step, paths)| { + json!({ + "step": step.to_string(), + "paths": paths.into_iter().map(|x| x.to_string()).collect::>(), + }) + }).collect::>(), + "self_import": self.self_import, + "model_dependencies": self.model_dependencies.iter().map(|x| json!(x.borrow().get_name())).collect::>(), + "dependencies": dependencies_to_json(&self.dependencies), + "dependents": dependents_to_json(&self.dependents), + "processed_text_hash": self.processed_text_hash, + + "sections": self.sections.iter().map(|x| { + json!({ + "start": x.start, + "index": x.index, + }) + }).collect::>(), + "symbols": self.symbols.iter().map(|(name, sections)| { + json!({ + "name": name.to_string(), + "sections": sections.iter().map(|(section, symbols)| { + json!({ + "section": section, + "symbols": symbols.iter().map(|sym| json!(sym.borrow().name().to_string())).collect::>(), + }) + }).collect::>(), + }) + }).collect::>(), + "ext_symbols": self.ext_symbols.iter().map(|(name, symbols)| { + json!({ + "name": name.to_string(), + "symbols": symbols.iter().map(|sym| json!(sym.borrow().name().to_string())).collect::>(), + }) + }).collect::>(), + }) + } + } \ No newline at end of file diff --git a/server/src/lib.rs b/server/src/lib.rs index 959a3044..dac619b1 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -8,4 +8,5 @@ pub mod features; pub mod server; pub mod tasks; pub mod utils; -pub mod crash_buffer; \ No newline at end of file +pub mod crash_buffer; +pub mod tool_api; diff --git a/server/src/main.rs b/server/src/main.rs index 62e9a0d9..9b6147a5 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -79,6 +79,9 @@ fn main() { info!("Compiled setting: DEBUG_STEPS: {}", DEBUG_STEPS); info!("Compiled setting: DEBUG_REBUILD_NOW: {}", DEBUG_REBUILD_NOW); info!("Operating system: {}", std::env::consts::OS); + if cli.spy { + info!("Spy mode enabled"); + } info!(""); if cli.parse { @@ -97,6 +100,9 @@ fn main() { cli.config_path.map(|config_path| { serv.set_config_path(config_path.clone()); }); + if cli.spy { + serv.create_spy_connection(serv.sync_odoo.clone()); + } let sender_panic = serv.connection.as_ref().unwrap().sender.clone(); std::panic::set_hook(Box::new(move |panic_info| { let backtrace = std::backtrace::Backtrace::capture(); diff --git a/server/src/server.rs b/server/src/server.rs index a5d29b3c..3a3c4f0b 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -1,4 +1,4 @@ -use std::{io::Error, panic, sync::{Arc, Mutex, atomic::AtomicBool}, thread::JoinHandle}; +use std::{io::Error, panic, sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}}, thread::JoinHandle}; use crossbeam_channel::{Receiver, Select, Sender}; use lsp_server::{Connection, IoThreads, Message, ProtocolError, RequestId, ResponseError}; @@ -8,7 +8,7 @@ use serde_json::json; use nix; use tracing::{error, info, warn}; -use crate::{constants::{DEBUG_THREADS, EXTENSION_VERSION}, core::{file_mgr::FileMgr, odoo::SyncOdoo}, threads::{delayed_changes_process_thread, message_processor_thread_main, DelayedProcessingMessage}, S, crash_buffer}; +use crate::{S, constants::{DEBUG_THREADS, EXTENSION_VERSION}, core::{file_mgr::FileMgr, odoo::SyncOdoo}, crash_buffer, threads::{DelayedProcessingMessage, delayed_changes_process_thread, message_processor_thread_main}, tool_api::tool_api::CAN_TOOL_API_RUN}; /** @@ -26,10 +26,12 @@ pub struct Server { req_sender_s_to_main: Sender, //channel server to main threads. Will handle new request message (client -> s -> main and back) delayed_process_thread: JoinHandle<()>, sender_to_delayed_process: Sender, //unique channel to delayed process thread - sync_odoo: Arc>, + pub sync_odoo: Arc>, interrupt_rebuild_boolean: Arc, //ref to the one on sync_odoo terminate_rebuild_boolean: Arc, //ref to the one on sync_odoo running_request_ids: Arc>>, //ref to the one on sync_odoo, but with dedicated mutex + + pub spy_thread: Option>, } #[derive(Debug)] @@ -112,6 +114,7 @@ impl Server { interrupt_rebuild_boolean: interrupt_rebuild_boolean, terminate_rebuild_boolean, running_request_ids: running_request_ids, + spy_thread: None, } } @@ -253,6 +256,10 @@ impl Server { }); self.req_sender_s_to_main.send(shutdown_notification.clone()).unwrap(); self.res_sender_s_to_main.send(shutdown_notification.clone()).unwrap(); + CAN_TOOL_API_RUN.store(false, Ordering::SeqCst); + if let Some(spy_thread) = self.spy_thread.take() { + spy_thread.join().unwrap(); + } info!(message); } diff --git a/server/src/tool_api/io_threads.rs b/server/src/tool_api/io_threads.rs new file mode 100644 index 00000000..5b920dc3 --- /dev/null +++ b/server/src/tool_api/io_threads.rs @@ -0,0 +1,25 @@ +use std::{io, thread}; + +pub struct ToolAPIIoThreads { + pub reader: thread::JoinHandle>, + pub writer: thread::JoinHandle>, +} + +impl ToolAPIIoThreads { + pub fn join(self) -> io::Result<()> { + match self.reader.join() { + Ok(r) => r?, + Err(err) => { + println!("reader panicked!"); + std::panic::panic_any(err) + } + } + match self.writer.join() { + Ok(r) => r, + Err(err) => { + println!("writer panicked!"); + std::panic::panic_any(err); + } + } + } +} \ No newline at end of file diff --git a/server/src/tool_api/mod.rs b/server/src/tool_api/mod.rs new file mode 100644 index 00000000..ce0fe2d1 --- /dev/null +++ b/server/src/tool_api/mod.rs @@ -0,0 +1,5 @@ +pub mod io_threads; +pub mod server; +pub mod socket; +pub mod to_json; +pub mod tool_api; \ No newline at end of file diff --git a/server/src/tool_api/server.rs b/server/src/tool_api/server.rs new file mode 100644 index 00000000..9b5e720d --- /dev/null +++ b/server/src/tool_api/server.rs @@ -0,0 +1,23 @@ +use std::net::TcpListener; +use std::sync::{Arc, Mutex}; + +use tracing::{error, info}; + +use crate::core::odoo::SyncOdoo; +use crate::server::Server; +use crate::tool_api::tool_api::ToolAPI; + +impl Server { + + pub fn create_spy_connection(&mut self, sync_odoo: Arc>) { + info!("ToolAPI: Creating spy connection"); + let listener = TcpListener::bind("127.0.0.1:8072"); + let Ok(listener) = listener else { + error!("ToolAPI: Unable to bind to 127.0.0.1:8072. Spy connection will be not available - {}", listener.unwrap_err()); + return; + }; + self.spy_thread = Some(std::thread::spawn(move || { + ToolAPI::listen_to_spy(listener, sync_odoo); + })) + } +} \ No newline at end of file diff --git a/server/src/tool_api/socket.rs b/server/src/tool_api/socket.rs new file mode 100644 index 00000000..774a8ae8 --- /dev/null +++ b/server/src/tool_api/socket.rs @@ -0,0 +1,52 @@ +//from lsp_server crate +use std::{ + io::{self, BufReader}, + net::TcpStream, + thread, +}; + +use crossbeam_channel::{bounded, Receiver, Sender}; + +use lsp_server::{Message}; + +use super::io_threads::ToolAPIIoThreads; + +pub(crate) fn socket_transport( + stream: TcpStream, +) -> (Sender, Receiver, ToolAPIIoThreads) { + let (reader_receiver, reader) = make_reader(stream.try_clone().unwrap()); + let (writer_sender, writer) = make_write(stream); + let io_threads = ToolAPIIoThreads{ reader, writer}; + (writer_sender, reader_receiver, io_threads) +} + +fn make_reader(stream: TcpStream) -> (Receiver, thread::JoinHandle>) { + let (reader_sender, reader_receiver) = bounded::(0); + let reader = thread::spawn(move || { + let mut buf_read = BufReader::new(stream); + while let Some(msg) = match Message::read(&mut buf_read) { + Ok(msg) => msg, + Err(e) => { + eprintln!("Error reading message: {}", e); + None + } + } { + let is_exit = matches!(&msg, Message::Notification(n) if n.method == "exit"); + reader_sender.send(msg).unwrap(); + if is_exit { + break; + } + } + Ok(()) + }); + (reader_receiver, reader) +} + +fn make_write(mut stream: TcpStream) -> (Sender, thread::JoinHandle>) { + let (writer_sender, writer_receiver) = bounded::(0); + let writer = thread::spawn(move || { + writer_receiver.into_iter().try_for_each(|it| it.write(&mut stream)).unwrap(); + Ok(()) + }); + (writer_sender, writer) +} diff --git a/server/src/tool_api/to_json.rs b/server/src/tool_api/to_json.rs new file mode 100644 index 00000000..61455a5f --- /dev/null +++ b/server/src/tool_api/to_json.rs @@ -0,0 +1,46 @@ +use std::{cell::RefCell, rc::Weak}; + +use serde_json::json; +use weak_table::PtrWeakHashSet; + +use crate::core::symbols::symbol::Symbol; + + +pub fn dependencies_to_json(dependencies: &Vec>>>>>) -> serde_json::Value { + if dependencies.is_empty() { + return json!(null); + } + json!({ + "arch": json!({ + "arch": json!(dependencies[0][0].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + }), + "arch_eval": json!({ + "arch": json!(dependencies[1][0].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + "arch_eval": json!(dependencies[1][1].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + }), + "validation": json!({ + "arch": json!(dependencies[2][0].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + "arch_eval": json!(dependencies[2][1].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + "validation": json!(dependencies[2][2].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + }), + }) +} +pub fn dependents_to_json(dependents: &Vec>>>>>) -> serde_json::Value { + if dependents.is_empty() { + return json!(null); + } + json!({ + "arch": json!({ + "arch": json!(dependents[0][0].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + "arch_eval": json!(dependents[0][1].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + "validation": json!(dependents[0][2].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + }), + "arch_eval": json!({ + "arch_eval": json!(dependents[1][0].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + "validation": json!(dependents[1][1].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + }), + "validation": json!({ + "validation": json!(dependents[2][0].as_ref().map(|set| set.iter().map(|x| x.borrow().paths()).collect::()).unwrap_or(json!(null))), + }), + }) +} \ No newline at end of file diff --git a/server/src/tool_api/tool_api.rs b/server/src/tool_api/tool_api.rs new file mode 100644 index 00000000..9edf8d50 --- /dev/null +++ b/server/src/tool_api/tool_api.rs @@ -0,0 +1,296 @@ +use std::cell::RefCell; +use std::net::TcpListener; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::io::{prelude::*, ErrorKind}; +use std::time::Duration; + +use byteyarn::Yarn; +use lsp_server::{Connection, Message, Request, Response}; +use serde::{Deserialize, Serialize}; +use tracing::{error, info}; +use serde_json::json; + +use crate::constants::{OYarn, PackageType, SymType, Tree}; +use crate::core::entry_point::{EntryPoint, EntryPointType}; +use crate::core::odoo::SyncOdoo; +use crate::core::symbols::symbol::Symbol; +use crate::utils::{tree_yarn_to_string, PathSanitizer}; +use crate::{Sy, oyarn}; + +use super::io_threads::ToolAPIIoThreads; +use super::socket; + +pub static CAN_TOOL_API_RUN: AtomicBool = AtomicBool::new(true); + +pub struct ToolAPI { + +} + +impl ToolAPI { + pub fn listen_to_spy(listener: TcpListener, sync_odoo: Arc>) { + let mut threads = vec![]; + while CAN_TOOL_API_RUN.load(Ordering::SeqCst) { + match listener.accept() { + Ok((stream, _addr)) => { + let (sender, receiver, io_threads) = socket::socket_transport(stream); + let sync_odoo = sync_odoo.clone(); + threads.push(std::thread::spawn(move || Self::handle_connection(Connection { sender, receiver }, io_threads, sync_odoo))) + } + Err(ref e) if e.kind() == ErrorKind::WouldBlock => { + // No connection yet, so just sleep for a bit to prevent busy waiting + thread::sleep(Duration::from_millis(2000)); + } + Err(e) => { + eprintln!("Connection failed: {}", e); + break; + } + } + } + for thread in threads { + thread.join().unwrap(); + } + } + + fn handle_connection(connection: Connection, io_threads: ToolAPIIoThreads, sync_odoo: Arc>) { + info!("ToolAPI: Connection established"); + while CAN_TOOL_API_RUN.load(Ordering::SeqCst) { + let msg = connection.receiver.try_recv(); + if let Err(ref e) = msg { + match e { + crossbeam_channel::TryRecvError::Empty => { + thread::sleep(Duration::from_millis(80)); + continue; + }, + crossbeam_channel::TryRecvError::Disconnected => { + info!("ToolAPI: Connection closed"); + break; + } + } + } + ToolAPI::handle_msg(&connection, msg.unwrap(), &sync_odoo); + } + io_threads.join(); + } + + fn handle_msg(connection: &Connection, msg: Message, sync_odoo: &Arc>) { + match msg { + Message::Request(req) => { + let response = ToolAPI::handle_request(&sync_odoo, req); + if let Err(e) = connection.sender.send(Message::Response(response)) { + error!("ToolAPI: Error sending response: {}", e); + } + }, + Message::Notification(not) => { + ToolAPI::handle_notification(&sync_odoo, not); + }, + Message::Response(_) => { + error!("ToolAPI: Unexpected response"); + } + } + } + + fn handle_request(sync_odoo: &Arc>, request: Request) -> Response { + let response_value; + match request.method.as_str() { + "$/ToolAPI/list_entries" => { + let sync_odoo = sync_odoo.lock().unwrap(); + response_value = ToolAPI::list_entries(&sync_odoo); + }, + "$/ToolAPI/get_symbol" => { + let sync_odoo = sync_odoo.lock().unwrap(); + response_value = ToolAPI::get_symbol(&sync_odoo, serde_json::from_value(request.params).unwrap()); + }, + "$/ToolAPI/get_symbol_with_path" => { + let sync_odoo = sync_odoo.lock().unwrap(); + response_value = ToolAPI::get_symbol_with_path(&sync_odoo, serde_json::from_value(request.params).unwrap()); + }, + "$/ToolAPI/browse_tree" => { + let sync_odoo = sync_odoo.lock().unwrap(); + response_value = ToolAPI::browse_tree(&sync_odoo, serde_json::from_value(request.params).unwrap()); + }, + _ => { + error!("ToolAPI: Unknown request method: {}", request.method); + return Response::new_err(request.id.clone(), lsp_server::ErrorCode::MethodNotFound as i32, format!("Method not found {}", request.method)); + } + } + Response::new_ok(request.id.clone(), response_value) + } + + fn handle_notification(sync_odoo: &Arc>, _notification: lsp_server::Notification) { + let sync_odoo = sync_odoo.lock().unwrap(); + // Do nothing + } + + fn entry_to_json(entry: &EntryPoint) -> serde_json::Value { + let mut not_found = vec![]; + for symbol in entry.not_found_symbols.iter() { + not_found.push(json!(tree_yarn_to_string(&symbol.borrow().get_tree()))); + } + json!({ + "path": entry.path, + "tree": entry.tree.iter().map(|x| x.to_string()).collect::>(), + "type": match entry.typ.clone() { + EntryPointType::MAIN => "main", + EntryPointType::ADDON => "addon", + EntryPointType::BUILTIN => "builtin", + EntryPointType::PUBLIC => "public", + EntryPointType::CUSTOM => "custom", + _ => {"unknown"} + }, + "addon_to_odoo_path": entry.addon_to_odoo_path, + "addon_to_odoo_tree": entry.addon_to_odoo_tree.as_ref().map(|x| x.iter().map(|x| x.to_string()).collect::>()), + "not_found_symbols": not_found, + }) + } + + fn list_entries(sync_odoo: &SyncOdoo) -> serde_json::Value { + let mut entries = vec![]; + for entry in sync_odoo.entry_point_mgr.borrow().iter_all() { + entries.push(ToolAPI::entry_to_json(&entry.borrow())); + } + serde_json::Value::Array(entries) + } + + fn get_symbol(sync_odoo: &SyncOdoo, params: GetSymbolParams) -> serde_json::Value { + let mut entry = None; + let ep_mgr = sync_odoo.entry_point_mgr.borrow(); + for e in ep_mgr.iter_all() { + if e.borrow().path == params.entry_path { + entry = Some(e); + break; + } + } + if let Some(entry) = entry { + return ToolAPI::symbol_to_json(entry.clone(), &ToolAPI::vec_tree_to_yarn_tree(¶ms.tree)) + } + serde_json::Value::Null + } + + pub fn vec_tree_to_yarn_tree(str_tree: &(Vec, Vec)) -> Tree { + (str_tree.0.iter().map(|x| oyarn!("{}", x.clone())).collect::>(), str_tree.1.iter().map(|x| oyarn!("{}", x.clone())).collect::>()) + } + + fn symbol_to_json(entry: Rc>, tree: &Tree) -> serde_json::Value { + let mut symbols = entry.borrow().root.borrow().get_symbol(tree, u32::MAX); + if symbols.len() > 1 { + panic!() + } + if tree.0.is_empty() && tree.1.is_empty() { + symbols.push(entry.borrow().root.clone()); + } + let Some(symbol) = symbols.first() else {return serde_json::Value::Null}; + let typ = symbol.borrow().typ(); + match typ { + SymType::ROOT => { + return symbol.borrow().as_root().to_json(); + } + SymType::DISK_DIR => { + return symbol.borrow().as_disk_dir_sym().to_json(); + } + SymType::NAMESPACE => { + return symbol.borrow().as_namespace().to_json(); + }, + SymType::PACKAGE(PackageType::MODULE) => { + return symbol.borrow().as_module_package().to_json(); + }, + SymType::PACKAGE(PackageType::PYTHON_PACKAGE) => { + return symbol.borrow().as_python_package().to_json(); + }, + SymType::FILE => { + return symbol.borrow().as_file().to_json(); + }, + SymType::COMPILED => {return symbol.borrow().as_compiled().to_json();}, + SymType::VARIABLE => { + return symbol.borrow().as_variable().to_json(); + }, + SymType::CLASS => { + return symbol.borrow().as_class_sym().to_json(); + }, + SymType::FUNCTION => { + return symbol.borrow().as_func().to_json(); + }, + SymType::XML_FILE => { + return symbol.borrow().as_xml_file_sym().to_json(); + }, + SymType::CSV_FILE => { + return symbol.borrow().as_csv_file_sym().to_json(); + } + } + } + + fn get_symbol_with_path(sync_odoo: &SyncOdoo, params: GetSymbolWithPathParams) -> serde_json::Value { + let mut entry = None; + let ep_mgr = sync_odoo.entry_point_mgr.borrow(); + for e in ep_mgr.iter_all() { + if e.borrow().path == params.entry_path { + entry = Some(e); + break; + } + } + let path = PathBuf::from(params.path).to_tree(); + if let Some(entry) = entry { + return ToolAPI::symbol_to_json(entry.clone(), &path) + } + serde_json::Value::Null + } + + fn browse_tree(sync_odoo: &SyncOdoo, params: BrowseTreeParams) -> serde_json::Value { + let mut entry = None; + let ep_mgr = sync_odoo.entry_point_mgr.borrow(); + for e in ep_mgr.iter_all() { + if e.borrow().path == params.entry_path { + entry = Some(e); + break; + } + } + if let Some(entry) = entry { + let mut symbols = entry.borrow().root.borrow().get_symbol(&ToolAPI::vec_tree_to_yarn_tree(¶ms.tree), u32::MAX); + if symbols.len() > 1 { + panic!() + } + if params.tree.0.is_empty() && params.tree.1.is_empty() { + symbols.push(entry.borrow().root.clone()); + } + if let Some(symbol) = symbols.first() { + let has_modules = symbol.borrow().has_modules(); + let module_sym: Vec>> = if has_modules { + symbol.borrow().all_module_symbol().map(|x| x.clone()).collect() + } else { + vec![] + }; + let module_sym: Vec = module_sym.iter().map(|sym| { + json!({ + "name": sym.borrow().name().to_string(), + "type": sym.borrow().typ().to_string(), + }) + }).collect(); + return json!({ + "modules": module_sym, + }); + } + } + serde_json::Value::Null + } +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct BrowseTreeParams { + pub entry_path: String, + pub tree: (Vec, Vec), +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct GetSymbolParams { + pub entry_path: String, + pub tree: (Vec, Vec), +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub struct GetSymbolWithPathParams { + pub entry_path: String, + pub path: String, +} diff --git a/server/src/utils.rs b/server/src/utils.rs index f63f61cd..7f2172b9 100644 --- a/server/src/utils.rs +++ b/server/src/utils.rs @@ -132,6 +132,13 @@ pub fn compare_semver(a: &str, b: &str) -> std::cmp::Ordering { va.cmp(&vb) } +pub fn tree_yarn_to_string(tree: &Tree) -> (Vec, Vec) { + ( + tree.0.iter().map(|x| x.to_string()).collect(), + tree.1.iter().map(|x| x.to_string()).collect(), + ) +} + pub trait ToFilePath { fn to_file_path(&self) -> Result; } diff --git a/tools/.vscode/launch.json b/tools/.vscode/launch.json new file mode 100644 index 00000000..13329e0a --- /dev/null +++ b/tools/.vscode/launch.json @@ -0,0 +1,15 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +{ + "version": "1.0.0", + "configurations": [ + { + "name": "OdooLS Spy", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/odoolsspy/odoo-ls-spy.py", + "args": [], + "cwd": "${workspaceFolder}/odoolsspy", + "console": "integratedTerminal", + }, + ] +} diff --git a/tools/odoolsspy/FontAwesome.otf b/tools/odoolsspy/FontAwesome.otf new file mode 100644 index 00000000..17a83506 Binary files /dev/null and b/tools/odoolsspy/FontAwesome.otf differ diff --git a/tools/odoolsspy/OpenFontIcons.ttf b/tools/odoolsspy/OpenFontIcons.ttf new file mode 100644 index 00000000..804fbc1c Binary files /dev/null and b/tools/odoolsspy/OpenFontIcons.ttf differ diff --git a/tools/odoolsspy/__init__.py b/tools/odoolsspy/__init__.py new file mode 100644 index 00000000..5b3cb012 --- /dev/null +++ b/tools/odoolsspy/__init__.py @@ -0,0 +1 @@ +from . import views \ No newline at end of file diff --git a/tools/odoolsspy/arial.ttf b/tools/odoolsspy/arial.ttf new file mode 100644 index 00000000..810df57c Binary files /dev/null and b/tools/odoolsspy/arial.ttf differ diff --git a/tools/odoolsspy/arialbd.ttf b/tools/odoolsspy/arialbd.ttf new file mode 100644 index 00000000..22040fc7 Binary files /dev/null and b/tools/odoolsspy/arialbd.ttf differ diff --git a/tools/odoolsspy/connection/__init__.py b/tools/odoolsspy/connection/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/odoolsspy/connection/connection.py b/tools/odoolsspy/connection/connection.py new file mode 100644 index 00000000..467eae58 --- /dev/null +++ b/tools/odoolsspy/connection/connection.py @@ -0,0 +1,195 @@ +import json +import socket +import threading +import time +import dearpygui.dearpygui as dpg +from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QProgressBar +from PyQt6.QtCore import Qt, QThread, pyqtSignal +from threading import Event + +class ConnectionThread(QThread): + connection_successful = pyqtSignal() + + def __init__(self, connection_mgr): + super().__init__() + self.connection_mgr = connection_mgr + + def run(self): + self.connection_mgr._try_connect(self) + self.connection_successful.emit() + +class ListenToMessagesThread(QThread): + + def __init__(self, connection_mgr): + super().__init__() + self.connection_mgr = connection_mgr + + def run(self): + self.connection_mgr.listen_to_messages() + +class ConnectionPopup(QWidget): + def __init__(self, app, connection_mgr): + super().__init__() + self.app = app + self.connection_mgr = connection_mgr + self.init_ui() + self.start_connection() + + def init_ui(self): + self.setWindowTitle("Connection") + self.setFixedSize(400, 150) + self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint) + self.setStyleSheet(""" + QWidget#popup { + background-color: #666666; + } + """) + self.setObjectName("popup") + + layout = QVBoxLayout() + self.label = QLabel("Waiting to connect to a running Odoo LS with --spy parameter") + self.label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.progress = QProgressBar() + self.progress.setRange(0, 0) + self.progress.setAlignment(Qt.AlignmentFlag.AlignCenter) + + layout.addWidget(self.label) + layout.addWidget(self.progress) + self.setLayout(layout) + + def start_connection(self): + self.connection_mgr.connection_thread = ConnectionThread(self.connection_mgr) + self.connection_mgr.connection_thread.connection_successful.connect(self.close) + self.connection_mgr.connection_thread.start() + + def close(self): + super().close() + self.app.entry_tab.setup_tab(self.app, self.app.connection_mgr) + + +class ConnectionManager: + def __init__(self, app): + self.app = app + self.connection = None + self.answers = {} + self.answer_events = {} + self.request_id = 1 + self.listening_thread = None + self.connection_thread = None + self.popup = None + + def get_next_id(self): + self.request_id += 1 + return self.request_id + + def _try_connect(self, thread): + while not thread.isInterruptionRequested(): + try: + self.connection = socket.create_connection(("localhost", 8072), timeout=1) + print("Connected to localhost:8072") + break + except Exception as e: + print(f"Failed to connect: {e}. Retrying in 1 second...") + time.sleep(1) + if self.connection: + self.listening_thread = ListenToMessagesThread(self) + self.listening_thread.start() + + def connect(self): + self.popup = ConnectionPopup(self.app, self) + self.popup.show() + + def read_lsp_message(self): + buffer = b"" + + # Lire les en-têtes jusqu'à trouver une ligne vide (fin des en-têtes) + while b"\r\n\r\n" not in buffer: + chunk = self.connection.recv(4096) + if not chunk: + raise ConnectionError("Distant connection closed.") + buffer += chunk + + # Séparer les en-têtes et le début du message + header, _, remaining = buffer.partition(b"\r\n\r\n") + headers = header.decode("utf-8").split("\r\n") + + # Extraire Content-Length + content_length = None + for line in headers: + if line.lower().startswith("content-length:"): + try: + content_length = int(line.split(":")[1].strip()) + except ValueError: + raise ValueError(f"Invalid header : {line}") + + if content_length is None: + raise ValueError("LSP message missing Content-Length header") + + # Lire le contenu du message + message = remaining + while len(message) < content_length: + chunk = self.connection.recv(4096) + if not chunk: + raise ConnectionError("Distant connection closed while reading.") + message += chunk + + return message.decode("utf-8") + + def listen_to_messages(self): + while self.connection and not self.listening_thread.isInterruptionRequested(): + try: + message = self.read_lsp_message() + try: + message_data = json.loads(message) + except json.JSONDecodeError: + print(f"JSON decoding error: {message}") + continue + + if "method" in message_data: + # Handle requests and notifications + if message_data["method"] == "$/ToolAPI/is_busy": + self.handle_is_busy(message_data) + + elif "id" in message_data: + print("got answer to request " + str(message_data["id"])) + self.answers[message_data["id"]] = message_data + self.answer_events[message_data["id"]].set() + + except (socket.timeout, TimeoutError): + continue # Avoid timeout errors and keep listening + except ConnectionError as e: + print(f"Remote connection closed: {e}") + break + except Exception as e: + print(f"Unexpected error while reading message: {e}") + break + + def get_response(self, id): + event = self.answer_events.get(id) + if event is not None: + event.wait() + return self.answers.get(id) + + def send_message(self, method, params, is_request=True): + if self.connection is None: + raise ConnectionError("No connection established") + message = { + "jsonrpc": "2.0", + "method": method, + "params": params + } + if is_request: + message["id"] = self.get_next_id() + self.answer_events[message["id"]] = Event() + message_json = json.dumps(message) + print(message_json) + lsp_message = f"Content-Length: {len(message_json)}\r\n\r\n{message_json}" + self.connection.send(lsp_message.encode('utf-8')) + if is_request: + return message["id"] + + def send_exit_notification(self): + if self.connection is not None: + print("sending exit notification") + self.send_message("exit", {}, False) \ No newline at end of file diff --git a/tools/odoolsspy/odoo-ls-spy.py b/tools/odoolsspy/odoo-ls-spy.py new file mode 100644 index 00000000..f930ab08 --- /dev/null +++ b/tools/odoolsspy/odoo-ls-spy.py @@ -0,0 +1,117 @@ +import sys +import threading +import time +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QMenuBar, QVBoxLayout, + QWidget, QTableWidget, QTableWidgetItem, QTabWidget, QLabel, QPushButton, QSplitter, QStackedWidget, QSizePolicy, + QGridLayout +) +from PyQt6.QtGui import QAction +from PyQt6.QtCore import Qt, QThread, pyqtSignal +from connection.connection import ConnectionManager +from views.list_entries import EntryTab +from views.monitoring import Monitoring + +class OdooLSSpyApp(QMainWindow): + def __init__(self): + super().__init__() + + self.connection_mgr = ConnectionManager(self) + #self.monitoring = Monitoring() + self.entry_tab = EntryTab(self) + self.tree_browsers = [] + + self.setWindowTitle("Odoo LS Spy") + self.setGeometry(100, 100, 1920, 1080) + self.init_ui() + self.right_tab_map = {} + + self.connection_mgr.connect() + + def init_ui(self): + self.central_widget = QWidget(self) + self.setCentralWidget(self.central_widget) + + # Création du splitter principal + self.splitter = QSplitter(Qt.Orientation.Horizontal) + self.splitter.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + self.layout = QVBoxLayout(self.central_widget) + self.layout.addWidget(self.splitter) + + # Menu Bar + menu_bar = self.menuBar() + file_menu = menu_bar.addMenu("File") + + open_action = QAction("Open EntryPoints list", self) + open_action.triggered.connect(self.open_entry_points_list) + file_menu.addAction(open_action) + + close_action = QAction("Close", self) + close_action.triggered.connect(self.close) + file_menu.addAction(close_action) + + # Settings Menu + settings_menu = menu_bar.addMenu("Settings") + toggle_cpu_action = QAction("Toggle CPU usage", self, checkable=True) + #toggle_cpu_action.triggered.connect(self.monitoring.toggle_cpu_usage) + settings_menu.addAction(toggle_cpu_action) + + # Left tab bar + self.left_tab_bar = QTabWidget() + self.left_tab_bar.setTabsClosable(True) + self.left_tab_bar.tabCloseRequested.connect(self.close_left_tab) + self.splitter.addWidget(self.left_tab_bar) + + + self.right_tab_bar = QTabWidget() + self.right_tab_bar.setTabsClosable(True) + self.right_tab_bar.tabCloseRequested.connect(self.close_right_tab) + self.splitter.addWidget(self.right_tab_bar) + + # Start monitoring + #self.monitoring.start() + + def close_left_tab(self, index): + self.left_tab_bar.removeTab(index) + + def close_right_tab(self, index): + self.right_tab_bar.removeTab(index) + + def replace_right_tab(self, tab_name, widget, tab_hash): + if self.right_tab_map.get(tab_hash): + index = self.right_tab_bar.indexOf(self.right_tab_map.pop(tab_hash)) + self.right_tab_bar.removeTab(index) + self.right_tab_map[tab_hash] = widget + self.right_tab_bar.addTab(widget, tab_name) + self.right_tab_bar.setCurrentWidget(widget) + + def on_connection_established(self): + print("Connection established!") + + def open_entry_points_list(self): + if self.connection_mgr.connection is None: + return + self.entry_tab.setup_tab(self, self.connection_mgr) + + def closeEvent(self, event): + if self.connection_mgr.connection_thread and self.connection_mgr.connection_thread.isRunning(): + self.connection_mgr.connection_thread.requestInterruption() + self.connection_mgr.connection_thread.wait() + if self.connection_mgr.listening_thread and self.connection_mgr.listening_thread.isRunning(): + self.connection_mgr.listening_thread.requestInterruption() + self.connection_mgr.listening_thread.wait() + self.connection_mgr.send_exit_notification() + # self.monitoring.close_window() + event.accept() + + +def main(): + app = QApplication(sys.argv) + window = OdooLSSpyApp() + window.showMaximized() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/odoolsspy/views/__init__.py b/tools/odoolsspy/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/odoolsspy/views/list_entries.py b/tools/odoolsspy/views/list_entries.py new file mode 100644 index 00000000..c6a3f38d --- /dev/null +++ b/tools/odoolsspy/views/list_entries.py @@ -0,0 +1,179 @@ +import json +import sys +from PyQt6.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QToolButton, QFrame, QLabel, QScrollArea, QHBoxLayout, QSpacerItem, QSizePolicy, QPushButton +) +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QFont + +class EntryTab(): + + def __init__(self, app): + self.app = app + self.entries = [] + self.tab_ids = 0 + self.is_theme_setup = False + + def find_tab_index(self, tab_name: str) -> int: + for i in range(self.app.left_tab_bar.count()): + if self.app.left_tab_bar.tabText(i) == tab_name: + return i + return -1 + + def setup_tab(self, app, connection_mgr): + id = connection_mgr.send_message("$/ToolAPI/list_entries", {}) + existing_index = self.find_tab_index("Entry Points") + + scroll_area = QScrollArea() + new_widget = QWidget() + scroll_area.setWidget(new_widget) + scroll_area.setWidgetResizable(True) + layout = QVBoxLayout() + + if existing_index != -1: + # Remplace l'onglet existant + self.app.left_tab_bar.removeTab(existing_index) + self.app.left_tab_bar.insertTab(existing_index, scroll_area, "Entry Points") + else: + # Ajoute un nouvel onglet + self.app.left_tab_bar.addTab(scroll_area, "Entry Points") + + response = connection_mgr.get_response(id) + if "result" in response: + entries = response["result"] + for entry in entries: + self.entries.append(CollapsibleSection(app, entry)) + layout.addWidget(self.entries[-1]) + self.tab_ids += 1 + else: + layout.addWidget(QLabel("Unable to get valid answer from Odoo LS")) + + layout.addItem(QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)) + new_widget.setLayout(layout) + + +class CollapsibleSection(QWidget): + def __init__(self, app, entry): + super().__init__() + self.app = app + self.symbol = None + self.layout = QVBoxLayout(self) + + self.entry = entry + entry_path_split = entry["path"].split("/") + entry_name = entry["type"] + ": " + (".../" if len(entry_path_split) > 4 else "") + "/".join(entry_path_split[-4:]) + # Bouton pour ouvrir/fermer la section + self.toggle_button = QToolButton(text=entry_name) + self.toggle_button.setCheckable(True) + self.toggle_button.setChecked(False) + self.toggle_button.setStyleSheet("font-weight: bold;") + self.toggle_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + self.toggle_button.setArrowType(Qt.ArrowType.RightArrow) + if entry["type"] == "main": + self.toggle_button.setStyleSheet("background-color: rgb(41, 107, 31)") + elif entry["type"] == "addon": + self.toggle_button.setStyleSheet("background-color: rgb(32, 110, 63)") + elif entry["type"] == "public": + self.toggle_button.setStyleSheet("background-color: rgb(112, 85, 31)") + elif entry["type"] == "builtin": + self.toggle_button.setStyleSheet("background-color: rgb(32, 51, 115)") + elif entry["type"] == "custom": + self.toggle_button.setStyleSheet("background-color: rgb(33, 99, 117)") + + self.toggle_button.clicked.connect(self.toggle_section) + + # Conteneur des widgets + self.content_area = QWidget() + self.content_layout = QVBoxLayout(self.content_area) + + self.content_area.setVisible(False) # Masqué au départµ + + # Ajout des widgets à la section + self.layout.addWidget(self.toggle_button) + self.layout.addWidget(self.content_area) + self.layout.setContentsMargins(0, 0, 0, 0) + + self.add_sub_widgets() + + def add_sub_widgets(self): + font = QFont() + font.setBold(True) + self.entry_path = self.entry["path"] + if self.entry.get("path"): + group = QHBoxLayout() + path = QLabel("Path: ") + path.setFont(font) + group.addWidget(path, 0) + group.addWidget(QLabel(self.entry["path"]), 1) + self.content_layout.addLayout(group) + + if self.entry.get("tree"): + symbol_group = QHBoxLayout() + tree = QLabel("Tree: ") + tree.setFont(font) + symbol_group.addWidget(tree, 0) + symbol_group.addWidget(QLabel(str(self.entry["tree"])), 1) + browse_button = QPushButton("Browse") + browse_button.clicked.connect(lambda _: self.browse_tree(self.app, self.entry["path"], self.entry["tree"])) + symbol_group.addWidget(browse_button) + spacer = QSpacerItem(5, 5, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + symbol_group.addItem(spacer) + self.content_layout.addLayout(symbol_group) + + if self.entry.get("addon_to_odoo_path"): + group = QHBoxLayout() + aop = QLabel("Addon to Odoo Path: ") + aop.setFont(font) + group.addWidget(aop, 0) + group.addWidget(QLabel(self.entry["addon_to_odoo_path"]), 1) + self.content_layout.addLayout(group) + + if self.entry.get("addon_to_odoo_tree"): + group = QHBoxLayout() + aot = QLabel("Addon to Odoo Tree: ") + aot.setFont(font) + group.addWidget(aot, 0) + group.addWidget(QLabel(str(self.entry["addon_to_odoo_tree"])), 1) + self.content_layout.addLayout(group) + + if len(self.entry["not_found_symbols"]) > 0: + group = QVBoxLayout() + nfs = QLabel("Not found symbols: ") + nfs.setFont(font) + group.addWidget(nfs) + + for symbol in self.entry["not_found_symbols"]: + symbol_group = QHBoxLayout() + symbol_group.addWidget(QLabel(str(symbol)), 0) + go_to_button = QPushButton("Go to") + go_to_button.clicked.connect(lambda _, s=symbol: self.go_to_symbol(s)) + symbol_group.addWidget(go_to_button) + spacer = QSpacerItem(5, 5, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + symbol_group.addItem(spacer) + group.addLayout(symbol_group) + + self.content_layout.addLayout(group) + + def toggle_section(self): + """Open and close the section""" + is_open = self.toggle_button.isChecked() + self.content_area.setVisible(is_open) + self.toggle_button.setArrowType(Qt.ArrowType.DownArrow if is_open else Qt.ArrowType.RightArrow) + + def go_to_symbol(self, symbol): + from views.symbols import Symbol + sym_data = Symbol.prepare_symbol(self.app, symbol[1][-1] if symbol[1] else symbol[0][-1], self.entry_path, symbol) + if sym_data: + self.symbol = Symbol(sym_data) + self.app.replace_right_tab(self.symbol.name, self.symbol, "symbol_tree_" + str(symbol)) + + def browse_tree(self, app, path, tree): + from views.tree_browser import TreeBrowser + for i in range(app.left_tab_bar.count()): + widget = app.left_tab_bar.widget(i) + if isinstance(widget, TreeBrowser) and widget.path == path and widget.tree == tree: + app.left_tab_bar.setCurrentIndex(i) + return + tree_browser = TreeBrowser(app, path, [tree, []]) + tree_browser.setup_ui(app) + app.left_tab_bar.addTab(tree_browser, tree[-1]) \ No newline at end of file diff --git a/tools/odoolsspy/views/monitoring.py b/tools/odoolsspy/views/monitoring.py new file mode 100644 index 00000000..a5cf392a --- /dev/null +++ b/tools/odoolsspy/views/monitoring.py @@ -0,0 +1,83 @@ +import psutil +import dearpygui.dearpygui as dpg +import threading +import time +from connection import connection + +class Monitoring: + def __init__(self): + self.running = True + self.window_height_per_cpu = 75 + self.theme_color_id = None + self.thread = None + + def get_cpu_usage_by_name(self, name): + processes = {} + for process in psutil.process_iter(attrs=['pid', 'name']): + try: + if name.lower() in process.info['name'].lower(): + proc = psutil.Process(process.info['pid']) + cpu_usage = proc.cpu_percent(interval=1.0) + processes[proc.pid] = cpu_usage + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + pass + return processes + + def update_cpu_usage(self): + while self.running: + cpu_usages = self.get_cpu_usage_by_name("odoo_ls_server") + + for pid, cpu in cpu_usages.items(): + if not dpg.does_item_exist(f"progress_{pid}"): + with dpg.group(parent="cpu_window"): + dpg.add_progress_bar(default_value=0.0, tag=f"progress_{pid}", width=200, height=10) + text = dpg.add_text(f"PID {pid}:", tag=f"label_{pid}", pos=(10, 2)) + dpg.bind_item_font(text, "arial11") + dpg.set_item_height("cpu_window", self.window_height_per_cpu * len(cpu_usages)) + + dpg.set_value(f"progress_{pid}", cpu / 100.0) + dpg.configure_item(f"label_{pid}", default_value=f"PID {pid}: {cpu:.2f}% CPU") + + existing_pids = {int(tag.split("_")[1]) for tag in dpg.get_all_items() if dpg.get_item_label(tag).startswith("progress_")} + for pid in existing_pids - cpu_usages.keys(): + dpg.delete_item(f"label_{pid}") + dpg.delete_item(f"progress_{pid}") + + time.sleep(1) + + def close_window(self): + self.running = False + if self.thread: + self.thread.join() + dpg.delete_item("cpu_window") + + def start(self): + with dpg.window(label="CPU Usage", tag="cpu_window", width=200, height=self.window_height_per_cpu, no_title_bar=True, on_close=self.close_window, no_move=True, no_resize=True): + pass + with dpg.theme() as bg_window_theme: + with dpg.theme_component(dpg.mvWindowAppItem): + dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 5, 5, parent=dpg.last_item()) + self.theme_color_id = dpg.add_theme_color(dpg.mvThemeCol_WindowBg, (30, 30, 30, 255), category=dpg.mvThemeCat_Core) + + dpg.bind_item_theme("cpu_window", bg_window_theme) + self.thread = threading.Thread(target=self.update_cpu_usage, daemon=True) + self.thread.start() + self.update_window_position() + + def update_window_position(self): + viewport_width = dpg.get_viewport_width() + viewport_height = dpg.get_viewport_height() + window_height = 20 + + y_pos = viewport_height - window_height - 20 + + dpg.set_item_width("cpu_window", viewport_width) + dpg.set_item_pos("cpu_window", (0, y_pos)) + + def toggle_cpu_usage(self): + dpg.set_value("cpu_wdw_visibility", not dpg.get_value("cpu_wdw_visibility")) + cpu_window_visibility = dpg.get_value("cpu_wdw_visibility") + dpg.configure_item("cpu_window", show=cpu_window_visibility) + + def set_busy(self, is_busy): + dpg.set_value(self.theme_color_id, (210, 170, 0, 255) if is_busy else (30, 230, 30, 255)) \ No newline at end of file diff --git a/tools/odoolsspy/views/symbols.py b/tools/odoolsspy/views/symbols.py new file mode 100644 index 00000000..471e72e1 --- /dev/null +++ b/tools/odoolsspy/views/symbols.py @@ -0,0 +1,180 @@ +from collections import defaultdict +import json +import sys +from PyQt6.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QToolButton, QFrame, QLabel, QScrollArea, QHBoxLayout, + QSpacerItem, QSizePolicy, QPushButton, QGroupBox, QListWidget, QComboBox, QMessageBox +) +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QFont + +class Symbol(QWidget): + + def prepare_symbol(app, name, entry_path, tree=None, path=None): + data = {} + data["name"] = name + data["tree"] = tree + data["entry_path"] = entry_path + data["app"] = app + if data["tree"]: + id = data["app"].connection_mgr.send_message("$/ToolAPI/get_symbol", { + "entry_path": data["entry_path"], + "tree": data["tree"] + }) + data["response"] = data["app"].connection_mgr.get_response(id) + elif path: + id = data["app"].connection_mgr.send_message("$/ToolAPI/get_symbol_with_path", { + "entry_path": data["entry_path"], + "path": path + }) + data["response"] = data["app"].connection_mgr.get_response(id) + data["name"] = path.split("/")[-1] + if data["response"]["result"] is None: + QMessageBox.critical(None, "Error", "This symbol does not exist") + return None + return data + + def __init__(self, data): + super().__init__() + self.name = data["name"] + self.tree = data["tree"] + self.entry_path = data["entry_path"] + self.app = data["app"] + self.response = data["response"] + layout_scroll = QVBoxLayout(self) + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + layout_scroll.addWidget(self.scroll_area) + content_widget = QWidget() + self.scroll_area.setWidget(content_widget) + self.content_layout = QVBoxLayout(content_widget) + self.display() + + def display(self): + font = QFont() + font.setBold(True) + result = self.response["result"] + self.add_one_line_text(font, "Name: ", self.name) + if "type" in result: + self.add_one_line_text(font, "Type: ", result.pop("type")) + if "path" in result: + self.add_one_line_text(font, "Path: ", str(result.pop("path"))) + if "arch_status" in result: + self.add_arch_status(font, result) + if "in_workspace" in result: + self.add_one_line_text(font, "In Workspace: ", str(result.pop("in_workspace"))) + if "is_external" in result: + self.add_one_line_text(font, "Is External: ", str(result.pop("is_external"))) + if "self_import" in result: + self.add_one_line_text(font, "Self import: ", str(result.pop("self_import"))) + if "not_found_paths" in result: + self.add_not_found_paths(font, result) + if "processed_text_hash" in result: + self.add_one_line_text(font, "Processed Text Hash: ", str(result.pop("processed_text_hash"))) + if "sections" in result: + self.add_one_line_text(font, "Sections: ", str(result.pop("sections"))) + if "dependencies" in result: + self.add_dependencies(font, result) + for key, value in result.items(): + print("not printed key: " + key) + print("value: " + str(value)) + + spacer = QSpacerItem(5, 5, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + self.content_layout.addItem(spacer) + + def add_not_found_paths(self, bold_font, result): + groupBox = QGroupBox("Not found paths") + paths = result.pop("not_found_paths") + p = defaultdict(list) + for path in paths: + p[path["step"]].append(path["paths"]) + v_layout = QVBoxLayout() + groupBox.setLayout(v_layout) + for step, paths in p.items(): + in_groupBox = QGroupBox(step) + in_layout = QVBoxLayout() + v_layout.addWidget(in_groupBox) + in_groupBox.setLayout(in_layout) + listWidget = QListWidget() + for item in paths: + listWidget.addItem(str(item)) + in_layout.addWidget(listWidget) + if len(p) != 0: + self.content_layout.addWidget(groupBox) + + def add_arch_status(self, bold_font, result): + groupBox = QGroupBox("Arch Status") + v_layout = QVBoxLayout() + groupBox.setLayout(v_layout) + h1_layout = QHBoxLayout() + h2_layout = QHBoxLayout() + v_layout.addLayout(h1_layout) + v_layout.addLayout(h2_layout) + arch = QLabel("Architecture: ") + arch.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + h1_layout.addWidget(arch, 0) + h1_layout.addWidget(QLabel(result.pop("arch_status")), 0) + arch_eval = QLabel("Arch Evaluation: ") + arch_eval.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + h1_layout.addWidget(arch_eval, 0) + h1_layout.addWidget(QLabel(result.pop("arch_eval_status")), 0) + validation = QLabel("Validation: ") + validation.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + h2_layout.addWidget(validation, 0) + h2_layout.addWidget(QLabel(result.pop("validation_status")), 0) + self.content_layout.addWidget(groupBox) + + def add_one_line_text(self, bold_font, title, text): + group = QHBoxLayout() + aot = QLabel(title) + aot.setFont(bold_font) + group.addWidget(aot, 0) + group.addWidget(QLabel(text), 1) + self.content_layout.addLayout(group) + + def add_dependencies(self, bold_font, result): + dependencies_btn = QPushButton() + dependencies_btn.setText("Dependencies") + dependencies_btn.setMaximumSize(200, 30) + dependencies_btn.pressed.connect(lambda: self.group_dependencies.setVisible(not self.group_dependencies.isVisible())) + self.content_layout.addWidget(dependencies_btn) + + self.group_dependencies = QGroupBox("Dependencies") + main_layout = QVBoxLayout() + self.group_dependencies.setLayout(main_layout) + for key, value in result.pop("dependencies").items(): + found_one = False + groupBox = QGroupBox(key) + v_layout = QVBoxLayout() + groupBox.setLayout(v_layout) + for step, dep in value.items(): + if not dep: + continue + found_one = True + step_groupBox = QGroupBox(step) + step_layout = QVBoxLayout() + step_groupBox.setLayout(step_layout) + v_layout.addWidget(step_groupBox) + for d in dep: + group = QHBoxLayout() + goto = QPushButton("Go To") + goto.setFont(bold_font) + goto.pressed.connect(lambda: self.goto_symbol(d)) + group.addWidget(goto, 0) + group.addWidget(QLabel(str(d)), 1) + step_layout.addLayout(group) + if found_one: + main_layout.addWidget(groupBox) + self.content_layout.addWidget(self.group_dependencies) + + self.group_dependencies.setVisible(False) + + def goto_symbol(self, path): + if isinstance(path, list): + if len(path) == 0: + return + path = path[0] + data = Symbol.prepare_symbol(self.app, "", self.entry_path, None, path) + if data: + self.symbol = Symbol(data) + self.app.replace_right_tab(self.symbol.name, self.symbol, "symbol_tree_" + str(self.symbol.tree)) \ No newline at end of file diff --git a/tools/odoolsspy/views/themes.py b/tools/odoolsspy/views/themes.py new file mode 100644 index 00000000..0f062f32 --- /dev/null +++ b/tools/odoolsspy/views/themes.py @@ -0,0 +1,61 @@ +import dearpygui.dearpygui as dpg + +def setup_themes(): + with dpg.font_registry(): + dpg.add_font("arial.ttf", 11, tag="arial11") + dpg.add_font("arialbd.ttf", 14, tag="arialbd14") + dpg.add_font("arial.ttf", 14, tag="arial14") + with dpg.font("FontAwesome.otf", 12, tag="arialFA12"): + dpg.add_font_range_hint(dpg.mvFontRangeHint_Default) + # add specific range of glyphs + dpg.add_font_range(0xe000, 0xf8ff) + + with dpg.theme(tag="header_theme_main"): + with dpg.theme_component(dpg.mvCollapsingHeader): + dpg.add_theme_color(dpg.mvThemeCol_Header, (41, 107, 31, 255), category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderHovered, (63, 161, 48, 255), category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderActive, (82, 209, 63, 255), category=dpg.mvThemeCat_Core) + + with dpg.theme(tag="header_theme_addon"): + with dpg.theme_component(dpg.mvCollapsingHeader): + dpg.add_theme_color(dpg.mvThemeCol_Header, (32, 110, 63, 255), category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderHovered, (41, 143, 82, 255), category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderActive, (49, 168, 97, 255), category=dpg.mvThemeCat_Core) + + with dpg.theme(tag="header_theme_public"): + with dpg.theme_component(dpg.mvCollapsingHeader): + dpg.add_theme_color(dpg.mvThemeCol_Header, (112, 85, 31, 255), category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderHovered, (128, 97, 36, 255), category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderActive, (143, 108, 40, 255), category=dpg.mvThemeCat_Core) + + with dpg.theme(tag="header_theme_builtin"): + with dpg.theme_component(dpg.mvCollapsingHeader): + dpg.add_theme_color(dpg.mvThemeCol_Header, (32, 51, 115, 255), category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderHovered, (39, 61, 138, 255), category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderActive, (43, 67, 153, 255), category=dpg.mvThemeCat_Core) + + with dpg.theme(tag="header_theme_custom"): + with dpg.theme_component(dpg.mvCollapsingHeader): + dpg.add_theme_color(dpg.mvThemeCol_Header, (33, 99, 117, 255), category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderHovered, (39, 119, 140, 255), category=dpg.mvThemeCat_Core) + dpg.add_theme_color(dpg.mvThemeCol_HeaderActive, (47, 143, 168, 255), category=dpg.mvThemeCat_Core) + + with dpg.theme(tag="tree_node_folder"): + with dpg.theme_component(dpg.mvTreeNode): + dpg.add_theme_color(dpg.mvThemeCol_Header, (209, 177, 15, 150)) + dpg.add_theme_color(dpg.mvThemeCol_HeaderHovered, (222, 188, 16, 200)) + dpg.add_theme_color(dpg.mvThemeCol_HeaderActive, (235, 199, 16, 255)) + with dpg.theme_component(dpg.mvTreeNode): + dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (209, 177, 15, 150)) # color even if closed + + with dpg.theme(tag="no_padding_theme"): + with dpg.theme_component(dpg.mvWindowAppItem): + dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 0, 0, parent=dpg.last_item()) + + with dpg.theme(tag="padding_theme"): + with dpg.theme_component(dpg.mvWindowAppItem): + dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 30, 30, parent=dpg.last_item()) + + with dpg.theme(tag="no_padding_table_theme"): + with dpg.theme_component(dpg.mvTable): + dpg.add_theme_style(dpg.mvStyleVar_CellPadding, 0, 0) \ No newline at end of file diff --git a/tools/odoolsspy/views/tree_browser.py b/tools/odoolsspy/views/tree_browser.py new file mode 100644 index 00000000..e606f8f2 --- /dev/null +++ b/tools/odoolsspy/views/tree_browser.py @@ -0,0 +1,180 @@ +import sys +from PyQt6.QtWidgets import QApplication, QTreeView, QWidget, QVBoxLayout, QFileIconProvider +from PyQt6.QtCore import Qt, QAbstractItemModel, QModelIndex +from PyQt6.QtGui import QIcon + +from views.symbols import Symbol + +class TreeBrowser(QWidget): + + def __init__(self, app, path, tree): + self.app = app + self.path = path + self.tree = tree + self.symbols = {} + super().__init__() + + def setup_ui(self, app): + root = Node(app, "Root", self.path, [[], []], typ="ROOT") + self.model = CustomFileModel(root) + self.tree_view = QTreeView() + self.tree_view.setModel(self.model) + self.selection_model = self.tree_view.selectionModel() + self.selection_model.selectionChanged.connect(self.on_selection_changed) + previous = root + parent = QModelIndex() + #do not add them here, but open them here + for tree_el in self.tree[0]: + previous.load_children() + for inode in previous.children: + if inode.name == tree_el: + previous = inode + break + if previous: + parent = self.expand_node(parent, previous) + else: + break + for tree_el in self.tree[1]: + if not previous: + break + previous.load_children() + previous = None + for inode in previous.children: + if inode.name == tree_el: + previous = inode + break + if previous: + parent = self.expand_node(parent, previous) + else: + break + layout = QVBoxLayout() + layout.addWidget(self.tree_view) + self.setLayout(layout) + + def expand_node(self, parent: QModelIndex, node): + """Expands a node in the tree view""" + if not node or not node.parent: + return + + node_index = self.model.index(node.row(), 0, parent) + + if node_index.isValid(): + self.tree_view.setExpanded(node_index, True) + + return node_index + + def on_selection_changed(self, selected, deselected): + for index in selected.indexes(): # Get selected indexes + if index.isValid(): + node = index.internalPointer() + from views.symbols import Symbol + data = Symbol.prepare_symbol(self.app, node.name, self.path, node.tree) + if data: + self.symbol = Symbol(data) + self.app.replace_right_tab(self.symbol.name, self.symbol, "symbol_tree_" + str(node.tree)) + +class Node: + def __init__(self, app, name, entry_path, tree, typ, parent=None): + self.name = name + self.app = app + self.entry_path = entry_path + self.tree = tree + self.parent = parent + self.children = [] + self.typ = typ + self.is_loaded = False + + def add_child(self, child): + child.parent = self + self.children.append(child) + + def load_children(self): + """ Load only if opened""" + if not self.is_loaded: + id = self.app.connection_mgr.send_message("$/ToolAPI/browse_tree", { + "entry_path": self.entry_path, + "tree": self.tree + }) + response = self.app.connection_mgr.get_response(id) + if "result" in response: + self.is_loaded = True + result = response["result"] + modules = result["modules"] + for entry in modules: + self.add_child(Node(self.app, entry["name"], self.entry_path, [self.tree[0] + [entry["name"]], []], entry["type"])) + + def child(self, row): + return self.children[row] if 0 <= row < len(self.children) else None + + def child_count(self): + return len(self.children) + + + def row(self): + if self.parent and self in self.parent.children: + return self.parent.children.index(self) + return 0 + +class CustomFileModel(QAbstractItemModel): + def __init__(self, root, parent=None): + super().__init__(parent) + self.root = root # Le nœud racine + self.icon_provider = QFileIconProvider() + + def index(self, row, column, parent=QModelIndex()): + if not parent.isValid(): + parent_node = self.root + else: + parent_node = parent.internalPointer() + + child_node = parent_node.child(row) + if child_node: + return self.createIndex(row, column, child_node) + return QModelIndex() + + def parent(self, index): + if not index.isValid(): + return QModelIndex() + + child_node = index.internalPointer() + if child_node: + parent_node = child_node.parent + if parent_node and parent_node != self.root: + return self.createIndex(parent_node.row(), 0, parent_node) + + return QModelIndex() + + def rowCount(self, parent=QModelIndex()): + if not parent.isValid(): + return self.root.child_count() + node = parent.internalPointer() + + if not node.is_loaded: + node.load_children() + return node.child_count() + + def columnCount(self, parent=QModelIndex()): + return 1 + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + if not index.isValid(): + return None + + node = index.internalPointer() + + if role == Qt.ItemDataRole.DisplayRole: + return node.name + + if role == Qt.ItemDataRole.DecorationRole: + if node.typ == "DISK_DIR": + return self.icon_provider.icon(QFileIconProvider.IconType.Folder) + else: + return self.icon_provider.icon(QFileIconProvider.IconType.File) + + return None + + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): + """Sets column header text.""" + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + return "Root" + return None \ No newline at end of file diff --git a/vscode/folder.png b/vscode/folder.png new file mode 100644 index 00000000..295a8894 Binary files /dev/null and b/vscode/folder.png differ