Files
FERRUM/ripgrep.c
2025-04-28 12:35:52 +02:00

746 lines
24 KiB
C

/**
* ripgrep.c
* Interactive wrapper for the ripgrep (rg) command-line search tool
*/
#include "ripgrep.h"
#include "common.h"
#include "line_reader.h"
#include "shell.h" // Add this for lsh_execute
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h> // For PATH_MAX
#include <unistd.h> // For access function
#include <termios.h> // For terminal control
/**
* Check if ripgrep (rg) is installed on the system
*
* @return 1 if installed, 0 if not
*/
int is_rg_installed(void) {
// Try to run rg --version to check if it's installed
FILE *fp = popen("rg --version 2>/dev/null", "r");
if (fp == NULL) {
return 0;
}
// Read output (if any)
char buffer[128];
int has_output = (fgets(buffer, sizeof(buffer), fp) != NULL);
// Close the process
pclose(fp);
return has_output;
}
/**
* Display instructions for installing ripgrep
*/
void show_rg_install_instructions(void) {
printf("\nripgrep (rg) is not installed on this system. To use this feature, "
"install ripgrep:\n\n");
printf("Installation options:\n");
printf("1. Using package manager (Debian/Ubuntu):\n");
printf(" sudo apt install ripgrep\n\n");
printf("2. Using package manager (Fedora):\n");
printf(" sudo dnf install ripgrep\n\n");
printf("3. Using package manager (Arch Linux):\n");
printf(" sudo pacman -S ripgrep\n\n");
printf("4. Download prebuilt binary from: "
"https://github.com/BurntSushi/ripgrep/releases\n\n");
printf("After installation, restart your shell.\n");
}
/**
* Check if a specific editor is available
*
* @param editor Name of the editor executable
* @return 1 if available, 0 if not
*/
int is_editor_available_for_rg(const char *editor) {
char command[256];
snprintf(command, sizeof(command), "%s --version >/dev/null 2>&1", editor);
return (system(command) == 0);
}
/**
* Open a file at a specific line in the best available editor
*
* @param file_path Path to the file to open
* @param line_number Line number to position cursor at
* @return 1 if successful, 0 if failed
*/
int rg_open_in_editor(const char *file_path, int line_number) {
char command[2048] = {0};
int success = 0;
// Try to detect available editors (in order of preference)
if (is_editor_available_for_rg("nvim")) {
// Neovim is available - construct command with +line_number
snprintf(command, sizeof(command), "nvim +%d \"%s\"", line_number,
file_path);
success = 1;
} else if (is_editor_available_for_rg("vim")) {
// Vim is available
snprintf(command, sizeof(command), "vim +%d \"%s\"", line_number,
file_path);
success = 1;
} else if (is_editor_available_for_rg("nano")) {
// Try nano if available
// Note: nano doesn't have perfect line number navigation
snprintf(command, sizeof(command), "nano +%d \"%s\"", line_number,
file_path);
success = 1;
} else if (is_editor_available_for_rg("code")) {
// VSCode with line number
snprintf(command, sizeof(command), "code -g \"%s:%d\" -r", file_path,
line_number);
success = 1;
} else if (is_editor_available_for_rg("gedit")) {
// gedit as last resort (limited line number support)
snprintf(command, sizeof(command), "gedit +%d \"%s\"", line_number, file_path);
success = 1;
}
if (success) {
// Clear the screen before launching the editor
system("clear");
// Execute the command directly in the current terminal
int result = system(command);
success = (result == 0);
return success;
} else {
// No suitable editor found
printf("No compatible editor (neovim, vim, nano, VSCode, gedit) found.\n");
return 0;
}
}
/**
* Parse a ripgrep result line to extract file path and line number
*
* @param result_line The result line from ripgrep (format:
* file:line:column:text)
* @param file_path Buffer to store extracted file path
* @param file_path_size Size of file_path buffer
* @param line_number Pointer to store extracted line number
* @return 1 if parsing successful, 0 if failed
*/
static int parse_rg_result(const char *result_line, char *file_path,
size_t file_path_size, int *line_number) {
// Ripgrep output format is: file:line:column:text
const char *first_colon = strchr(result_line, ':');
if (!first_colon)
return 0;
const char *second_colon = strchr(first_colon + 1, ':');
if (!second_colon)
return 0;
// Extract file path
size_t path_length = first_colon - result_line;
if (path_length >= file_path_size)
path_length = file_path_size - 1;
strncpy(file_path, result_line, path_length);
file_path[path_length] = '\0';
// Extract line number
char line_str[16] = {0};
size_t line_str_length = second_colon - (first_colon + 1);
if (line_str_length >= sizeof(line_str))
line_str_length = sizeof(line_str) - 1;
strncpy(line_str, first_colon + 1, line_str_length);
line_str[line_str_length] = '\0';
*line_number = atoi(line_str);
return 1;
}
/**
* Run ripgrep with interactive filtering
*
* @param args Command arguments or NULL for interactive mode
* @return Selected filename/line or NULL if canceled
*/
char *run_interactive_ripgrep(char **args) {
// Check if ripgrep is installed
if (!is_rg_installed()) {
show_rg_install_instructions();
return NULL;
}
// Create temporary files for the search results
char temp_rg_results[PATH_MAX] = "/tmp/rg_results.txt";
char temp_fzf_output[PATH_MAX] = "/tmp/rg_selected.txt";
// Build the initial command
char command[4096] = {0};
// Base ripgrep command with sensible defaults
strcpy(command,
"rg --line-number --column --no-heading --color=always --smart-case");
// Add any user-provided arguments
if (args && args[1] != NULL) {
int i = 1;
while (args[i] != NULL) {
strcat(command, " ");
// If the argument contains spaces, quote it
if (strchr(args[i], ' ') != NULL) {
strcat(command, "\"");
strcat(command, args[i]);
strcat(command, "\"");
} else {
strcat(command, args[i]);
}
i++;
}
} else {
// If no search pattern provided, use empty string to search all files
// Will be filtered by fzf as user types
strcat(command, " \"\"");
}
// Pipe to fzf for interactive selection with preview
strcat(command, " | fzf --ansi");
// Add keybindings for navigation
strcat(command, " --bind=\"ctrl-j:down,ctrl-k:up,/:toggle-search\"");
// Add preview window showing file content with line highlighted
strcat(command, " --preview=\"bat --color=always --style=numbers "
"--highlight-line={2} {1}\"");
strcat(command, " --preview-window=+{2}-10");
// Add output redirection
strcat(command, " > ");
strcat(command, temp_fzf_output);
// Run the command
printf("Starting interactive ripgrep search...\n");
int result = system(command);
// Check if user canceled (fzf returns non-zero)
if (result != 0) {
unlink(temp_fzf_output);
return NULL;
}
// Read the selected result from the temporary file
FILE *fp = fopen(temp_fzf_output, "r");
if (!fp) {
unlink(temp_fzf_output);
return NULL;
}
// Read the selected line
char *selected = (char *)malloc(PATH_MAX * 2);
if (!selected) {
fclose(fp);
unlink(temp_fzf_output);
return NULL;
}
if (fgets(selected, PATH_MAX * 2, fp) == NULL) {
fclose(fp);
unlink(temp_fzf_output);
free(selected);
return NULL;
}
// Remove newline if present
size_t len = strlen(selected);
if (len > 0 && selected[len - 1] == '\n') {
selected[len - 1] = '\0';
}
// Close and delete the temporary file
fclose(fp);
unlink(temp_fzf_output);
return selected;
}
/**
* Run ripgrep with interactive editing capabilities for a custom Neovim-like
* experience
*/
void run_ripgrep_interactive_session(void) {
// Check if ripgrep and fzf are installed
if (!is_rg_installed()) {
show_rg_install_instructions();
return;
}
// Initialize the search query
char search_query[256] = "";
char last_query[256] = "";
// Create a temporary file for results
char temp_results[PATH_MAX] = "/tmp/ripgrep_results.txt";
// Save original terminal settings to restore later
struct termios old_tio, new_tio;
tcgetattr(STDIN_FILENO, &old_tio);
// Copy the old settings and modify for raw input
new_tio = old_tio;
new_tio.c_lflag &= ~(ICANON | ECHO); // Disable canonical mode and echo
new_tio.c_cc[VMIN] = 1; // Read one character at a time
new_tio.c_cc[VTIME] = 0; // No timeout
tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
// Clear screen and show initial UI
system("clear");
printf("--- Interactive Ripgrep Search ---\n");
printf(
"Type to search | Ctrl+N/P: navigate | Enter: open | Ctrl+C: exit\n\n");
printf("Search: ");
int running = 1;
int selected_index = 0;
int results_count = 0;
char **results = NULL;
// Main interaction loop
while (running) {
// Display the current search query
printf("\rSearch: %s", search_query);
fflush(stdout);
// Check if the query has changed and we need to run ripgrep again
if (strcmp(search_query, last_query) != 0) {
// Free previous results if any
if (results) {
for (int i = 0; i < results_count; i++) {
free(results[i]);
}
free(results);
results = NULL;
results_count = 0;
}
// Execute ripgrep with the current query if not empty
if (strlen(search_query) > 0) {
// Build ripgrep command
char command[4096];
snprintf(command, sizeof(command),
"rg --line-number --column --no-heading --color=never "
"--smart-case \"%s\" > %s",
search_query, temp_results);
system(command);
// Read results from the temp file
FILE *fp = fopen(temp_results, "r");
if (fp) {
// Count lines first
char line[1024];
results_count = 0;
while (fgets(line, sizeof(line), fp) != NULL) {
results_count++;
}
// Allocate memory for results
rewind(fp);
results = (char **)malloc(results_count * sizeof(char *));
if (results) {
int i = 0;
while (i < results_count && fgets(line, sizeof(line), fp) != NULL) {
// Remove newline
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n') {
line[len - 1] = '\0';
}
results[i] = strdup(line);
i++;
}
}
fclose(fp);
}
}
// Update last query
strcpy(last_query, search_query);
// Reset selection to first result
selected_index = 0;
// Clear the results display area
printf("\n\033[J"); // Clear from cursor to end of screen
// Display results count
printf("\nFound %d matches\n\n", results_count);
// Display results with selection
int max_display = 10; // Show at most 10 results at a time
for (int i = 0; i < results_count && i < max_display; i++) {
if (i == selected_index) {
printf("\033[7m> %s\033[0m\n",
results[i]); // Invert colors for selection
} else {
printf(" %s\n", results[i]);
}
}
}
// Process user input
unsigned char c;
if (read(STDIN_FILENO, &c, 1) > 0) {
if (c == 3) { // Ctrl+C
running = 0;
} else if (c == 13 || c == 10) { // Enter or newline
// Open the selected file if there are results
if (results_count > 0 && selected_index < results_count) {
char file_path[PATH_MAX];
int line_number;
if (parse_rg_result(results[selected_index], file_path,
sizeof(file_path), &line_number)) {
rg_open_in_editor(file_path, line_number);
// After returning from editor, refresh the UI
system("clear");
printf("--- Interactive Ripgrep Search ---\n");
printf("Type to search | Ctrl+N/P: navigate | Enter: open | Ctrl+C: "
"exit\n\n");
printf("Search: %s\n\n", search_query);
// Re-display results count
printf("Found %d matches\n\n", results_count);
// Re-display results with selection
int max_display = 10;
for (int i = 0; i < results_count && i < max_display; i++) {
if (i == selected_index) {
printf("\033[7m> %s\033[0m\n", results[i]);
} else {
printf(" %s\n", results[i]);
}
}
}
}
} else if (c == 14) { // Ctrl+N
if (results_count > 0) {
selected_index = (selected_index + 1) % results_count;
// Redraw results with new selection
printf("\033[%dA",
results_count > 10
? 10
: results_count); // Move cursor up to results start
int max_display = 10;
for (int i = 0; i < results_count && i < max_display; i++) {
if (i == selected_index) {
printf("\033[2K\033[7m> %s\033[0m\n",
results[i]); // Clear line and invert colors
} else {
printf("\033[2K %s\n", results[i]); // Clear line
}
}
}
} else if (c == 16) { // Ctrl+P
if (results_count > 0) {
selected_index = (selected_index - 1 + results_count) % results_count;
// Redraw results with new selection
printf("\033[%dA",
results_count > 10
? 10
: results_count); // Move cursor up to results start
int max_display = 10;
for (int i = 0; i < results_count && i < max_display; i++) {
if (i == selected_index) {
printf("\033[2K\033[7m> %s\033[0m\n",
results[i]); // Clear line and invert colors
} else {
printf("\033[2K %s\n", results[i]); // Clear line
}
}
}
} else if (c == 127 || c == 8) { // Backspace (127 on Linux, 8 on some terminals)
// Remove last character from search query
size_t len = strlen(search_query);
if (len > 0) {
search_query[len - 1] = '\0';
}
} else if (isprint(c)) { // Printable character
// Add character to search query
size_t len = strlen(search_query);
if (len < sizeof(search_query) - 1) {
search_query[len] = c;
search_query[len + 1] = '\0';
}
}
}
}
// Clean up
if (results) {
for (int i = 0; i < results_count; i++) {
free(results[i]);
}
free(results);
}
unlink(temp_results);
// Restore original terminal settings
tcsetattr(STDIN_FILENO, TCSANOW, &old_tio);
// Final screen clear
system("clear");
}
/**
* Command handler for the ripgrep command
*
* @param args Command arguments
* @return 1 to continue shell execution, 0 to exit shell
*/
int lsh_ripgrep(char **args) {
// Check if ripgrep is installed
if (!is_rg_installed()) {
printf("Ripgrep (rg) is not installed. Falling back to custom "
"implementation.\n");
printf("For better performance, consider installing ripgrep:\n");
show_rg_install_instructions();
printf("\nRunning with custom implementation...\n\n");
// Fall back to our custom implementation
run_ripgrep_interactive_session();
return 1;
}
// Also check if fzf is installed for the interactive UI
int fzf_available = is_fzf_installed(); // Reuse from fzf_native.h
// Check for help flag
if (args[1] &&
(strcmp(args[1], "--help") == 0 || strcmp(args[1], "-h") == 0)) {
printf("Usage: ripgrep [pattern] [options]\n");
printf("Interactive code search using ripgrep (rg) with fzf.\n\n");
printf("If called without arguments, launches fzf with ripgrep for "
"interactive searching.\n\n");
printf("Options:\n");
printf(" -t, --type [TYPE] Only search files matching TYPE (e.g., -t "
"cpp)\n");
printf(" -i, --ignore-case Case insensitive search\n");
printf(" -w, --word-regexp Only match whole words\n");
printf(" -e, --regexp Treat pattern as a regular expression\n");
printf(" -f, --fixed-strings Treat pattern as a literal string\n");
printf(" -g, --glob [GLOB] Include/exclude files matching the glob\n");
return 1;
}
// If no arguments provided, use fzf with ripgrep
if (!args[1]) {
if (!fzf_available) {
printf("fzf is not installed. Falling back to custom implementation.\n");
show_fzf_install_instructions();
printf("\nRunning with custom implementation...\n\n");
// Fall back to custom implementation if fzf is not available
run_ripgrep_interactive_session();
return 1;
}
// Create a temporary file for the fzf preview script
char preview_script[PATH_MAX] = "/tmp/fzf_preview.sh";
// Create the preview script as a shell script with highlighting support
FILE *preview_fp = fopen(preview_script, "w");
if (preview_fp) {
// Write a shell script that highlights searched content
fprintf(preview_fp, "#!/bin/bash\n");
fprintf(preview_fp, "file=\"$1\"\n");
fprintf(preview_fp, "line=\"$2\"\n");
fprintf(preview_fp, "query=\"$3\"\n");
fprintf(preview_fp, "if [ -z \"$query\" ]; then\n");
// If no query, just show the file with line highlighting
fprintf(preview_fp, " bat --color=always --highlight-line \"$line\" \"$file\" 2>/dev/null || cat \"$file\"\n");
fprintf(preview_fp, "else\n");
// If query provided, highlight both the line and the search term
fprintf(preview_fp, " if grep -i \"$query\" \"$file\" >/dev/null 2>&1; then\n");
fprintf(preview_fp, " rg --color=always --context 3 --line-number \"$query\" \"$file\" 2>/dev/null || ");
fprintf(preview_fp, "bat --color=always --highlight-line \"$line\" \"$file\" 2>/dev/null || cat \"$file\"\n");
fprintf(preview_fp, " else\n");
fprintf(preview_fp, " bat --color=always --highlight-line \"$line\" \"$file\" 2>/dev/null || cat \"$file\"\n");
fprintf(preview_fp, " fi\n");
fprintf(preview_fp, "fi\n");
fclose(preview_fp);
// Make the script executable
chmod(preview_script, 0755);
}
// Use fzf with ripgrep to provide an interactive UI similar to Neovim
char command[4096] = {0};
// Use ripgrep with an empty pattern to search all text files
// and pipe to fzf for interactive filtering - now with fullscreen mode
snprintf(
command, sizeof(command),
"clear && rg --line-number --column --no-heading --color=always \"\" | "
"fzf --ansi --delimiter : "
"--preview \"%s {1} {2} {q}\" "
"--preview-window=right:60%%:wrap "
"--bind \"ctrl-j:down,ctrl-k:up,enter:accept\" "
"--border "
"--height=100%% > %s",
preview_script, "/tmp/rg_selection.txt");
// Execute the command
int result = system(command);
// Check if user selected something (fzf returns non-zero on cancel)
if (result == 0) {
// Read the selected file/line from the temporary file
FILE *fp = fopen("/tmp/rg_selection.txt", "r");
if (fp) {
char selected[1024];
if (fgets(selected, sizeof(selected), fp)) {
// Remove newline if present
size_t len = strlen(selected);
if (len > 0 && selected[len - 1] == '\n') {
selected[len - 1] = '\0';
}
// Parse the selection to extract file path and line number
char file_path[PATH_MAX];
int line_number;
if (parse_rg_result(selected, file_path, sizeof(file_path),
&line_number)) {
printf("Opening %s at line %d\n", file_path, line_number);
rg_open_in_editor(file_path, line_number);
}
}
fclose(fp);
unlink("/tmp/rg_selection.txt"); // Clean up
}
}
// Remove the preview script
unlink(preview_script);
return 1;
}
// If options are provided but no pattern, run regular ripgrep
if (args[1][0] == '-') {
// Build command to pass to system
char command[4096] = "rg";
// Add all arguments
for (int i = 1; args[i] != NULL; i++) {
strcat(command, " ");
// If argument contains spaces, quote it
if (strchr(args[i], ' ') != NULL) {
strcat(command, "\"");
strcat(command, args[i]);
strcat(command, "\"");
} else {
strcat(command, args[i]);
}
}
// Execute standard ripgrep command
system(command);
return 1;
}
// If a pattern is provided, run ripgrep with fzf for selection
if (fzf_available) {
// Create a temporary file for the fzf preview script
char preview_script[PATH_MAX] = "/tmp/fzf_preview.sh";
// Create the preview script as a shell script with highlighting
FILE *preview_fp = fopen(preview_script, "w");
if (preview_fp) {
// Write a shell script that highlights searched content
fprintf(preview_fp, "#!/bin/bash\n");
fprintf(preview_fp, "file=\"$1\"\n");
fprintf(preview_fp, "line=\"$2\"\n");
fprintf(preview_fp, "search_term=\"%s\"\n", args[1]); // Use the search term provided
// Show file with both line highlighting and search term highlighting
fprintf(preview_fp,
"rg --color=always --context 3 --line-number \"%s\" \"$file\" 2>/dev/null || ",
args[1]);
fprintf(preview_fp, "bat --color=always --highlight-line \"$line\" \"$file\" 2>/dev/null || cat \"$file\"\n");
fclose(preview_fp);
// Make the script executable
chmod(preview_script, 0755);
}
// Build command with fzf for better interaction
char command[4096] = {0};
// Now with better UI but using compatible options
snprintf(
command, sizeof(command),
"clear && rg --line-number --column --no-heading --color=always \"%s\" | "
"fzf --ansi --delimiter : "
"--preview \"%s {1} {2}\" "
"--preview-window=right:60%%:wrap "
"--bind \"ctrl-j:down,ctrl-k:up,enter:accept\" "
"--border "
"--height=100%% > %s",
args[1], preview_script, "/tmp/rg_selection.txt");
// Execute the command
int result = system(command);
// Check if user selected something
if (result == 0) {
// Read the selected file/line from the temporary file
FILE *fp = fopen("/tmp/rg_selection.txt", "r");
if (fp) {
char selected[1024];
if (fgets(selected, sizeof(selected), fp)) {
// Remove newline if present
size_t len = strlen(selected);
if (len > 0 && selected[len - 1] == '\n') {
selected[len - 1] = '\0';
}
// Parse the selection to extract file path and line number
char file_path[PATH_MAX];
int line_number;
if (parse_rg_result(selected, file_path, sizeof(file_path),
&line_number)) {
printf("Opening %s at line %d\n", file_path, line_number);
rg_open_in_editor(file_path, line_number);
}
}
fclose(fp);
unlink("/tmp/rg_selection.txt"); // Clean up
}
}
// Remove the preview script
unlink(preview_script);
} else {
// If fzf is not available, fall back to custom implementation
printf("fzf is not installed. Falling back to custom implementation.\n");
run_ripgrep_interactive_session();
}
return 1;
}