Skip to content

refactor(picker_ui): split monolithic picker_ui.lua into focused submodules#564

Open
thuan1412 wants to merge 2 commits into
dmtrKovalenko:mainfrom
thuan1412:refactor/picker-ui-modules
Open

refactor(picker_ui): split monolithic picker_ui.lua into focused submodules#564
thuan1412 wants to merge 2 commits into
dmtrKovalenko:mainfrom
thuan1412:refactor/picker-ui-modules

Conversation

@thuan1412
Copy link
Copy Markdown
Contributor

@thuan1412 thuan1412 commented Jun 3, 2026

Break the ~2535-line picker_ui.lua into 8 modules under lua/fff/picker_ui/:

  • state_manager.lua: pure data store for all picker state
  • ui_creator.lua: buffers, windows, keymaps, autocmds
  • search_manager.lua: search execution, pagination, query history
  • renderer.lua: list rendering, combo separator, scrollbar, empty state
  • preview_manager.lua: preview debounce, title, update/clear
  • navigation.lua: cursor movement, pagination wrap, preview scrolling
  • layout_manager.lua: relayout on VimResized, close/cleanup
  • init.lua: coordinator wiring all submodules via init(P) pattern

Move picker_ui.lua to picker_ui/init.lua for clean module resolution.

Also removes unused scrollbar import in ui_creator.lua and need-check-nil diagnostic suppression comments in preview_manager.lua (nil was already handled by the guard check).

Resolve: #560

Screen.Recording.2026-06-03.at.20.40.14.mov

The code is AI generated. Below are plan generated by AI

Click to expand

Picker UI Refactoring Plan

Overview

Split the 2535-line picker_ui.lua into 8 smaller, focused modules under a picker_ui/ directory, following the existing pattern used by lua/fff/file_picker/.

Current State

  • File: lua/fff/picker_ui.lua
  • Lines: 2535
  • Target layout: All extracted modules live under lua/fff/picker_ui/
  • Problem: Monolithic file with multiple responsibilities, hard to maintain and test

Refactoring Strategy

All extracted modules live under lua/fff/picker_ui/ and are required via require('fff.picker_ui.<module>'). The top-level picker_ui.lua remains the public entry point — it coordinates the submodules and preserves the existing public API.

Module Directory Layout

lua/fff/picker_ui.lua        ← Main entry point (coordinator, preserved public API)
lua/fff/picker_ui/
├── state_manager.lua         ← Phase 1: State structure, selection, reset
├── ui_creator.lua            ← Phase 2: Window/buffer/keymap creation
├── search_manager.lua        ← Phase 3: Search execution, pagination
├── renderer.lua              ← Phase 4: List rendering, formatting
├── preview_manager.lua       ← Phase 5: Preview updates, debouncing
├── navigation.lua            ← Phase 6: Movement, mouse handling
└── layout_manager.lua        ← Phase 7: Resize, window cleanup

Phase 1: State Management Module

File: lua/fff/picker_ui/state_manager.lua
Lines to extract: 41-100, 1946-2200 (selection functions, close reset logic)
Content:

  • State structure definitions
  • Selection management (toggle_select, send_to_quickfix)
  • State reset functions (reset_state, reset_history_state)
  • Selection helpers (clear_selections, get_selected_items)

Public API:

  • M.reset_state() - Complete state reset
  • M.clear_selections() - Clear all selections
  • M.toggle_selection() - Toggle item selection
  • M.send_to_quickfix() - Send selections to quickfix
  • M.get_selected_items() - Get selected items

Phase 2: UI Creation Module

File: lua/fff/picker_ui/ui_creator.lua
Lines to extract: 140-380, 570-650 (focus functions)
Content:

  • Main UI creation (create_ui)
  • Buffer setup (setup_buffers)
  • Window configuration (setup_windows)
  • Keymap setup (setup_keymaps)
  • Focus management functions

Public API:

  • M.create_ui() - Main UI creation
  • M.setup_buffers() - Buffer configuration
  • M.setup_windows() - Window configuration
  • M.setup_keymaps() - Keybinding setup
  • M.focus_list_win() - Focus list window
  • M.focus_preview_win() - Focus preview window

Phase 3: Search and Results Module

File: lua/fff/picker_ui/search_manager.lua
Lines to extract: 650-950, 1700-1850 (search and pagination logic)
Content:

  • Search execution (update_results, update_results_sync)
  • Pagination functions (load_page_at_index, load_next_page)
  • Search coordination and result handling
  • Cross-mode suggestion logic

Public API:

  • M.perform_search() - Execute search
  • M.load_page_at_index() - Load specific page
  • M.load_next_page() - Load next page
  • M.load_previous_page() - Load previous page
  • M.get_current_results() - Get current results

Phase 4: Rendering System Module

File: lua/fff/picker_ui/renderer.lua
Lines to extract: 950-1400
Content:

  • Main list rendering (render_list)
  • Rendering context building (build_render_context)
  • Rendering finalization (finalize_render)
  • File display formatting (format_file_display)
  • Empty state rendering (render_grep_empty_state)

Public API:

  • M.render_filtered_items() - Render filtered items
  • M.build_render_context() - Build rendering context
  • M.format_file_display() - Format file display
  • M.render_grep_empty_state() - Render empty state

Phase 5: Preview System Module

File: lua/fff/picker_ui/preview_manager.lua
Lines to extract: 1400-1700, 1850-1950 (preview functions)
Content:

  • Preview updates (update_preview, update_preview_smart)
  • Debounced preview updates (update_preview_debounced)
  • Preview title management (update_preview_title)
  • Preview cleanup (clear_preview)
  • Location-based preview logic

Public API:

  • M.update_preview() - Update preview
  • M.update_preview_smart() - Smart preview update
  • M.update_preview_debounced() - Debounced preview update
  • M.clear_preview() - Clear preview
  • M.update_preview_title() - Update preview title

Phase 6: Navigation Module

File: lua/fff/picker_ui/navigation.lua
Lines to extract: 1950-2100, 1700-1850 (movement functions)
Content:

  • Core navigation (move_up, move_down)
  • Wrapping logic (wrap_to_first, wrap_to_last)
  • Pagination-aware movement
  • UI state management during navigation
  • Mouse click handling

Public API:

  • M.navigate_up() - Navigate up
  • M.navigate_down() - Navigate down
  • M.wrap_to_first() - Wrap to first item
  • M.wrap_to_last() - Wrap to last item
  • M.handle_mouse_click() - Handle mouse clicks

Phase 7: Layout and Window Module

File: lua/fff/picker_ui/layout_manager.lua
Lines to extract: 2200-2400
Content:

  • Dynamic layout management (relayout)
  • Window cleanup functions
  • Layout computation helpers
  • Window resize handling
  • Window management utilities

Public API:

  • M.handle_resize() - Handle window resize
  • M.cleanup_windows() - Cleanup windows
  • M.recalculate_layout() - Recalculate layout
  • M.close_preview() - Close preview window

Phase 8: Main Module Refactoring

File: lua/fff/picker_ui.lua (update existing file)
Changes:

  • Import and coordinate all extracted submodules from lua/fff/picker_ui/
  • Maintain existing public API
  • Remove duplicated code (state definition, selection logic, etc.)
  • Add module coordination logic

Public API (preserved):

  • M.open(opts) - Main entry point
  • M.open_with_callback(query, callback, opts) - Callback-based opening
  • M.close() - Cleanup and close
  • M.relayout() - Handle window resizing
  • M.toggle_debug() - Toggle debug mode
  • M.cycle_grep_modes() - Cycle through grep search modes
  • All existing navigation, selection, and preview functions

Key Principles

1. Preserve Public API

  • All existing functions (M.open, M.close, etc.) remain unchanged
  • Function signatures and behavior preserved
  • Backward compatibility maintained

2. Module Cohesion

  • Each module has a single, clear responsibility
  • Related functionality grouped together
  • Clear interfaces between modules

3. Testability

  • Each module can be tested independently
  • Reduced coupling between components
  • Easier to write unit tests

4. Maintainability

  • Smaller files are easier to understand
  • Clear separation of concerns
  • Easier to modify and extend

5. Gradual Migration

  • Phase-based approach minimizes risk
  • Each phase can be verified independently
  • Rollback possible if issues arise

Implementation Timeline with Verification

Week 1: Foundation

  • Phase 1: State Management Module
    • Extract state management code
    • Verification: Unit tests for all state functions
    • Verification: Integration test with existing picker
  • Phase 2: UI Creation Module
    • Extract UI creation code
    • Verification: UI creation test without full picker
    • Verification: Keymap setup test
  • Integration Testing: Basic picker functionality

Week 2: Core Functionality

  • Phase 3: Search and Results Module
    • Extract search and pagination logic
    • Verification: Search functionality unit tests
    • Verification: Pagination edge case tests
  • Phase 4: Rendering System Module
    • Extract rendering logic
    • Verification: Rendering unit tests
    • Verification: Visual rendering tests
  • Integration Testing: Full search and render workflow

Week 3: Advanced Features

  • Phase 5: Preview System Module
    • Extract preview functionality
    • Verification: Preview update tests
    • Verification: Preview debounce tests
  • Phase 6: Navigation Module
    • Extract navigation logic
    • Verification: Navigation unit tests
    • Verification: Mouse interaction tests
  • Integration Testing: Complete user interaction workflow

Week 4: Final Integration

  • Phase 7: Layout and Window Module
    • Extract layout management
    • Verification: Layout resize tests
    • Verification: Window cleanup tests
  • Phase 8: Main Module Refactoring
    • Update main module to coordinate extracted modules
    • Verification: Full end-to-end tests
    • Verification: Performance regression testing
  • Comprehensive Testing: All functionality verification

Verification Strategy

Verification Types

1. Unit Testing

  • Purpose: Test individual functions in isolation
  • Scope: Each module's public API functions
  • Tools: Lua unit testing framework (e.g., busted, luassert)
  • Coverage: 100% of public API functions

2. Integration Testing

  • Purpose: Test module interactions
  • Scope: Cross-module function calls
  • Tools: Test environment with multiple modules loaded
  • Coverage: All module-to-module interactions

3. End-to-End Testing

  • Purpose: Test complete user workflows
  • Scope: Full picker functionality from open to close
  • Tools: Neovim test environment or automated UI testing
  • Coverage: All user interactions and features

4. Performance Testing

  • Purpose: Ensure no performance regression
  • Scope: Search speed, rendering performance, memory usage
  • Tools: Neovim profiling, timing benchmarks
  • Coverage: Critical performance paths

Phase-by-Phase Verification Steps

Phase 1: State Management Module

Verification Checklist:

  • State structure initialization works correctly
  • reset_state() clears all state properly
  • toggle_selection() toggles file/grep selections
  • send_to_quickfix() builds correct quickfix lists
  • State persistence across picker operations
  • Edge cases (empty state, large selections)

Test Cases:

-- State reset test
local state = require('fff.picker_ui.state_manager')
state.reset_state()
assert(state.state.active == false)
assert(next(state.state.selected_files) == nil)

-- Selection toggle test
state.state.filtered_items = {{relative_path = "test.lua"}}
state.state.mode = 'files'
state.toggle_selection()
assert(state.state.selected_files["test.lua"] == true)
state.toggle_selection()
assert(state.state.selected_files["test.lua"] == nil)

Phase 2: UI Creation Module

Verification Checklist:

  • create_ui() creates all windows and buffers
  • Buffer configurations are correct (buftype, filetype)
  • Window options are set properly
  • Keymaps are registered correctly
  • Focus management works
  • No memory leaks in window/buffer creation

Test Cases:

-- UI creation test
local ui = require('fff.picker_ui.ui_creator')
local state = require('fff.picker_ui.state_manager')
state.state.config = test_config
local success = ui.create_ui()
assert(success == true)
assert(state.state.input_win ~= nil)
assert(state.state.list_win ~= nil)

Phase 3: Search and Results Module

Verification Checklist:

  • Search execution with various queries
  • Pagination works correctly (next/previous page)
  • Large result set handling
  • Error handling for invalid searches
  • Search result filtering and sorting
  • Cross-mode suggestions work

Test Cases:

-- Search test
local search = require('fff.picker_ui.search_manager')
state.state.query = "test"
state.state.config = test_config
local results = search.perform_search()
assert(#results > 0)
assert(state.state.filtered_items == results)

-- Pagination test
search.load_next_page()
assert(state.state.pagination.page_index == 1)

Phase 4: Rendering System Module

Verification Checklist:

  • List rendering works with various data
  • File display formatting is correct
  • Empty state rendering works
  • Performance with large lists
  • Syntax highlighting integration
  • Scrollbar rendering

Test Cases:

-- Rendering test
local renderer = require('fff.picker_ui.renderer')
state.state.filtered_items = test_items
state.state.cursor = 1
local render_context = renderer.build_render_context()
renderer.render_filtered_items(render_context)
-- Verify visual output matches expectations

Phase 5: Preview System Module

Verification Checklist:

  • Preview updates when item changes
  • Debounced preview works correctly
  • Preview title updates
  • Large file preview handling
  • Error handling for invalid files
  • Preview cleanup works

Test Cases:

-- Preview update test
local preview = require('fff.picker_ui.preview_manager')
state.state.filtered_items = {{relative_path = "test.lua", line_number = 10}}
state.state.cursor = 1
preview.update_preview()
-- Verify preview content is correct

Phase 6: Navigation Module

Verification Checklist:

  • Up/down navigation works
  • Wrapping logic works (first/last item)
  • Mouse click handling
  • Keyboard navigation
  • Pagination-aware navigation
  • Cursor position updates

Test Cases:

-- Navigation test
local nav = require('fff.picker_ui.navigation')
state.state.filtered_items = test_items
state.state.cursor = 1
nav.navigate_down()
assert(state.state.cursor == 2)
nav.navigate_down()
-- Test wrapping if enabled

Phase 7: Layout and Window Module

Verification Checklist:

  • Layout recalculation on resize
  • Window cleanup works
  • Preview window management
  • Responsive layout changes
  • Window focus handling
  • Edge cases (tiny terminal, large terminal)

Test Cases:

-- Layout resize test
local layout = require('fff.picker_ui.layout_manager')
state.state.config = test_config
layout.handle_resize()
-- Verify layout adjusts correctly

Phase 8: Main Module Refactoring

Verification Checklist:

  • All public API functions work
  • Integration between modules works
  • No circular dependencies
  • Performance no worse than original
  • All existing features preserved
  • Error handling works correctly

Test Cases:

-- Full integration test
local picker = require('fff.picker_ui')
local success = picker.open(test_config)
assert(success == true)
-- Test all features: search, navigate, select, preview, close

Automated Testing Framework

Test Structure

tests/
├── unit/
│   ├── picker_ui/
│   │   ├── state_manager_spec.lua
│   │   ├── ui_creator_spec.lua
│   │   ├── search_manager_spec.lua
│   │   ├── renderer_spec.lua
│   │   ├── preview_manager_spec.lua
│   │   ├── navigation_spec.lua
│   │   └── layout_manager_spec.lua
├── integration/
│   ├── picker_workflow_spec.lua
│   ├── module_interaction_spec.lua
│   └── search_render_spec.lua
└── e2e/
    ├── full_picker_test.lua
    ├── performance_test.lua
    └── edge_cases_test.lua

Test Commands

# Unit tests
make test-unit

# Integration tests
make test-integration

# End-to-end tests
make test-e2e

# Performance tests
make test-performance

# All tests
make test-all

Continuous Integration

Pre-Merge Checks

  • All unit tests pass
  • All integration tests pass
  • Performance benchmarks meet requirements
  • Code coverage maintained above 80%
  • No new memory leaks

Quality Gates

  • Test Coverage: Minimum 80% line coverage
  • Performance: No more than 5% slowdown in critical paths
  • Memory Usage: No memory leaks during normal operation
  • Error Handling: All edge cases covered

Rollback Strategy

If Verification Fails

  1. Immediate Rollback: Revert to previous working state
  2. Root Cause Analysis: Identify why verification failed
  3. Fix Implementation: Address the issue
  4. Re-run Verification: Ensure fix works
  5. Continue Phase: Proceed with confidence

Automated Rollback Scripts

# Rollback last phase
make rollback-phase N=1

# Full rollback to original
make rollback-original

Risk Mitigation

High-Risk Areas

  1. Search and Results Module - Complex logic, many dependencies
  2. Navigation Module - User interaction, state management
  3. Main Module Integration - Coordination between modules

Mitigation Strategies

  1. Incremental Changes - Implement one phase at a time
  2. Testing - Comprehensive testing after each phase
  3. Backward Compatibility - Preserve existing API throughout
  4. Documentation - Update documentation as modules are created

Success Metrics

Code Quality Metrics

  • Lines of Code: 2535 → ~200-300 per module
  • Cohesion: High (single responsibility per module)
  • Coupling: Low (clear interfaces between modules)
  • Complexity: Reduced (smaller, focused functions)

Maintainability Metrics

  • Time to Understand: Reduced (smaller files)
  • Bug Fix Time: Reduced (isolated components)
  • Feature Addition: Easier (modular design)
  • Testing Coverage: Improved (independent modules)

Team Collaboration

  • Parallel Development: Multiple developers can work on different modules
  • Code Reviews: Easier to review smaller, focused modules
  • Knowledge Sharing: Clear boundaries between modules
  • Onboarding: Easier to understand specific areas

Post-Refactoring Activities

Documentation

  • Update API documentation for each module
  • Create module-level documentation
  • Update integration guides

Testing

  • Add comprehensive unit tests for each module
  • Add integration tests for module interactions
  • Performance testing to ensure no regression

Monitoring

  • Monitor for performance regressions
  • Monitor for new bug patterns
  • Collect feedback from team members

Conclusion

This refactoring plan transforms a monolithic 2535-line file into 8 focused modules of 200-300 lines each. The result will be:

  • More maintainable code with clear separation of concerns
  • Easier testing with independent modules
  • Better team collaboration with parallel development opportunities
  • Preserved functionality with full backward compatibility

The phase-based approach ensures manageable risk while delivering significant improvements in code organization and maintainability.

…odules

Break the ~2535-line picker_ui.lua into 8 modules under lua/fff/picker_ui/:
- state_manager.lua: pure data store for all picker state
- ui_creator.lua: buffers, windows, keymaps, autocmds
- search_manager.lua: search execution, pagination, query history
- renderer.lua: list rendering, combo separator, scrollbar, empty state
- preview_manager.lua: preview debounce, title, update/clear
- navigation.lua: cursor movement, pagination wrap, preview scrolling
- layout_manager.lua: relayout on VimResized, close/cleanup
- init.lua: coordinator wiring all submodules via init(P) pattern

Move picker_ui.lua to picker_ui/init.lua for clean module resolution.

Also removes unused scrollbar import in ui_creator.lua and need-check-nil
diagnostic suppression comments in preview_manager.lua (nil was already
handled by the guard check).
@thuan1412 thuan1412 force-pushed the refactor/picker-ui-modules branch from a8eccf3 to 9a89cad Compare June 3, 2026 13:46
@thuan1412 thuan1412 marked this pull request as ready for review June 3, 2026 14:08
@thuan1412
Copy link
Copy Markdown
Contributor Author

CI has passed. Should we YOLO and merge it @dmtrKovalenko ? 😆 Otherwise, the PR will get conflict when change is made to to picker_ui.lua

@dmtrKovalenko
Copy link
Copy Markdown
Owner

let's do a few changes to it if you don't mind, I can do them myself:

  1. I don't like _manager suffix, also let's rename state_mangaer.lua to picker_ui_state.lua
    1.a for my own sanity let's rename picker_ui/init.lua to picker_ui/picker_ui.lua as I have it in mind as picker_ui.lua
  2. we already have layout.lua file let's put everything from layout_manager there same with preview.lua

I don't think the files can be called managers - if you wish to call them like this you should restructure them to actually keep the state inside. I would say that all the functions from the layout_manager and preview_manager should be splitted between the preview.lua/layout.lua and state_manager.lua

  1. Please remove the -------------------------- diviers between the sections for me they are just visual noise
  2. Now reading the state_manager more I am not sure what is the purpose of it? If we store there all the state I expect the functions there to be working with state, why it has send_to_quickfix this kind of small functionality related to neovim internal apis could be probably living in nvim_utils separately
  3. we should also move list_renderer, combo_renderer, grep_renderer, and file_renderer in this folder if we do the restructure

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restructure picker_ui.lua

2 participants