Blame plugins/findinfiles/job.vala

Packit 6978fb
/*
Packit 6978fb
* Copyright (C) 2015 The Lemon Man
Packit 6978fb
*
Packit 6978fb
* This program is free software; you can redistribute it and/or modify
Packit 6978fb
* it under the terms of the GNU General Public License as published by
Packit 6978fb
* the Free Software Foundation; either version 2, or (at your option)
Packit 6978fb
* any later version.
Packit 6978fb
*
Packit 6978fb
* This program is distributed in the hope that it will be useful,
Packit 6978fb
* but WITHOUT ANY WARRANTY; without even the implied warranty of
Packit 6978fb
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Packit 6978fb
* GNU General Public License for more details.
Packit 6978fb
*
Packit 6978fb
* You should have received a copy of the GNU General Public License
Packit 6978fb
* along with this program; if not, write to the Free Software
Packit 6978fb
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Packit 6978fb
*/
Packit 6978fb
Packit 6978fb
namespace Gedit {
Packit 6978fb
namespace FindInFilesPlugin {
Packit 6978fb
Packit 6978fb
interface IMatcher : Object {
Packit 6978fb
    public abstract bool has_match (uint8 *text, size_t text_length, size_t pos, ref Range match);
Packit 6978fb
}
Packit 6978fb
Packit 6978fb
struct Range {
Packit 6978fb
    size_t from;
Packit 6978fb
    size_t to;
Packit 6978fb
}
Packit 6978fb
Packit 6978fb
struct Bookmark {
Packit 6978fb
    int line_number;
Packit 6978fb
    size_t line_start;
Packit 6978fb
    size_t line_length;
Packit 6978fb
    public static const Bookmark EmptyBookmark = { 0, 0 };
Packit 6978fb
}
Packit 6978fb
Packit 6978fb
struct Result {
Packit 6978fb
    string path;
Packit 6978fb
    size_t line;
Packit 6978fb
    string context;
Packit 6978fb
}
Packit 6978fb
Packit 6978fb
class FindJob {
Packit 6978fb
    public signal void on_match_found (Result result);
Packit 6978fb
    public signal void on_search_finished ();
Packit 6978fb
Packit 6978fb
    // This queue holds all the file names to scan
Packit 6978fb
    private AsyncQueue<string> scan_queue = new AsyncQueue<string>();
Packit 6978fb
Packit 6978fb
    // The list of (hard) workers
Packit 6978fb
    private List<Thread<int>> thread_workers;
Packit 6978fb
Packit 6978fb
    // Count how many workers are still working
Packit 6978fb
    private uint running_workers = 0;
Packit 6978fb
Packit 6978fb
    private IMatcher? matcher = null;
Packit 6978fb
Packit 6978fb
    private Cancellable cancellable;
Packit 6978fb
Packit 6978fb
    // The actual job parameters
Packit 6978fb
    public bool include_hidden;
Packit 6978fb
    public bool match_whole_word;
Packit 6978fb
    public bool ignore_case;
Packit 6978fb
    public string? needle { get; private set; }
Packit 6978fb
Packit 6978fb
    int worker () {
Packit 6978fb
        while (true) {
Packit 6978fb
            // Wait 0.5 seconds
Packit 6978fb
            var tv = TimeVal ();
Packit 6978fb
            tv.add (1000000 / 2);
Packit 6978fb
Packit 6978fb
            var path = scan_queue.timed_pop (ref tv);
Packit 6978fb
Packit 6978fb
            // Check for interruption
Packit 6978fb
            if (cancellable.is_cancelled ())
Packit 6978fb
                break;
Packit 6978fb
Packit 6978fb
            // If path is null then we're probably done
Packit 6978fb
            if (path == null)
Packit 6978fb
                break;
Packit 6978fb
Packit 6978fb
            // Scan the file
Packit 6978fb
            scan_file (path);
Packit 6978fb
        }
Packit 6978fb
Packit 6978fb
        // We're done, check if we're the last worker active and signal it to the user
Packit 6978fb
        lock (running_workers) {
Packit 6978fb
            if (0 == (--running_workers)) {
Packit 6978fb
                // Run the completion callback in the main thread
Packit 6978fb
                Idle.add (() => { on_search_finished (); return false; });
Packit 6978fb
            }
Packit 6978fb
        }
Packit 6978fb
Packit 6978fb
        return 0;
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    public FindJob (Cancellable? cancellable) {
Packit 6978fb
        this.cancellable = cancellable ?? new Cancellable ();
Packit 6978fb
        needle = null;
Packit 6978fb
        include_hidden = false;
Packit 6978fb
        match_whole_word = false;
Packit 6978fb
        ignore_case = false;
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    public void prepare (string needle, bool is_regex) throws Error {
Packit 6978fb
        // Construct the matcher instance
Packit 6978fb
        if (is_regex) {
Packit 6978fb
              matcher = new RegexFind (needle, ignore_case);
Packit 6978fb
        } else {
Packit 6978fb
              matcher = new BoyerMooreHorspool (needle, ignore_case);
Packit 6978fb
        }
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    void start_workers_pool () {
Packit 6978fb
        if (running_workers != 0) {
Packit 6978fb
            return;
Packit 6978fb
        }
Packit 6978fb
Packit 6978fb
        var online_cpus = get_num_processors ();
Packit 6978fb
Packit 6978fb
        for (var i = 0; i < online_cpus; i++) {
Packit 6978fb
            thread_workers.append (new Thread<int> ("Worker %d".printf (i), worker));
Packit 6978fb
        }
Packit 6978fb
Packit 6978fb
        running_workers = online_cpus;
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    public string extract_context (uint8 *buffer, Range range) {
Packit 6978fb
        uint8 *slice = new uint8[range.to - range.from];
Packit 6978fb
        Posix.memcpy (slice, buffer + range.from, range.to - range.from);
Packit 6978fb
        return Gedit.utils_make_valid_utf8 ((string)slice);
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    public void halt () {
Packit 6978fb
        if (running_workers == 0)
Packit 6978fb
            return;
Packit 6978fb
Packit 6978fb
        cancellable.cancel ();
Packit 6978fb
Packit 6978fb
        foreach (var worker in thread_workers) {
Packit 6978fb
            worker.join ();
Packit 6978fb
        }
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    bool is_binary (uint8 *buffer, size_t buffer_size) {
Packit 6978fb
        // Poor-man binary detection, check for NULL bytes
Packit 6978fb
        return Posix.memchr (buffer, '\0', buffer_size) != null;
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    bool ignore_vcs_dir (FileInfo file) {
Packit 6978fb
        if (file.get_file_type () == FileType.DIRECTORY) {
Packit 6978fb
            var name = file.get_name ();
Packit 6978fb
Packit 6978fb
            if (name == ".git" ||
Packit 6978fb
                name == ".bzr" ||
Packit 6978fb
                name == ".svn" ||
Packit 6978fb
                name == ".hg"  ||
Packit 6978fb
                name == "_darcs")
Packit 6978fb
                return true;
Packit 6978fb
        }
Packit 6978fb
Packit 6978fb
        return false;
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    public async void execute (string root) throws Error {
Packit 6978fb
        var queue = new Queue<File> ();
Packit 6978fb
        var visited = new HashTable<string, bool> (str_hash, str_equal);
Packit 6978fb
Packit 6978fb
        start_workers_pool ();
Packit 6978fb
Packit 6978fb
        queue.push_tail (File.new_for_path (root));
Packit 6978fb
Packit 6978fb
        while (!queue.is_empty ()) {
Packit 6978fb
            if (cancellable.is_cancelled ())
Packit 6978fb
                break;
Packit 6978fb
Packit 6978fb
            var dir = queue.pop_head ();
Packit 6978fb
Packit 6978fb
            var e = yield dir.enumerate_children_async (FileAttribute.STANDARD_NAME,
Packit 6978fb
                                                        FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
Packit 6978fb
                                                        Priority.DEFAULT);
Packit 6978fb
Packit 6978fb
            // Process the entries we got so far
Packit 6978fb
            while (true) {
Packit 6978fb
                var files = yield e.next_files_async (100);
Packit 6978fb
Packit 6978fb
                if (files == null)
Packit 6978fb
                    break;
Packit 6978fb
Packit 6978fb
                if (cancellable.is_cancelled ())
Packit 6978fb
                    break;
Packit 6978fb
Packit 6978fb
                foreach (var file in files) {
Packit 6978fb
                    if (!include_hidden && file.get_name ()[0] == '.')
Packit 6978fb
                        continue;
Packit 6978fb
Packit 6978fb
                    if (ignore_vcs_dir (file))
Packit 6978fb
                        continue;
Packit 6978fb
Packit 6978fb
                    var _file = dir.resolve_relative_path (file.get_name ());
Packit 6978fb
Packit 6978fb
                    switch (file.get_file_type ()) {
Packit 6978fb
                    case FileType.REGULAR:
Packit 6978fb
                        scan_queue.push (_file.get_path ());
Packit 6978fb
                        break;
Packit 6978fb
Packit 6978fb
                    case FileType.DIRECTORY:
Packit 6978fb
                        queue.push_tail (_file);
Packit 6978fb
                        visited.insert (_file.get_path (), true);
Packit 6978fb
                        break;
Packit 6978fb
Packit 6978fb
                    case FileType.SYMBOLIC_LINK:
Packit 6978fb
                        var link_dest = Posix.realpath (_file.get_path ());
Packit 6978fb
Packit 6978fb
                        // Follow the link only if its not broken and we didn't visit it
Packit 6978fb
                        if (link_dest != null && !visited.contains (link_dest)) {
Packit 6978fb
                            if (FileUtils.test (link_dest, FileTest.IS_DIR) &&
Packit 6978fb
                                (!ignore_case && link_dest[0] != '.'))
Packit 6978fb
                                queue.push_tail (File.new_for_path (link_dest));
Packit 6978fb
                        }
Packit 6978fb
Packit 6978fb
                        break;
Packit 6978fb
                    }
Packit 6978fb
                }
Packit 6978fb
            }
Packit 6978fb
        }
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    // Returns the line number where the text span starting at 'from' and ending at 'to' lies.
Packit 6978fb
    void get_line (uint8 *buffer, size_t buffer_size, ref Range span, ref Bookmark bookmark) {
Packit 6978fb
        // We take an advantage by knowing that all the calls to get_line are sequential, hence
Packit 6978fb
        // we save the position of the last matched line and start from there
Packit 6978fb
        var line_count = bookmark.line_number;
Packit 6978fb
        var line_start = bookmark.line_start;
Packit 6978fb
Packit 6978fb
        var ptr = buffer + bookmark.line_start;
Packit 6978fb
Packit 6978fb
        while (ptr < buffer + buffer_size) {
Packit 6978fb
            // Find the newline
Packit 6978fb
            uint8 *nl = Posix.memchr (ptr, '\n', buffer_size - (ptr - buffer));
Packit 6978fb
Packit 6978fb
            if (nl == null) {
Packit 6978fb
                // No more newlines, consider the trailing NULL as the final newline
Packit 6978fb
                nl = buffer + buffer_size + 1;
Packit 6978fb
            } else {
Packit 6978fb
                // Skip the '\n'
Packit 6978fb
                nl++;
Packit 6978fb
            }
Packit 6978fb
Packit 6978fb
            var line_length = nl - ptr;
Packit 6978fb
Packit 6978fb
            // Check if the match is within this line
Packit 6978fb
            if (span.from >= line_start && span.to < line_start + line_length) {
Packit 6978fb
                // Update the bookmark
Packit 6978fb
                bookmark.line_number = line_count;
Packit 6978fb
                bookmark.line_start = line_start;
Packit 6978fb
                bookmark.line_length = line_length - 1;
Packit 6978fb
Packit 6978fb
                return;
Packit 6978fb
            }
Packit 6978fb
Packit 6978fb
            line_count++;
Packit 6978fb
            line_start += line_length;
Packit 6978fb
Packit 6978fb
            ptr = nl;
Packit 6978fb
        }
Packit 6978fb
Packit 6978fb
        // There's no way to exit the loop, unless there's a problem in the logic above
Packit 6978fb
        assert_not_reached ();
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    bool is_word_boundary (uint8 *buf, size_t buf_size, size_t from, size_t to) {
Packit 6978fb
        unichar ch;
Packit 6978fb
        bool prev, next;
Packit 6978fb
        unowned string str;
Packit 6978fb
Packit 6978fb
        assert (to > from && to <= buf_size);
Packit 6978fb
Packit 6978fb
        // There's not much we can do
Packit 6978fb
        if (to - from > int.MAX)
Packit 6978fb
            return false;
Packit 6978fb
Packit 6978fb
        // Silence the warning about ch being uninitialized
Packit 6978fb
        ch = '\0';
Packit 6978fb
Packit 6978fb
        prev = next = true;
Packit 6978fb
        str = (string)(buf + from);
Packit 6978fb
Packit 6978fb
        // Those are being modified by the get_{prev,next}_char calls
Packit 6978fb
        int start = 0;
Packit 6978fb
        int end = (int)(to - from);
Packit 6978fb
Packit 6978fb
        // The match is on a word boundary if there are no consecutive alphanumeric
Packit 6978fb
        // characters right before or after the match
Packit 6978fb
        var head = str.get_char (0);
Packit 6978fb
        if (start > 0 && str.get_prev_char (ref start, out ch))
Packit 6978fb
            prev = head.isalnum () != ch.isalnum ();
Packit 6978fb
Packit 6978fb
        var tail = str.get_char (end - 1);
Packit 6978fb
        if (end < buf_size && str.get_next_char (ref end, out ch))
Packit 6978fb
            next = tail.isalnum () != ch.isalnum ();
Packit 6978fb
Packit 6978fb
        return prev && next;
Packit 6978fb
    }
Packit 6978fb
Packit 6978fb
    void scan_file (string path) {
Packit 6978fb
        MappedFile file;
Packit 6978fb
Packit 6978fb
        try {
Packit 6978fb
            file = new MappedFile (path, false);
Packit 6978fb
        }
Packit 6978fb
        catch (FileError err) {
Packit 6978fb
            warning (err.message);
Packit 6978fb
            return;
Packit 6978fb
        }
Packit 6978fb
Packit 6978fb
        var buffer_size = file.get_length ();
Packit 6978fb
        var buffer = (uint8 *)file.get_contents ();
Packit 6978fb
Packit 6978fb
        // Skip binary files for obvious reasons
Packit 6978fb
        if (is_binary (buffer, buffer_size))
Packit 6978fb
            return;
Packit 6978fb
Packit 6978fb
        Range match = { 0, 0 };
Packit 6978fb
        Bookmark bookmark = Bookmark.EmptyBookmark;
Packit 6978fb
        var last_line = -1;
Packit 6978fb
Packit 6978fb
        for (size_t buffer_pos = 0; buffer_pos < buffer_size; ) {
Packit 6978fb
            if (cancellable.is_cancelled ())
Packit 6978fb
                break;
Packit 6978fb
Packit 6978fb
            // Exit when there's no match
Packit 6978fb
            if (!matcher.has_match (buffer, buffer_size, buffer_pos, ref match))
Packit 6978fb
                break;
Packit 6978fb
Packit 6978fb
            // Check if the match lies on a word boundary
Packit 6978fb
            if (match_whole_word) {
Packit 6978fb
                if (!is_word_boundary (buffer, buffer_size, (int)match.from, (int)match.to)) {
Packit 6978fb
                    buffer_pos = match.to;
Packit 6978fb
                    continue;
Packit 6978fb
                }
Packit 6978fb
            }
Packit 6978fb
Packit 6978fb
            get_line (buffer, buffer_size, ref match, ref bookmark);
Packit 6978fb
Packit 6978fb
            // Find out what line the match lies in
Packit 6978fb
            var match_line = 1 + bookmark.line_number;
Packit 6978fb
Packit 6978fb
            // Notify that we got a match
Packit 6978fb
            if (last_line != match_line) {
Packit 6978fb
                Result res = { path, match_line, extract_context (buffer, match) };
Packit 6978fb
                on_match_found (res);
Packit 6978fb
            }
Packit 6978fb
Packit 6978fb
            last_line = match_line;
Packit 6978fb
Packit 6978fb
            // Keep searching past the match
Packit 6978fb
            buffer_pos = match.to;
Packit 6978fb
        }
Packit 6978fb
    }
Packit 6978fb
}
Packit 6978fb
Packit 6978fb
} // namespace FindInFilesPlugin
Packit 6978fb
} // namespace Gedit