Softwrapping improvements (#5893)

* use max_line_width + 1 during softwrap to account for newline char

Helix softwrap implementation always wraps lines so that the newline
character doesn't get cut off so he line wraps one chars earlier then
in other editors. This is necessary, because newline chars are always
selecatble in helix and must never be hidden.

However That means that `max_line_width` currently wraps one char
earlier than expected. The typical definition of line width does not
include the newline character and other helix commands like `:reflow`
also don't count the newline character here.

This commit makes softwrap use `max_line_width + 1` instead of
`max_line_width` to correct the impedance missmatch.

* fix typos

Co-authored-by: Jonathan Lebon <jonathan@jlebon.com>

* Add text-width to config.toml

* text-width: update setting documentation

* rename leftover config item

* remove leftover max-line-length occurrences

* Make `text-width` optional in editor config

When it was only used for `:reflow` it made sense to have a default
value set to `80`, but now that soft-wrapping uses this setting, keeping
a default set to `80` would make soft-wrapping behave more aggressively.

* Allow softwrapping to ignore `text-width`

Softwrapping wraps by default to the viewport width or a configured
`text-width` (whichever's smaller). In some cases we only want to set
`text-width` to use for hard-wrapping and let longer lines flow if they
have enough space. This setting allows that.

* Revert "Make `text-width` optional in editor config"

This reverts commit b247d526d6.

* soft-wrap: allow per-language overrides

* Update book/src/configuration.md

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Update book/src/languages.md

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Update book/src/configuration.md

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

---------

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
Co-authored-by: Jonathan Lebon <jonathan@jlebon.com>
Co-authored-by: Alex Boehm <alexb@ozrunways.com>
Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
pull/5465/merge
Clément Delafargue 2 years ago committed by GitHub
parent f4bdbe4674
commit 8dd1ab4899
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -55,6 +55,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` | | `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | | `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` |
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | | `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` |
### `[editor.statusline]` Section ### `[editor.statusline]` Section
@ -314,12 +315,13 @@ Currently unused
Options for soft wrapping lines that exceed the view width: Options for soft wrapping lines that exceed the view width:
| Key | Description | Default | | Key | Description | Default |
| --- | --- | --- | | --- | --- | --- |
| `enable` | Whether soft wrapping is enabled | `false` | | `enable` | Whether soft wrapping is enabled. | `false` |
| `max-wrap` | Maximum free space left at the end of the line | `20` | | `max-wrap` | Maximum free space left at the end of the line. | `20` |
| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line | `40` | | `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` |
| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` | | `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` |
| `wrap-at-text-width` | Soft wrap at `text-width` instead of using the full viewport size. | `false` |
Example: Example:

@ -63,7 +63,7 @@ These configuration keys are available:
| `config` | Language Server configuration | | `config` | Language Server configuration |
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
| `max-line-length` | Maximum line length. Used for the `:reflow` command and soft-wrapping | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` |
### File-type detection and the `file-types` key ### File-type detection and the `file-types` key

@ -82,7 +82,8 @@ pub struct LanguageConfiguration {
pub shebangs: Vec<String>, // interpreter(s) associated with language pub shebangs: Vec<String>, // interpreter(s) associated with language
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml> pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
pub comment_token: Option<String>, pub comment_token: Option<String>,
pub max_line_length: Option<usize>, pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>,
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")] #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>, pub config: Option<serde_json::Value>,
@ -546,6 +547,33 @@ impl LanguageConfiguration {
.ok() .ok()
} }
} }
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct SoftWrap {
/// Soft wrap lines that exceed viewport width. Default to off
pub enable: Option<bool>,
/// Maximum space left free at the end of the line.
/// This space is used to wrap text at word boundaries. If that is not possible within this limit
/// the word is simply split at the end of the line.
///
/// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
///
/// Default to 20
pub max_wrap: Option<u16>,
/// Maximum number of indentation that can be carried over from the previous line when softwrapping.
/// If a line is indented further then this limit it is rendered at the start of the viewport instead.
///
/// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
///
/// Default to 40
pub max_indent_retain: Option<u16>,
/// Indicator placed at the beginning of softwrapped lines
///
/// Defaults to ↪
pub wrap_indicator: Option<String>,
/// Softwrap at `text_width` instead of viewport width if it is shorter
pub wrap_at_text_width: Option<bool>,
}
// Expose loader as Lazy<> global since it's always static? // Expose loader as Lazy<> global since it's always static?

@ -2,6 +2,6 @@ use smartstring::{LazyCompact, SmartString};
/// Given a slice of text, return the text re-wrapped to fit it /// Given a slice of text, return the text re-wrapped to fit it
/// within the given width. /// within the given width.
pub fn reflow_hard_wrap(text: &str, max_line_len: usize) -> SmartString<LazyCompact> { pub fn reflow_hard_wrap(text: &str, text_width: usize) -> SmartString<LazyCompact> {
textwrap::refill(text, max_line_len).into() textwrap::refill(text, text_width).into()
} }

@ -1840,30 +1840,26 @@ fn reflow(
} }
let scrolloff = cx.editor.config().scrolloff; let scrolloff = cx.editor.config().scrolloff;
let cfg_text_width: usize = cx.editor.config().text_width;
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
const DEFAULT_MAX_LEN: usize = 79; // Find the text_width by checking the following sources in order:
// Find the max line length by checking the following sources in order:
// - The passed argument in `args` // - The passed argument in `args`
// - The configured max_line_len for this language in languages.toml // - The configured text-width for this language in languages.toml
// - The const default we set above // - The configured text-width in the config.toml
let max_line_len: usize = args let text_width: usize = args
.get(0) .get(0)
.map(|num| num.parse::<usize>()) .map(|num| num.parse::<usize>())
.transpose()? .transpose()?
.or_else(|| { .or_else(|| doc.language_config().and_then(|config| config.text_width))
doc.language_config() .unwrap_or(cfg_text_width);
.and_then(|config| config.max_line_length)
})
.unwrap_or(DEFAULT_MAX_LEN);
let rope = doc.text(); let rope = doc.text();
let selection = doc.selection(view.id); let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(rope, selection, |range| { let transaction = Transaction::change_by_selection(rope, selection, |range| {
let fragment = range.fragment(rope.slice(..)); let fragment = range.fragment(rope.slice(..));
let reflowed_text = helix_core::wrap::reflow_hard_wrap(&fragment, max_line_len); let reflowed_text = helix_core::wrap::reflow_hard_wrap(&fragment, text_width);
(range.from(), range.to(), Some(reflowed_text)) (range.from(), range.to(), Some(reflowed_text))
}); });

@ -1238,24 +1238,61 @@ impl Document {
} }
pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> TextFormat { pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> TextFormat {
if let Some(max_line_len) = self let config = self.config.load();
let text_width = self
.language_config() .language_config()
.and_then(|config| config.max_line_length) .and_then(|config| config.text_width)
{ .unwrap_or(config.text_width);
viewport_width = viewport_width.min(max_line_len as u16) let soft_wrap_at_text_width = self
.language_config()
.and_then(|config| {
config
.soft_wrap
.as_ref()
.and_then(|soft_wrap| soft_wrap.wrap_at_text_width)
})
.or(config.soft_wrap.wrap_at_text_width)
.unwrap_or(false);
if soft_wrap_at_text_width {
// We increase max_line_len by 1 because softwrap considers the newline character
// as part of the line length while the "typical" expectation is that this is not the case.
// In particular other commands like :reflow do not count the line terminator.
// This is technically inconsistent for the last line as that line never has a line terminator
// but having the last visual line exceed the width by 1 seems like a rare edge case.
viewport_width = viewport_width.min(text_width as u16 + 1)
} }
let config = self.config.load(); let config = self.config.load();
let soft_wrap = &config.soft_wrap; let editor_soft_wrap = &config.soft_wrap;
let language_soft_wrap = self
.language
.as_ref()
.and_then(|config| config.soft_wrap.as_ref());
let enable_soft_wrap = language_soft_wrap
.and_then(|soft_wrap| soft_wrap.enable)
.or(editor_soft_wrap.enable)
.unwrap_or(false);
let max_wrap = language_soft_wrap
.and_then(|soft_wrap| soft_wrap.max_wrap)
.or(config.soft_wrap.max_wrap)
.unwrap_or(20);
let max_indent_retain = language_soft_wrap
.and_then(|soft_wrap| soft_wrap.max_indent_retain)
.or(editor_soft_wrap.max_indent_retain)
.unwrap_or(40);
let wrap_indicator = language_soft_wrap
.and_then(|soft_wrap| soft_wrap.wrap_indicator.clone())
.or_else(|| config.soft_wrap.wrap_indicator.clone())
.unwrap_or_else(|| "↪ ".into());
let tab_width = self.tab_width() as u16; let tab_width = self.tab_width() as u16;
TextFormat { TextFormat {
soft_wrap: soft_wrap.enable && viewport_width > 10, soft_wrap: enable_soft_wrap && viewport_width > 10,
tab_width, tab_width,
max_wrap: soft_wrap.max_wrap.min(viewport_width / 4), max_wrap: max_wrap.min(viewport_width / 4),
max_indent_retain: soft_wrap.max_indent_retain.min(viewport_width * 2 / 5), max_indent_retain: max_indent_retain.min(viewport_width * 2 / 5),
// avoid spinning forever when the window manager // avoid spinning forever when the window manager
// sets the size to something tiny // sets the size to something tiny
viewport_width, viewport_width,
wrap_indicator: soft_wrap.wrap_indicator.clone().into_boxed_str(), wrap_indicator: wrap_indicator.into_boxed_str(),
wrap_indicator_highlight: theme wrap_indicator_highlight: theme
.and_then(|theme| theme.find_scope_index("ui.virtual.wrap")) .and_then(|theme| theme.find_scope_index("ui.virtual.wrap"))
.map(Highlight), .map(Highlight),

@ -42,7 +42,7 @@ pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers; pub use helix_core::register::Registers;
use helix_core::{ use helix_core::{
auto_pairs::AutoPairs, auto_pairs::AutoPairs,
syntax::{self, AutoPairConfig}, syntax::{self, AutoPairConfig, SoftWrap},
Change, Change,
}; };
use helix_core::{Position, Selection}; use helix_core::{Position, Selection};
@ -241,6 +241,8 @@ pub struct Config {
pub auto_format: bool, pub auto_format: bool,
/// Automatic save on focus lost. Defaults to false. /// Automatic save on focus lost. Defaults to false.
pub auto_save: bool, pub auto_save: bool,
/// Set a global text_width
pub text_width: usize,
/// Time in milliseconds since last keypress before idle timers trigger. /// Time in milliseconds since last keypress before idle timers trigger.
/// Used for autocompletion, set to 0 for instant. Defaults to 400ms. /// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
#[serde( #[serde(
@ -276,43 +278,6 @@ pub struct Config {
pub soft_wrap: SoftWrap, pub soft_wrap: SoftWrap,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct SoftWrap {
/// Soft wrap lines that exceed viewport width. Default to off
pub enable: bool,
/// Maximum space left free at the end of the line.
/// This space is used to wrap text at word boundaries. If that is not possible within this limit
/// the word is simply split at the end of the line.
///
/// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
///
/// Default to 20
pub max_wrap: u16,
/// Maximum number of indentation that can be carried over from the previous line when softwrapping.
/// If a line is indented further then this limit it is rendered at the start of the viewport instead.
///
/// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
///
/// Default to 40
pub max_indent_retain: u16,
/// Indicator placed at the beginning of softwrapped lines
///
/// Defaults to ↪
pub wrap_indicator: String,
}
impl Default for SoftWrap {
fn default() -> Self {
SoftWrap {
enable: false,
max_wrap: 20,
max_indent_retain: 40,
wrap_indicator: "↪ ".into(),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct TerminalConfig { pub struct TerminalConfig {
@ -772,6 +737,7 @@ impl Default for Config {
indent_guides: IndentGuidesConfig::default(), indent_guides: IndentGuidesConfig::default(),
color_modes: false, color_modes: false,
soft_wrap: SoftWrap::default(), soft_wrap: SoftWrap::default(),
text_width: 80,
} }
} }
} }

@ -1104,7 +1104,7 @@ file-types = ["COMMIT_EDITMSG"]
comment-token = "#" comment-token = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
rulers = [50, 72] rulers = [50, 72]
max-line-length = 72 text-width = 72
[[grammar]] [[grammar]]
name = "git-commit" name = "git-commit"

Loading…
Cancel
Save