1 /*
2 * lineoperations.c - Line operations, remove duplicate lines, empty lines,
3 * lines with only whitespace, sort lines.
4 *
5 * Copyright 2015 Sylvan Mostert <smostert.dev@gmail.com>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 */
21
22
23 #ifdef HAVE_CONFIG_H
24 #include "config.h" /* for the gettext domain */
25 #endif
26
27 #include <geanyplugin.h>
28 #include "Scintilla.h"
29 #include "lo_fns.h"
30 #include "lo_prefs.h"
31
32
33 static GtkWidget *main_menu_item = NULL;
34
35
36 /* represents a selection of lines that will have Operation applied to */
37 struct lo_lines
38 {
39 gboolean is_selection;
40 gint start_line;
41 gint end_line;
42 };
43
44
45 /* represents a menu item and key binding */
46 struct lo_menu_item
47 {
48 const gchar *label;
49 const gchar *kb_section_name;
50 GCallback cb_activate;
51 gpointer cb_data;
52 };
53
54
55 typedef void (*CB_USER_FUNCTION)(GtkMenuItem *menuitem, gpointer gdata);
56
57
58 /* selects lines in document (based on lo_lines struct parameter) */
59 static void
select_lines(GeanyEditor * editor,struct lo_lines * sel)60 select_lines(GeanyEditor *editor, struct lo_lines *sel)
61 {
62 /* set the selection to beginning of first line */
63 sci_set_selection_start(editor->sci,
64 sci_get_position_from_line(editor->sci, sel->start_line));
65
66 /* set the selection to end of last line */
67 sci_set_selection_end(editor->sci,
68 sci_get_line_end_position(editor->sci, sel->end_line) +
69 editor_get_eol_char_len(editor));
70 }
71
72
73 /* get lo_lines struct 'sel' from document */
74 static void
get_current_sel_lines(ScintillaObject * sci,struct lo_lines * sel)75 get_current_sel_lines(ScintillaObject *sci, struct lo_lines *sel)
76 {
77 gint start_posn = 0; /* position of selection start */
78 gint end_posn = 0; /* position of selection end */
79
80 /* check for selection */
81 if (sci_has_selection(sci))
82 {
83 /* get the start and end *positions* */
84 start_posn = sci_get_selection_start(sci);
85 end_posn = sci_get_selection_end (sci);
86
87 /* get the *line number* of those positions */
88 sel->start_line = scintilla_send_message(sci,
89 SCI_LINEFROMPOSITION,
90 start_posn, 0);
91
92 sel->end_line = scintilla_send_message(sci,
93 SCI_LINEFROMPOSITION,
94 end_posn, 0);
95
96 sel->is_selection = TRUE;
97 }
98 else
99 {
100 /* if there is no selection, start at first line */
101 sel->start_line = 0;
102 /* and end at last one */
103 sel->end_line = (sci_get_line_count(sci) - 1);
104
105 sel->is_selection = FALSE;
106 }
107 }
108
109
110 /* altered from geany/src/editor.c, ensure new line at file end */
111 static void
ensure_final_newline(GeanyEditor * editor,gint * num_lines,struct lo_lines * sel)112 ensure_final_newline(GeanyEditor *editor, gint *num_lines, struct lo_lines *sel)
113 {
114 gint end_document = sci_get_position_from_line(editor->sci, (*num_lines));
115 gboolean append_newline = end_document >
116 sci_get_position_from_line(editor->sci, ((*num_lines) - 1));
117
118 if (append_newline)
119 {
120 const gchar *eol = editor_get_eol_char(editor);
121 sci_insert_text(editor->sci, end_document, eol);
122
123 /* re-adjust the selection */
124 (*num_lines)++;
125 sel->end_line++;
126 }
127 }
128
129
130 /* set statusbar with message and select altered lines */
131 static void
user_indicate(GeanyEditor * editor,gint lines_affected,struct lo_lines * sel)132 user_indicate(GeanyEditor *editor, gint lines_affected, struct lo_lines *sel)
133 {
134 if (lines_affected < 0)
135 {
136 ui_set_statusbar(FALSE, _("Operation successful! %d lines removed."),
137 -lines_affected);
138
139 /* select lines to indicate to user what lines were altered */
140 sel->end_line += lines_affected;
141
142 if (sel->is_selection)
143 select_lines(editor, sel);
144 }
145 else if (lines_affected == 0)
146 {
147 ui_set_statusbar(FALSE, _("Operation successful! No lines removed."));
148
149 /* select lines to indicate to user what lines were altered */
150 if (sel->is_selection)
151 select_lines(editor, sel);
152 }
153 else
154 {
155 ui_set_statusbar(FALSE, _("Operation successful! %d lines affected."),
156 lines_affected);
157
158 /* select lines to indicate to user what lines were altered */
159 if (sel->is_selection)
160 select_lines(editor, sel);
161 }
162 }
163
164
165 /*
166 * Menu action for functions with indirect scintilla manipulation
167 * e.g. functions requiring **lines array, num_lines, *new_file
168 *
169 * Use 'action_sci_manip_item()' if possible, since direction
170 * manipulation of Scintilla doc is faster/better.
171 * Use this if the line operation cannot be easily done with
172 * scintilla functions.
173 */
174 static void
action_indir_manip_item(GtkMenuItem * menuitem,gpointer gdata)175 action_indir_manip_item(GtkMenuItem *menuitem, gpointer gdata)
176 {
177 /* function pointer to function to be used */
178 gint (*func)(gchar **lines, gint num_lines, gchar *new_file) = gdata;
179 GeanyDocument *doc = document_get_current();
180 g_return_if_fail(doc != NULL);
181
182 struct lo_lines sel;
183 gint num_chars = 0;
184 gint i = 0;
185 gint lines_affected = 0;
186
187 get_current_sel_lines(doc->editor->sci, &sel);
188 gint num_lines = (sel.end_line - sel.start_line) + 1;
189
190 /* if last line within selection ensure that the file ends with newline */
191 if ((sel.end_line + 1) == sci_get_line_count(doc->editor->sci))
192 ensure_final_newline(doc->editor, &num_lines, &sel);
193
194 /* get num_chars and **lines */
195 gchar **lines = g_malloc(sizeof(gchar *) * num_lines);
196 for (i = 0; i < num_lines; i++)
197 {
198 num_chars += (sci_get_line_length(doc->editor->sci,
199 (i + sel.start_line)));
200
201 lines[i] = sci_get_line(doc->editor->sci,
202 (i + sel.start_line));
203 }
204
205 gchar *new_file = g_malloc(sizeof(gchar) * (num_chars + 1));
206 new_file[0] = '\0';
207
208 /* select lines that will be replaced with array */
209 select_lines(doc->editor, &sel);
210
211 sci_start_undo_action(doc->editor->sci);
212
213 lines_affected = func(lines, num_lines, new_file);
214
215
216 /* set new document */
217 sci_replace_sel(doc->editor->sci, new_file);
218
219 /* select affected lines and set statusbar message */
220 user_indicate(doc->editor, lines_affected, &sel);
221
222 sci_end_undo_action(doc->editor->sci);
223
224 /* free used memory */
225 for (i = 0; i < num_lines; i++)
226 g_free(lines[i]);
227 g_free(lines);
228 g_free(new_file);
229 }
230
231
232 /*
233 * Menu action for functions with direct scintilla manipulation
234 * e.g. no need for **lines array, *new_file...
235 *
236 * Use this if the line operation can be directly done with
237 * scintilla functions.
238 */
239 static void
action_sci_manip_item(GtkMenuItem * menuitem,gpointer gdata)240 action_sci_manip_item(GtkMenuItem *menuitem, gpointer gdata)
241 {
242 /* function pointer to gdata -- function to be used */
243 gint (*func)(ScintillaObject *, gint, gint) = gdata;
244 GeanyDocument *doc = document_get_current();
245 g_return_if_fail(doc != NULL);
246
247 struct lo_lines sel;
248 get_current_sel_lines(doc->editor->sci, &sel);
249 gint lines_affected = 0;
250
251 sci_start_undo_action(doc->editor->sci);
252
253 lines_affected = func(doc->editor->sci, sel.start_line, sel.end_line);
254
255
256 /* put message in ui_statusbar, and highlight lines that were affected */
257 user_indicate(doc->editor, lines_affected, &sel);
258
259 sci_end_undo_action(doc->editor->sci);
260 }
261
262
263 /* List of menu items, also used for keybindings. */
264 static struct lo_menu_item menu_items[] = {
265 { N_("Remove Duplicate Lines, _Sorted"), "remove_duplicate_lines_s",
266 G_CALLBACK(action_indir_manip_item), (gpointer) rmdupst },
267 { N_("Remove Duplicate Lines, _Ordered"), "remove_duplicate_lines_o",
268 G_CALLBACK(action_indir_manip_item), (gpointer) rmdupln },
269 { N_("Remove _Unique Lines"), "remove_unique_lines",
270 G_CALLBACK(action_indir_manip_item), (gpointer) rmunqln },
271 { N_("Keep _Unique Lines"), "keep_unique_lines",
272 G_CALLBACK(action_indir_manip_item), (gpointer) kpunqln },
273 { NULL },
274 { N_("Remove _Empty Lines"), "remove_empty_lines",
275 G_CALLBACK(action_sci_manip_item), (gpointer) rmemtyln },
276 { N_("Remove _Whitespace Lines"), "remove_whitespace_lines",
277 G_CALLBACK(action_sci_manip_item), (gpointer) rmwhspln },
278 { NULL },
279 { N_("Remove Every _Nth Line"), "remove_every_nth_line",
280 G_CALLBACK(action_sci_manip_item), (gpointer) rmnthln },
281 { NULL },
282 { N_("Sort Lines _Ascending"), "sort_lines_ascending",
283 G_CALLBACK(action_indir_manip_item), (gpointer) sortlnsasc },
284 { N_("Sort Lines _Descending"), "sort_lines_descending",
285 G_CALLBACK(action_indir_manip_item), (gpointer) sortlndesc }
286 };
287
288
289 /* Keybinding callback */
lo_keybinding_callback(guint key_id)290 static void lo_keybinding_callback(guint key_id)
291 {
292 CB_USER_FUNCTION cb_activate;
293 g_return_if_fail(key_id < G_N_ELEMENTS(menu_items));
294 cb_activate = (CB_USER_FUNCTION)menu_items[key_id].cb_activate;
295 cb_activate(NULL, menu_items[key_id].cb_data);
296 }
297
298
299 /* Initialization */
300 static gboolean
lo_init(GeanyPlugin * plugin,G_GNUC_UNUSED gpointer gdata)301 lo_init(GeanyPlugin *plugin, G_GNUC_UNUSED gpointer gdata)
302 {
303 GeanyData *geany_data = plugin->geany_data;
304 GeanyKeyGroup *key_group;
305 GtkWidget *submenu;
306 guint i;
307
308 lo_init_prefs(plugin);
309
310 main_menu_item = gtk_menu_item_new_with_mnemonic(_("_Line Operations"));
311 gtk_widget_show(main_menu_item);
312
313 submenu = gtk_menu_new();
314 gtk_widget_show(submenu);
315
316 for (i = 0; i < G_N_ELEMENTS(menu_items); i++)
317 {
318 GtkWidget *item;
319
320 if (! menu_items[i].label) /* separator */
321 item = gtk_separator_menu_item_new();
322 else
323 {
324 item = gtk_menu_item_new_with_mnemonic(_(menu_items[i].label));
325 g_signal_connect(item,
326 "activate",
327 menu_items[i].cb_activate,
328 menu_items[i].cb_data);
329 ui_add_document_sensitive(item);
330 }
331
332 gtk_widget_show(item);
333 gtk_menu_shell_append(GTK_MENU_SHELL(submenu), item);
334 }
335
336 gtk_menu_item_set_submenu(GTK_MENU_ITEM(main_menu_item), submenu);
337 gtk_container_add(GTK_CONTAINER(geany->main_widgets->tools_menu),
338 main_menu_item);
339
340 /* Setup keybindings. */
341 key_group = plugin_set_key_group
342 (plugin, "Line Operations", G_N_ELEMENTS(menu_items), NULL);
343 for (i = 0; i < G_N_ELEMENTS(menu_items); i++)
344 {
345 if (menu_items[i].label != NULL)
346 {
347 keybindings_set_item(key_group, i,
348 lo_keybinding_callback, 0, 0, menu_items[i].kb_section_name,
349 menu_items[i].label, NULL);
350 }
351 }
352
353 return TRUE;
354 }
355
356
357 /* Show help */
358 static void
lo_help(G_GNUC_UNUSED GeanyPlugin * plugin,G_GNUC_UNUSED gpointer pdata)359 lo_help (G_GNUC_UNUSED GeanyPlugin *plugin, G_GNUC_UNUSED gpointer pdata)
360 {
361 utils_open_browser("https://plugins.geany.org/lineoperations.html");
362 }
363
364
365 static void
lo_cleanup(GeanyPlugin * plugin,gpointer pdata)366 lo_cleanup(GeanyPlugin *plugin, gpointer pdata)
367 {
368 gtk_widget_destroy(main_menu_item);
369 lo_free_info();
370 }
371
372
373 G_MODULE_EXPORT
geany_load_module(GeanyPlugin * plugin)374 void geany_load_module(GeanyPlugin *plugin)
375 {
376 main_locale_init(LOCALEDIR, GETTEXT_PACKAGE);
377
378 plugin->info->name = _("Line Operations");
379 plugin->info->description = _("Line Operations provides a handful of functions that can be applied to a document or selection such as, removing duplicate lines, removing empty lines, removing lines with only whitespace, and sorting lines.");
380 plugin->info->version = "0.3";
381 plugin->info->author = "Sylvan Mostert <smostert.dev@gmail.com>";
382
383 plugin->funcs->init = lo_init;
384 plugin->funcs->cleanup = lo_cleanup;
385 plugin->funcs->configure = lo_configure;
386 plugin->funcs->help = lo_help;
387
388 GEANY_PLUGIN_REGISTER(plugin, 225);
389 }
390