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 &format!("Failed to create comments: {}", 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(
122 NotificationLevel::Error,
123 &format!("Failed to write comments: {}", e),
124 );
125 return;
126 }
127 self.log(NotificationLevel::Info, "Comments saved.");
128 self.comments.reset_dirty();
129 }
130 }
131
132 pub(super) fn load_comments(&mut self, comments_path: Option<String>) {
134 let comments_path = comments_path.unwrap_or(self.get_comments_path());
135 match self.filesystem.read(&comments_path) {
136 Ok(comments_data) => match serde_json::from_slice::<Comments>(&comments_data) {
137 Ok(comments) => {
138 self.comments = comments;
139 self.comments
140 .check_max_address(self.data.bytes().len() as u64);
141 self.log(NotificationLevel::Info, "Comments loaded.");
142 }
143 Err(e) => {
144 self.log(
145 NotificationLevel::Error,
146 &format!("Failed to parse comments: {}", e),
147 );
148 }
149 },
150 Err(e) => {
151 self.log(
153 NotificationLevel::Debug,
154 &format!("Failed to read comments: {}", e),
155 );
156 self.comments = Comments::new();
157 }
158 }
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use crate::{app::plugins::plugin::Plugin, get_app_context};
165
166 use super::*;
167
168 #[test]
169 fn insert_and_remove() {
170 let mut comments = Comments::new();
171 comments.insert(0x1000, "comment_1".to_string());
172 assert!(comments.is_dirty());
173 comments.insert(0x2000, "comment_2".to_string());
174 comments.insert(0x3000, "comment_3".to_string());
175 comments.reset_dirty();
176 assert_eq!(comments.len(), 3);
177 assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
178 assert_eq!(comments.get(&0x2000), Some(&"comment_2".to_string()));
179 assert_eq!(comments.get(&0x3000), Some(&"comment_3".to_string()));
180 assert!(!comments.is_dirty());
181 comments.remove(&0x2000);
182 assert!(comments.is_dirty());
183 assert_eq!(comments.len(), 2);
184 assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
185 assert_eq!(comments.get(&0x2000), None);
186 assert_eq!(comments.get(&0x3000), Some(&"comment_3".to_string()));
187 comments.check_max_address(0x2000);
188 assert_eq!(comments.len(), 1);
189 assert_eq!(comments.get(&0x1000), Some(&"comment_1".to_string()));
190 assert_eq!(comments.get(&0x2000), None);
191 assert_eq!(comments.get(&0x3000), None);
192 }
193
194 #[test]
195 fn save_and_load() {
196 let mut app = App::mockup(vec![0x90; 0x100]);
197 let tmp_comments = tempfile::NamedTempFile::new().unwrap();
198 let comments_path = tmp_comments.path().to_str().unwrap().to_string();
199 app.comments.insert(0x10, "comment_1".to_string());
200 app.comments.insert(0x20, "comment_2".to_string());
201 app.comments.insert(0x30, "comment_3".to_string());
202 app.save_comments(Some(comments_path.clone()));
203 assert!(!app.comments.is_dirty());
204 assert!(app.logger.get_notification_level() < NotificationLevel::Warning);
205 app.comments = Comments::new();
206 app.load_comments(Some(comments_path));
207 assert_eq!(app.comments.len(), 3);
208 assert_eq!(app.comments.get(&0x10), Some(&"comment_1".to_string()));
209 assert_eq!(app.comments.get(&0x20), Some(&"comment_2".to_string()));
210 assert_eq!(app.comments.get(&0x30), Some(&"comment_3".to_string()));
211 }
212
213 #[test]
214 fn test_plugin() {
215 let source = "
216 function init(context)
217 context.set_comment(0x10, 'comment_1')
218 context.set_comment(0x20, 'comment_2')
219 context.set_comment(0x30, 'comment_3')
220 c1 = context.get_comment(0x10)
221 c2 = context.get_comment(0x20)
222 c3 = context.get_comment(0x30)
223 should_be_nil = context.get_comment(0x40)
224 assert(c1 == 'comment_1', 'c1')
225 assert(c2 == 'comment_2', 'c2')
226 assert(c3 == 'comment_3', 'c3')
227 assert(should_be_nil == nil, 'should_be_nil')
228 comments = context.get_comments()
229 assert(comments[0x10] == 'comment_1', 'table c1')
230 assert(comments[0x20] == 'comment_2', 'table c2')
231 assert(comments[0x30] == 'comment_3', 'table c3')
232 context.set_comment(0x10, '')
233 assert(context.get_comment(0x10) == nil, 'remove c1')
234 context.set_comment(0x20, nil)
235 assert(context.get_comment(0x20) == nil, 'remove c2')
236 end";
237
238 let mut app = App::mockup(vec![0; 0x100]);
239 let mut app_context = get_app_context!(app);
240 Plugin::new_from_source(source, &mut app_context).unwrap();
241 }
242}