Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

### Features

* **Multi-LSP Server Support**: Configure multiple LSP servers per language (e.g., pylsp + pyright for Python). Servers are routed by feature using `only_features`/`except_features` filters, completions are merged from all eligible servers, and diagnostics are tracked per-server. Per-server status is shown in the status bar.
* **Multi-LSP Server Support**: Configure multiple LSP servers per language (e.g., pylsp + pyright for Python). Servers are routed by feature using `only_features`/`except_features` filters, completions are merged from all eligible servers, and diagnostics are tracked per-server. Per-server status is shown in the status bar (#971).

* **Per-Language Editor Settings**: `line_wrap`, `wrap_column`, `page_view`, and `page_width` can now be configured per-language. For example, wrap Markdown at 80 columns while keeping code unwrapped (#1371).

* **Settings Deep Search**: Search now walks into Map entries, TextList items, and nested JSON values. Searching "python" finds the "python" key in language/LSP maps. Results show hierarchical breadcrumbs (e.g., "Languages > python") and auto-focus the matching entry.

* **Diff Chunk Navigation Plugin**: New built-in plugin for navigating between diff chunks, merging git and saved-diff sources.

### Improvements
Expand All @@ -18,6 +16,8 @@

* **Settings UI Overhaul**: Modernized visual design with wider modal (160 cols), rounded corner borders, Nerd Font category icons, styled `[✓]` toggles, and reverse-video key hints. Keyboard navigation rewritten: Tab cycles sequentially through all fields and buttons, composite controls (Map, ObjectArray, TextList) support internal navigation, entry dialogs have section headers with explicit field ordering, PageDown/PageUp work in the main panel, and TextList edits auto-accept on navigation. Focus indicator now highlights per-row in composite controls.

* **Settings Deep Search**: Also in the Settings UI: Search now walks into Map entries, TextList items, and nested JSON values. Searching "python" finds the "python" key in language/LSP maps. Results show hierarchical breadcrumbs (e.g., "Languages > python") and auto-focus the matching entry.

* **Per-Language Workspace Root Detection**: New `root_markers` field on LSP server configs. The editor walks upward from the file's directory looking for configured markers (e.g., `Cargo.toml`, `package.json`), replacing the old cwd-based root (#1360).

* **Page View Mode**: "Compose" mode renamed to "Page View". Can now auto-activate per language via `page_view: true` in language config. Old keybinding names continue to work.
Expand Down Expand Up @@ -58,8 +58,6 @@

* Fixed large file recovery saving the entire file as individual chunks instead of using the recovery format.

* Fixed `file://` URI handling silently dropping `..` path components, which could cause LSP root mismatches.

* Fixed read-only detection for files not owned by the current user (now checks effective uid/gid instead of file mode bits).

## 0.2.18
Expand Down
4 changes: 4 additions & 0 deletions crates/fresh-editor/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,10 @@
"cmd.toggle_fold_desc": "Collapse or expand the fold at the cursor",
"cmd.toggle_line_wrap": "Toggle Line Wrap",
"cmd.toggle_line_wrap_desc": "Enable or disable line wrapping in the editor",
"cmd.toggle_page_view": "Toggle Page View",
"cmd.toggle_page_view_desc": "Toggle narrow page view (compose) mode for the current buffer",
"cmd.set_page_width": "Set Page Width",
"cmd.set_page_width_desc": "Set the narrow page width for page view mode",
"cmd.toggle_read_only": "Toggle Read-Only Mode",
"cmd.toggle_read_only_desc": "Enable or disable read-only mode for the current buffer",
"cmd.toggle_maximize_split": "Toggle Maximize Split",
Expand Down
14 changes: 13 additions & 1 deletion crates/fresh-editor/plugins/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"line_wrap": true,
"wrap_indent": true,
"wrap_column": null,
"page_width": 80,
"syntax_highlighting": true,
"show_menu_bar": true,
"menu_bar_mnemonics": true,
Expand Down Expand Up @@ -273,6 +274,17 @@
"default": null,
"x-section": "Display"
},
"page_width": {
"description": "Width of the page in page view mode (in columns).\nControls the content width when page view is active, with centering margins.\nDefaults to 80. Set to `null` to use the full viewport width.",
"type": [
"integer",
"null"
],
"format": "uint",
"minimum": 0,
"default": 80,
"x-section": "Display"
},
"syntax_highlighting": {
"description": "Enable syntax highlighting for code files",
"type": "boolean",
Expand Down Expand Up @@ -957,7 +969,7 @@
"default": null
},
"page_width": {
"description": "Width of the page in page view mode (in columns).\nControls the content width when page view is active, with centering margins.\nIf not specified (`null`), uses the viewport width.",
"description": "Width of the page in page view mode (in columns).\nControls the content width when page view is active, with centering margins.\nIf not specified (`null`), falls back to the global `editor.page_width` setting.",
"type": [
"integer",
"null"
Expand Down
2 changes: 1 addition & 1 deletion crates/fresh-editor/src/app/buffer_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
let state = self.buffers.get(&buffer_id)?;
let lang_config = self.config.languages.get(&state.language)?;
if lang_config.page_view == Some(true) {
Some(lang_config.page_width)
Some(lang_config.page_width.or(self.config.editor.page_width))
} else {
None
}
Expand Down Expand Up @@ -2491,16 +2491,16 @@
}
}

pub fn queue_file_open(
&mut self,
path: PathBuf,
line: Option<usize>,
column: Option<usize>,
end_line: Option<usize>,
end_column: Option<usize>,
message: Option<String>,
wait_id: Option<u64>,
) {

Check warning on line 2503 in crates/fresh-editor/src/app/buffer_management.rs

View workflow job for this annotation

GitHub Actions / clippy

this function has too many arguments (8/7)

warning: this function has too many arguments (8/7) --> crates/fresh-editor/src/app/buffer_management.rs:2494:5 | 2494 | / pub fn queue_file_open( 2495 | | &mut self, 2496 | | path: PathBuf, 2497 | | line: Option<usize>, ... | 2502 | | wait_id: Option<u64>, 2503 | | ) { | |_____^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.92.0/index.html#too_many_arguments

Check warning on line 2503 in crates/fresh-editor/src/app/buffer_management.rs

View workflow job for this annotation

GitHub Actions / clippy

this function has too many arguments (8/7)

warning: this function has too many arguments (8/7) --> crates/fresh-editor/src/app/buffer_management.rs:2494:5 | 2494 | / pub fn queue_file_open( 2495 | | &mut self, 2496 | | path: PathBuf, 2497 | | line: Option<usize>, ... | 2502 | | wait_id: Option<u64>, 2503 | | ) { | |_____^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.92.0/index.html#too_many_arguments
self.pending_file_opens.push(super::PendingFileOpen {
path,
line,
Expand Down
3 changes: 2 additions & 1 deletion crates/fresh-editor/src/app/view_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ impl Editor {
.buffers
.get(&active_buffer)
.and_then(|s| self.config.languages.get(&s.language))
.and_then(|lc| lc.page_width);
.and_then(|lc| lc.page_width)
.or(self.config.editor.page_width);

let view_mode = {
let current = self
Expand Down
14 changes: 13 additions & 1 deletion crates/fresh-editor/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,13 @@ pub struct EditorConfig {
#[schemars(extend("x-section" = "Display"))]
pub wrap_column: Option<usize>,

/// Width of the page in page view mode (in columns).
/// Controls the content width when page view is active, with centering margins.
/// Defaults to 80. Set to `null` to use the full viewport width.
#[serde(default = "default_page_width")]
#[schemars(extend("x-section" = "Display"))]
pub page_width: Option<usize>,

/// Enable syntax highlighting for code files
#[serde(default = "default_true")]
#[schemars(extend("x-section" = "Display"))]
Expand Down Expand Up @@ -1125,6 +1132,7 @@ impl Default for EditorConfig {
line_wrap: true,
wrap_indent: true,
wrap_column: None,
page_width: default_page_width(),
highlight_timeout_ms: default_highlight_timeout(),
snapshot_interval: default_snapshot_interval(),
large_file_threshold_bytes: default_large_file_threshold(),
Expand Down Expand Up @@ -1426,6 +1434,10 @@ fn default_on_save_timeout() -> u64 {
10000
}

fn default_page_width() -> Option<usize> {
Some(80)
}

/// Language-specific configuration
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[schemars(extend("x-display-field" = "/grammar"))]
Expand Down Expand Up @@ -1495,7 +1507,7 @@ pub struct LanguageConfig {

/// Width of the page in page view mode (in columns).
/// Controls the content width when page view is active, with centering margins.
/// If not specified (`null`), uses the viewport width.
/// If not specified (`null`), falls back to the global `editor.page_width` setting.
#[serde(default)]
pub page_width: Option<usize>,

Expand Down
99 changes: 46 additions & 53 deletions crates/fresh-editor/src/input/command_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,83 +253,76 @@ impl CommandRegistry {
(suggestion, history_pos, score)
};

// First, try to match by name only
// Match by name or description
// Commands with unmet custom contexts are completely hidden
let mut suggestions: Vec<(Suggestion, Option<usize>, i32)> = commands
// match_kind: 0 = name match, 1 = description match
let mut suggestions: Vec<(Suggestion, Option<usize>, i32, u8)> = commands
.iter()
.filter(|cmd| is_visible(cmd))
.filter_map(|cmd| {
let localized_name = cmd.get_localized_name();
let name_result = fuzzy_match(query, &localized_name);
if name_result.matched {
let localized_desc = cmd.get_localized_description();
Some(make_suggestion(
cmd,
name_result.score,
localized_name,
localized_desc,
))
} else {
None
}
})
.collect();

// If no name matches found, try description matching as a fallback
if suggestions.is_empty() && !query.is_empty() {
suggestions = commands
.iter()
.filter(|cmd| is_visible(cmd))
.filter_map(|cmd| {
let (suggestion, hist, score) =
make_suggestion(cmd, name_result.score, localized_name, localized_desc);
Some((suggestion, hist, score, 0))
} else if !query.is_empty() {
let localized_desc = cmd.get_localized_description();
let desc_result = fuzzy_match(query, &localized_desc);
if desc_result.matched {
let localized_name = cmd.get_localized_name();
// Description matches get reduced score
Some(make_suggestion(
cmd,
desc_result.score.saturating_sub(50),
localized_name,
localized_desc,
))
let (suggestion, hist, score) =
make_suggestion(cmd, desc_result.score, localized_name, localized_desc);
Some((suggestion, hist, score, 1))
} else {
None
}
})
.collect();
}
} else {
None
}
})
.collect();

// Sort by:
// 1. Disabled status (enabled first)
// 2. Fuzzy match score (higher is better) - only when query is not empty
// 3. History position (recent first, then never-used alphabetically)
// 2. Match kind (name matches before description matches) - only when query is not empty
// 3. Fuzzy match score (higher is better) - only when query is not empty
// 4. History position (recent first, then never-used alphabetically)
let has_query = !query.is_empty();
suggestions.sort_by(|(a, a_hist, a_score), (b, b_hist, b_score)| {
// First sort by disabled status
match a.disabled.cmp(&b.disabled) {
std::cmp::Ordering::Equal => {}
other => return other,
}

// When there's a query, sort by fuzzy score (higher is better)
if has_query {
match b_score.cmp(a_score) {
suggestions.sort_by(
|(a, a_hist, a_score, a_kind), (b, b_hist, b_score, b_kind)| {
// First sort by disabled status
match a.disabled.cmp(&b.disabled) {
std::cmp::Ordering::Equal => {}
other => return other,
}
}

// Then sort by history position (lower = more recent = better)
match (a_hist, b_hist) {
(Some(a_pos), Some(b_pos)) => a_pos.cmp(b_pos),
(Some(_), None) => std::cmp::Ordering::Less, // In history beats not in history
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => a.text.cmp(&b.text), // Alphabetical for never-used commands
}
});
if has_query {
// Name matches before description matches
match a_kind.cmp(b_kind) {
std::cmp::Ordering::Equal => {}
other => return other,
}

// Within the same kind, sort by fuzzy score (higher is better)
match b_score.cmp(a_score) {
std::cmp::Ordering::Equal => {}
other => return other,
}
}

// Then sort by history position (lower = more recent = better)
match (a_hist, b_hist) {
(Some(a_pos), Some(b_pos)) => a_pos.cmp(b_pos),
(Some(_), None) => std::cmp::Ordering::Less, // In history beats not in history
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => a.text.cmp(&b.text), // Alphabetical for never-used commands
}
},
);

// Extract just the suggestions
suggestions.into_iter().map(|(s, _, _)| s).collect()
suggestions.into_iter().map(|(s, _, _, _)| s).collect()
}

/// Get count of registered plugin commands
Expand Down
14 changes: 14 additions & 0 deletions crates/fresh-editor/src/input/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,20 @@ static COMMAND_DEFS: &[CommandDef] = &[
contexts: &[Normal],
custom_contexts: &[],
},
CommandDef {
name_key: "cmd.toggle_page_view",
desc_key: "cmd.toggle_page_view_desc",
action: || Action::TogglePageView,
contexts: &[Normal],
custom_contexts: &[],
},
CommandDef {
name_key: "cmd.set_page_width",
desc_key: "cmd.set_page_width_desc",
action: || Action::SetPageWidth,
contexts: &[Normal],
custom_contexts: &[],
},
CommandDef {
name_key: "cmd.toggle_read_only",
desc_key: "cmd.toggle_read_only_desc",
Expand Down
4 changes: 4 additions & 0 deletions crates/fresh-editor/src/partial_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ pub struct PartialEditorConfig {
pub line_wrap: Option<bool>,
pub wrap_indent: Option<bool>,
pub wrap_column: Option<Option<usize>>,
pub page_width: Option<Option<usize>>,
pub highlight_timeout_ms: Option<u64>,
pub snapshot_interval: Option<usize>,
pub large_file_threshold_bytes: Option<u64>,
Expand Down Expand Up @@ -218,6 +219,7 @@ impl Merge for PartialEditorConfig {
self.line_wrap.merge_from(&other.line_wrap);
self.wrap_indent.merge_from(&other.wrap_indent);
self.wrap_column.merge_from(&other.wrap_column);
self.page_width.merge_from(&other.page_width);
self.highlight_timeout_ms
.merge_from(&other.highlight_timeout_ms);
self.snapshot_interval.merge_from(&other.snapshot_interval);
Expand Down Expand Up @@ -481,6 +483,7 @@ impl From<&crate::config::EditorConfig> for PartialEditorConfig {
line_wrap: Some(cfg.line_wrap),
wrap_indent: Some(cfg.wrap_indent),
wrap_column: Some(cfg.wrap_column),
page_width: Some(cfg.page_width),
highlight_timeout_ms: Some(cfg.highlight_timeout_ms),
snapshot_interval: Some(cfg.snapshot_interval),
large_file_threshold_bytes: Some(cfg.large_file_threshold_bytes),
Expand Down Expand Up @@ -557,6 +560,7 @@ impl PartialEditorConfig {
line_wrap: self.line_wrap.unwrap_or(defaults.line_wrap),
wrap_indent: self.wrap_indent.unwrap_or(defaults.wrap_indent),
wrap_column: self.wrap_column.unwrap_or(defaults.wrap_column),
page_width: self.page_width.unwrap_or(defaults.page_width),
highlight_timeout_ms: self
.highlight_timeout_ms
.unwrap_or(defaults.highlight_timeout_ms),
Expand Down
21 changes: 21 additions & 0 deletions crates/fresh-editor/tests/e2e/command_palette.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,3 +1047,24 @@ fn test_command_palette_select_cursor_style() {
.wait_for_screen_contains("Cursor style changed")
.unwrap();
}

/// Test that command palette searches descriptions, not just names
#[test]
fn test_command_palette_description_search() {
use crossterm::event::{KeyCode, KeyModifiers};
let mut harness = EditorTestHarness::new(100, 24).unwrap();

// Trigger the command palette
harness
.send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
.unwrap();

// Type "narrow" which only appears in the description of page view commands,
// not in their names
harness.type_text("narrow").unwrap();
harness.render().unwrap();

// Should find commands whose descriptions match
harness.assert_screen_contains("Toggle Page View");
harness.assert_screen_contains("Set Page Width");
}
Loading
Loading