1use std::collections::{hash_map::Iter, HashMap};
2
3use serde::{Deserialize, Serialize};
4
5use super::{log::NotificationLevel, App};
6
7#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct Comments {
9 comments: HashMap<u64, String>,
10 #[serde(skip)]
11 dirty: bool,
12}
13
14impl Comments {
15 pub fn new() -> Self {
16 Self {
17 comments: HashMap::new(),
18 dirty: false,
19 }
20 }
21
22 pub fn remove(&mut self, address: &u64) {
23 self.comments.remove(address);
24 self.dirty = true;
25 }
26
27 pub fn insert(&mut self, address: u64, comment: String) {
28 self.comments.insert(address, comment);
29 self.dirty = true;
30 }
31
32 pub fn iter(&self) -> Iter<'_, u64, String> {
33 self.comments.iter()
34 }
35
36 pub fn get(&self, address: &u64) -> Option<&String> {
37 self.comments.get(address)
38 }
39
40 pub fn len(&self) -> usize {
41 self.comments.len()
42 }
43
44 pub fn is_empty(&self) -> bool {
45 self.comments.is_empty()
46 }
47
48 pub fn is_dirty(&self) -> bool {
49 self.dirty
50 }
51
52 pub fn reset_dirty(&mut self) {
53 self.dirty = false;
54 }
55
56 pub fn to_vec(&self) -> Vec<(u64, String)> {
57 self.comments.iter().map(|(a, s)| (*a, s.clone())).collect()
58 }
59
60 pub fn check_max_address(&mut self, max_address: u64) {
61 let mut comments_removed = false;
62 self.comments.retain(|address, _| {
63 if *address > max_address {
64 comments_removed = true;
65 false
66 } else {
67 true
68 }
69 });
70 if comments_removed {
71 self.dirty = true;
72 }
73 }
74}
75
76impl App {
77 pub(super) fn edit_comment(&mut self, comment: &str) {
78 let address = self.get_cursor_position().global_byte_index as u64;
79 if comment.is_empty() {
80 self.comments.remove(&address);
81 } else {
82 self.comments.insert(address, comment.to_string());
83 }
84 }
85
86 pub(super) fn find_comments(&self, filter: &str) -> Vec<(u64, String)> {
87 if filter.is_empty() {
88 return Vec::new();
89 }
90 let mut comments: Vec<(u64, String)> = self
91 .comments
92 .iter()
93 .filter(|(_, symbol)| symbol.contains(filter))
94 .map(|(address, symbol)| (*address, symbol.clone()))
95 .collect();
96 comments.sort_by_key(|(_, symbol)| symbol.len());
97 comments
98 }
99
100 pub(super) fn get_comments_path(&self) -> String {
101 let path = self.filesystem.pwd();
102 path.to_string() + ".hp-data.json"
103 }
104
105 pub(super) fn save_comments(&mut self, comments_path: Option<String>) {
107 if self.comments.is_dirty() {
108 let comments_str = serde_json::to_string_pretty(&self.comments).unwrap();
109 let comments_path = comments_path.unwrap_or(self.get_comments_path());
110 if let Err(e) = self.filesystem.create(&comments_path) {
111 self.log(
112 NotificationLevel::Error,
113 t!("errors.create_comments", e = e),
114 );
115 return;
116 }
117 if let Err(e) = self
118 .filesystem
119 .write(&comments_path, comments_str.as_bytes())
120 {
121 self.log(NotificationLevel::Error, t!("errors.write_comments", e = e));
122 return;
123 }
124 self.log(NotificationLevel::Info, t!("app.messages.comments_saved"));
125 self.comments.reset_dirty();
126 }
127 }
128
129 pub(super) fn load_comments(&mut self, comments_path: Option<String>) {
131 let comments_path = comments_path.unwrap_or(self.get_comments_path());
132 match self.filesystem.read(&comments_path) {
133 Ok(comments_data) => match serde_json::from_slice::<Comments>(&comments_data) {
134 Ok(comments) => {
135 self.comments = comments;
136 self.comments
137 .check_max_address(self.data.bytes().len() as u64);
138 self.log(NotificationLevel::Info, t!("app.messages.comments_loaded"));
139 }
140 Err(e) => {
141 self.log(NotificationLevel::Error, t!("errors.parse_comments", e = e));
142 }
143 },
144 Err(e) => {
145 self.log(NotificationLevel::Debug, t!("errors.read_comments", e = e));
147 self.comments = Comments::new();
148 }
149 }
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use crate::{app::plugins::plugin::Plugin, get_app_context};
156
157 use super::*;
158
159 #[test]
160 fn insert_and_remove() {
161 let mut comments = Comments::new();
162 comments.insert(0x1000, "comment_1".to_string());
163 assert!(comments.is_dirty());
164 comments.insert(0x2000, "comment_2".to_string());
165 comments.insert(0x3000, "comment_3".to_string());
166 comments.reset_dirty();
167 assert_eq!(comments.len(), 3);
168 assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
169 assert_eq!(comments.get(&0x2000), Some(&"comment_2".to_string()));
170 assert_eq!(comments.get(&0x3000), Some(&"comment_3".to_string()));
171 assert!(!comments.is_dirty());
172 comments.remove(&0x2000);
173 assert!(comments.is_dirty());
174 assert_eq!(comments.len(), 2);
175 assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
176 assert_eq!(comments.get(&0x2000), None);
177 assert_eq!(comments.get(&0x3000), Some(&"comment_3".to_string()));
178 comments.check_max_address(0x2000);
179 assert_eq!(comments.len(), 1);
180 assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
181 assert_eq!(comments.get(&0x2000), None);
182 assert_eq!(comments.get(&0x3000), None);
183 }
184
185 #[test]
186 fn save_and_load() {
187 let mut app = App::mockup(vec![0x90; 0x100]);
188 let tmp_comments = tempfile::NamedTempFile::new().unwrap();
189 let comments_path = tmp_comments.path().to_str().unwrap().to_string();
190 app.comments.insert(0x10, "comment_1".to_string());
191 app.comments.insert(0x20, "comment_2".to_string());
192 app.comments.insert(0x30, "comment_3".to_string());
193 app.save_comments(Some(comments_path.clone()));
194 assert!(!app.comments.is_dirty());
195 assert!(app.logger.get_notification_level() < NotificationLevel::Warning);
196 app.comments = Comments::new();
197 app.load_comments(Some(comments_path));
198 assert_eq!(app.comments.len(), 3);
199 assert_eq!(app.comments.get(&0x10), Some(&"comment_1".to_string()));
200 assert_eq!(app.comments.get(&0x20), Some(&"comment_2".to_string()));
201 assert_eq!(app.comments.get(&0x30), Some(&"comment_3".to_string()));
202 }
203
204 #[test]
205 fn test_plugin() {
206 let source = "
207 function init(context)
208 context.set_comment(0x10, 'comment_1')
209 context.set_comment(0x20, 'comment_2')
210 context.set_comment(0x30, 'comment_3')
211 c1 = context.get_comment(0x10)
212 c2 = context.get_comment(0x20)
213 c3 = context.get_comment(0x30)
214 should_be_nil = context.get_comment(0x40)
215 assert(c1 == 'comment_1', 'c1')
216 assert(c2 == 'comment_2', 'c2')
217 assert(c3 == 'comment_3', 'c3')
218 assert(should_be_nil == nil, 'should_be_nil')
219 comments = context.get_comments()
220 assert(comments[0x10] == 'comment_1', 'table c1')
221 assert(comments[0x20] == 'comment_2', 'table c2')
222 assert(comments[0x30] == 'comment_3', 'table c3')
223 context.set_comment(0x10, '')
224 assert(context.get_comment(0x10) == nil, 'remove c1')
225 context.set_comment(0x20, nil)
226 assert(context.get_comment(0x20) == nil, 'remove c2')
227 end";
228
229 let mut app = App::mockup(vec![0; 0x100]);
230 let mut app_context = get_app_context!(app);
231 Plugin::new_from_source(source, &mut app_context).unwrap();
232 }
233}