#[allow(dead_code)]
#[derive(Clone)]
pub struct StickyLinesConfig {
pub enabled: bool,
pub max_lines: usize,
}
impl Default for StickyLinesConfig {
fn default() -> Self {
Self {
enabled: true,
max_lines: 5,
}
}
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct StickyLine {
pub line_number: usize,
pub content: String,
pub indentation: usize,
}
#[allow(dead_code)]
fn get_indentation(line: &str) -> usize {
let mut indent = 0;
for ch in line.chars() {
match ch {
' ' => indent += 1,
'\t' => indent += 4,
_ => break,
}
}
indent
}
#[allow(dead_code)]
fn is_block_opener(line: &str) -> bool {
let trimmed = line.trim();
if trimmed.is_empty() {
return false;
}
if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with("/*") {
return false;
}
if !trimmed.ends_with('{') && !trimmed.ends_with(':') {
return false;
}
if trimmed == "{" || trimmed == ":{" {
return false;
}
let lower = trimmed.to_lowercase();
if lower.contains("fn ")
|| lower.contains("func ")
|| lower.contains("function ")
|| lower.contains("def ")
|| lower.starts_with("pub fn ")
|| lower.starts_with("async fn ")
|| lower.starts_with("pub async fn ")
{
return true;
}
if lower.contains("impl ")
|| lower.contains("struct ")
|| lower.contains("enum ")
|| lower.contains("trait ")
|| lower.contains("class ")
|| lower.contains("interface ")
|| lower.starts_with("pub struct ")
|| lower.starts_with("pub enum ")
|| lower.starts_with("pub trait ")
{
return true;
}
if lower.starts_with("if ")
|| lower.starts_with("} else if ")
|| lower.starts_with("else if ")
|| lower == "else {"
|| lower.starts_with("for ")
|| lower.starts_with("while ")
|| lower.starts_with("loop ")
|| lower.starts_with("match ")
|| lower.starts_with("switch ")
|| lower.starts_with("try ")
|| lower.starts_with("catch ")
|| lower.starts_with("finally ")
{
return true;
}
if lower.starts_with("mod ")
|| lower.starts_with("pub mod ")
|| lower.starts_with("namespace ")
|| lower.starts_with("module ")
{
return true;
}
if (lower.contains("|") && lower.ends_with('{'))
|| lower.contains("=> {")
|| lower.contains("-> {")
{
return true;
}
false
}
#[allow(dead_code)]
fn is_multiline_fn_start(line: &str) -> bool {
let trimmed = line.trim();
if trimmed.is_empty() {
return false;
}
if !trimmed.ends_with('(') {
return false;
}
let lower = trimmed.to_lowercase();
if lower.contains("fn ")
|| lower.starts_with("pub fn ")
|| lower.starts_with("async fn ")
|| lower.starts_with("pub async fn ")
{
return true;
}
if lower.contains("function ") || lower.contains("function(") {
return true;
}
if (lower.contains("private ")
|| lower.contains("public ")
|| lower.contains("protected ")
|| lower.contains("static ")
|| lower.contains("async "))
&& trimmed.ends_with('(')
{
return true;
}
if lower.starts_with("def ") || lower.contains(" def ") {
return true;
}
false
}
#[allow(dead_code)]
fn is_multiline_fn_end(line: &str) -> bool {
let trimmed = line.trim();
if !trimmed.ends_with('{') {
return false;
}
trimmed.starts_with(')')
|| trimmed.starts_with("):")
|| trimmed.starts_with(") {")
|| trimmed.starts_with(") =")
|| trimmed.contains(") {")
|| trimmed.contains("): ")
|| trimmed.contains(") =>")
}
#[allow(dead_code)]
pub fn compute_sticky_lines(
lines: &[(usize, String)],
scroll_position: usize,
config: &StickyLinesConfig,
) -> Vec<StickyLine> {
if !config.enabled || lines.is_empty() || scroll_position == 0 {
return Vec::new();
}
let mut open_blocks: Vec<(usize, usize, String, usize)> = Vec::new();
let mut pending_multiline_fn: Option<(usize, usize, String, usize)> = None;
for (idx, (line_num, content)) in lines.iter().enumerate() {
if idx >= scroll_position {
break;
}
let indent = get_indentation(content);
let close_braces = content.matches('}').count();
if is_multiline_fn_start(content) {
pending_multiline_fn = Some((idx, *line_num, content.clone(), indent));
}
else if is_multiline_fn_end(content) {
if let Some((start_idx, start_line_num, start_content, start_indent)) =
pending_multiline_fn.take()
{
open_blocks.push((start_idx, start_line_num, start_content, start_indent));
} else {
open_blocks.push((idx, *line_num, content.clone(), indent));
}
}
else if is_block_opener(content) {
pending_multiline_fn = None; open_blocks.push((idx, *line_num, content.clone(), indent));
}
if close_braces > 0 && !is_multiline_fn_end(content) {
while let Some((_, _, _, block_indent)) = open_blocks.last() {
if indent <= *block_indent && !is_block_opener(content) {
open_blocks.pop();
} else {
break;
}
}
}
}
let current_indent = lines
.get(scroll_position)
.map(|(_, c)| get_indentation(c))
.unwrap_or(0);
let sticky_lines: Vec<StickyLine> = open_blocks
.into_iter()
.filter(|(_, _, _, indent)| *indent < current_indent || current_indent == 0)
.map(|(_, line_num, content, indent)| StickyLine {
line_number: line_num,
content,
indentation: indent,
})
.collect();
if sticky_lines.len() > config.max_lines {
sticky_lines.into_iter().take(config.max_lines).collect()
} else {
sticky_lines
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_lines() {
let config = StickyLinesConfig::default();
let lines: Vec<(usize, String)> = vec![];
assert!(compute_sticky_lines(&lines, 0, &config).is_empty());
}
#[test]
fn test_basic_function_scope() {
let config = StickyLinesConfig::default();
let lines: Vec<(usize, String)> = vec![
(1, "fn main() {".to_string()),
(2, " let x = 1;".to_string()),
(3, " let y = 2;".to_string()),
(4, " println!(\"hello\");".to_string()),
(5, "}".to_string()),
];
let sticky = compute_sticky_lines(&lines, 3, &config);
assert_eq!(sticky.len(), 1);
assert_eq!(sticky[0].content, "fn main() {");
}
#[test]
fn test_nested_scopes() {
let config = StickyLinesConfig::default();
let lines: Vec<(usize, String)> = vec![
(1, "impl Foo {".to_string()),
(2, " fn bar() {".to_string()),
(3, " if true {".to_string()),
(4, " let x = 1;".to_string()),
(5, " }".to_string()),
(6, " }".to_string()),
(7, "}".to_string()),
];
let sticky = compute_sticky_lines(&lines, 3, &config);
assert_eq!(sticky.len(), 3);
assert_eq!(sticky[0].content, "impl Foo {");
assert_eq!(sticky[1].content, " fn bar() {");
assert_eq!(sticky[2].content, " if true {");
}
#[test]
fn test_closed_block_not_shown() {
let config = StickyLinesConfig::default();
let lines: Vec<(usize, String)> = vec![
(1, "fn main() {".to_string()),
(2, " if true {".to_string()),
(3, " let x = 1;".to_string()),
(4, " }".to_string()),
(5, " let y = 2;".to_string()),
(6, "}".to_string()),
];
let sticky = compute_sticky_lines(&lines, 4, &config);
assert_eq!(sticky.len(), 1);
assert_eq!(sticky[0].content, "fn main() {");
}
#[test]
fn test_multiline_function_definition() {
let config = StickyLinesConfig::default();
let lines: Vec<(usize, String)> = vec![
(1, "class MyClass {".to_string()),
(2, " private async syncFilesFromGitPayload(".to_string()),
(3, " payload: VMPayloadWithGit,".to_string()),
(4, " env: string | undefined,".to_string()),
(
5,
" options?: { skipSyncAndRuntime?: boolean },".to_string(),
),
(6, " ): Promise<Map<string, Buffer>> {".to_string()),
(7, " const { git } = payload;".to_string()),
(8, " return new Map();".to_string()),
(9, " }".to_string()),
(10, "}".to_string()),
];
let sticky = compute_sticky_lines(&lines, 6, &config);
assert_eq!(sticky.len(), 2);
assert_eq!(sticky[0].content, "class MyClass {");
assert_eq!(
sticky[1].content,
" private async syncFilesFromGitPayload("
);
}
}