```rust
use std::{
    collections::{HashMap, VecDeque},
    fs::read_to_string,
    ops::Range,
    path::{Path, PathBuf},
};

#[derive(Default)]
pub struct SearchOptions {
    // pub regex: bool,
    pub case_insensitive: bool,
    pub whole_word: bool,
    pub line_buffer: usize,
}

pub type SearchFileList = Vec<(PathBuf, SearchFile)>;

pub type SearchFile = (HashMap<usize, String>, Vec<SearchPos>);

#[cfg_attr(test, derive(Debug, PartialEq))]
pub struct SearchPos {
    pub row: usize,
    pub col: Range<usize>,
}

pub fn search(path: impl AsRef<Path>, pattern: &str, options: &SearchOptions) -> SearchFileList {
    if pattern.is_empty() {
        return Vec::new();
    }

    let mut result = Vec::new();
    for entry in ignore::Walk::new(path).flatten() {
        let path = entry.path();
        if let Ok(f) = read_to_string(path) {
            if f.is_empty() {
                continue;
            }

            let mut poses = Vec::new();
            let mut lines = HashMap::new();

            let mut queue = VecDeque::with_capacity(options.line_buffer);
            let mut tail = 0;

            for (i, l) in f.split('\n').enumerate() {
                let row = i + 1;
                let line = l.to_string();
                let cols = find_cols(&line, pattern, options);

                if !cols.is_empty() {
                    lines.insert(row, line);
                    let len = queue.len();
                    for (i, l) in queue.drain(..).enumerate() {
                        lines.insert(row - len + i, l);
                    }
                    tail = options.line_buffer;
                } else if tail > 0 {
                    lines.insert(row, line);
                    tail -= 1;
                } else if options.line_buffer > 0 {
                    if queue.len() == options.line_buffer {
                        queue.pop_front();
                    }
                    queue.push_back(line);
                }
                for col in cols {
                    poses.push(SearchPos { row, col });
                }
            }
            if !poses.is_empty() {
                result.push((path.to_path_buf(), (lines, poses)));
            }
        }
    }
    result
}

fn find_cols(line: &str, pattern: &str, options: &SearchOptions) -> Vec<Range<usize>> {
    fn is_word_boundary(ch: char) -> bool {
        !(ch.is_alphanumeric() || ch == '_')
    }

    if pattern.is_empty() {
        return Vec::new();
    }

    let mut cols = Vec::new();
    let mut line = line.to_string();
    let mut pattern = pattern.to_string();
    if options.case_insensitive {
        line = line.to_lowercase();
        pattern = pattern.to_lowercase();
    }
    let mut start = 0;
    while let Some(i) = line[start..].find(&pattern) {
        let skip = i + pattern.len();
        if !options.whole_word
            || (line[..start + i]
                .chars()
                .next_back()
                .is_none_or(is_word_boundary)
                && line[start + skip..]
                    .chars()
                    .next()
                    .is_none_or(is_word_boundary))
        {
            cols.push(start + i..start + skip);
        }
        start += skip;
    }
    cols
}
```
find symbols that should be better named