on master help sign in
Softwrapping improvements (#5893)
8dd1ab4Clément Delafargue
book/src/configuration.md+8−6
@@ -55,6 +55,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
55| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
55| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
56| `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` |
56| `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` |
57| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
57| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
58+| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` |
58
59
59### `[editor.statusline]` Section
60### `[editor.statusline]` Section
60
61
@@ -314,12 +315,13 @@ Currently unused
314
315
315Options for soft wrapping lines that exceed the view width:
316Options for soft wrapping lines that exceed the view width:
316
317
317−| Key | Description | Default |
318+| Key | Description | Default |
318−| --- | --- | --- |
319+| --- | --- | --- |
319−| `enable` | Whether soft wrapping is enabled | `false` |
320+| `enable` | Whether soft wrapping is enabled. | `false` |
320−| `max-wrap` | Maximum free space left at the end of the line | `20` |
321+| `max-wrap` | Maximum free space left at the end of the line. | `20` |
321−| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line | `40` |
322+| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` |
322−| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` |
323+| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` |
324+| `wrap-at-text-width` | Soft wrap at `text-width` instead of using the full viewport size. | `false` |
323
325
324Example:
326Example:
325
327
book/src/languages.md+1−1
@@ -63,7 +63,7 @@ These configuration keys are available:
63| `config` | Language Server configuration |
63| `config` | Language Server configuration |
64| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
64| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
65| `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 |
65| `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 |
66−| `max-line-length` | Maximum line length. Used for the `:reflow` command and soft-wrapping |
66+| `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` |
67
67
68### File-type detection and the `file-types` key
68### File-type detection and the `file-types` key
69
69
helix-core/src/syntax.rs+29−1
@@ -82,7 +82,8 @@ pub struct LanguageConfiguration {
82 pub shebangs: Vec<String>, // interpreter(s) associated with language
82 pub shebangs: Vec<String>, // interpreter(s) associated with language
83 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
83 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
84 pub comment_token: Option<String>,
84 pub comment_token: Option<String>,
85− pub max_line_length: Option<usize>,
85+ pub text_width: Option<usize>,
86+ pub soft_wrap: Option<SoftWrap>,
86
87
87 #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
88 #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
88 pub config: Option<serde_json::Value>,
89 pub config: Option<serde_json::Value>,
@@ -546,6 +547,33 @@ impl LanguageConfiguration {
546 .ok()
547 .ok()
547 }
548 }
548}
549}
550+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
551+#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
552+pub struct SoftWrap {
553+ /// Soft wrap lines that exceed viewport width. Default to off
554+ pub enable: Option<bool>,
555+ /// Maximum space left free at the end of the line.
556+ /// This space is used to wrap text at word boundaries. If that is not possible within this limit
557+ /// the word is simply split at the end of the line.
558+ ///
559+ /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
560+ ///
561+ /// Default to 20
562+ pub max_wrap: Option<u16>,
563+ /// Maximum number of indentation that can be carried over from the previous line when softwrapping.
564+ /// If a line is indented further then this limit it is rendered at the start of the viewport instead.
565+ ///
566+ /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
567+ ///
568+ /// Default to 40
569+ pub max_indent_retain: Option<u16>,
570+ /// Indicator placed at the beginning of softwrapped lines
571+ ///
572+ /// Defaults to ↪
573+ pub wrap_indicator: Option<String>,
574+ /// Softwrap at `text_width` instead of viewport width if it is shorter
575+ pub wrap_at_text_width: Option<bool>,
576+}
549
577
550// Expose loader as Lazy<> global since it's always static?
578// Expose loader as Lazy<> global since it's always static?
551
579
helix-core/src/wrap.rs+2−2
@@ -2,6 +2,6 @@ use smartstring::{LazyCompact, SmartString};
2
2
3/// Given a slice of text, return the text re-wrapped to fit it
3/// Given a slice of text, return the text re-wrapped to fit it
4/// within the given width.
4/// within the given width.
5−pub fn reflow_hard_wrap(text: &str, max_line_len: usize) -> SmartString<LazyCompact> {
5+pub fn reflow_hard_wrap(text: &str, text_width: usize) -> SmartString<LazyCompact> {
6− textwrap::refill(text, max_line_len).into()
6+ textwrap::refill(text, text_width).into()
7}
7}
helix-term/src/commands/typed.rs+8−12
@@ -1840,30 +1840,26 @@ fn reflow(
1840 }
1840 }
1841
1841
1842 let scrolloff = cx.editor.config().scrolloff;
1842 let scrolloff = cx.editor.config().scrolloff;
1843+ let cfg_text_width: usize = cx.editor.config().text_width;
1843 let (view, doc) = current!(cx.editor);
1844 let (view, doc) = current!(cx.editor);
1844
1845
1845− const DEFAULT_MAX_LEN: usize = 79;
1846−
1847− // Find the max line length by checking the following sources in order:
1846+ // Find the text_width by checking the following sources in order:
1848 // - The passed argument in `args`
1847 // - The passed argument in `args`
1849− // - The configured max_line_len for this language in languages.toml
1848+ // - The configured text-width for this language in languages.toml
1850− // - The const default we set above
1849+ // - The configured text-width in the config.toml
1851− let max_line_len: usize = args
1850+ let text_width: usize = args
1852 .get(0)
1851 .get(0)
1853 .map(|num| num.parse::<usize>())
1852 .map(|num| num.parse::<usize>())
1854 .transpose()?
1853 .transpose()?
1855− .or_else(|| {
1856− doc.language_config()
1854+ .or_else(|| doc.language_config().and_then(|config| config.text_width))
1857− .and_then(|config| config.max_line_length)
1855+ .unwrap_or(cfg_text_width);
1858− })
1859− .unwrap_or(DEFAULT_MAX_LEN);
1860
1856
1861 let rope = doc.text();
1857 let rope = doc.text();
1862
1858
1863 let selection = doc.selection(view.id);
1859 let selection = doc.selection(view.id);
1864 let transaction = Transaction::change_by_selection(rope, selection, |range| {
1860 let transaction = Transaction::change_by_selection(rope, selection, |range| {
1865 let fragment = range.fragment(rope.slice(..));
1861 let fragment = range.fragment(rope.slice(..));
1866− let reflowed_text = helix_core::wrap::reflow_hard_wrap(&fragment, max_line_len);
1862+ let reflowed_text = helix_core::wrap::reflow_hard_wrap(&fragment, text_width);
1867
1863
1868 (range.from(), range.to(), Some(reflowed_text))
1864 (range.from(), range.to(), Some(reflowed_text))
1869 });
1865 });
helix-view/src/document.rs+46−9
@@ -1238,24 +1238,61 @@ impl Document {
1238 }
1238 }
1239
1239
1240 pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> TextFormat {
1240 pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> TextFormat {
1241− if let Some(max_line_len) = self
1241+ let config = self.config.load();
1242+ let text_width = self
1242 .language_config()
1243 .language_config()
1243− .and_then(|config| config.max_line_length)
1244+ .and_then(|config| config.text_width)
1244− {
1245+ .unwrap_or(config.text_width);
1246+ let soft_wrap_at_text_width = self
1247+ .language_config()
1248+ .and_then(|config| {
1249+ config
1250+ .soft_wrap
1251+ .as_ref()
1252+ .and_then(|soft_wrap| soft_wrap.wrap_at_text_width)
1253+ })
1254+ .or(config.soft_wrap.wrap_at_text_width)
1255+ .unwrap_or(false);
1256+ if soft_wrap_at_text_width {
1257+ // We increase max_line_len by 1 because softwrap considers the newline character
1258+ // as part of the line length while the "typical" expectation is that this is not the case.
1259+ // In particular other commands like :reflow do not count the line terminator.
1260+ // This is technically inconsistent for the last line as that line never has a line terminator
1261+ // but having the last visual line exceed the width by 1 seems like a rare edge case.
1245− viewport_width = viewport_width.min(max_line_len as u16)
1262+ viewport_width = viewport_width.min(text_width as u16 + 1)
1246 }
1263 }
1247 let config = self.config.load();
1264 let config = self.config.load();
1248− let soft_wrap = &config.soft_wrap;
1265+ let editor_soft_wrap = &config.soft_wrap;
1266+ let language_soft_wrap = self
1267+ .language
1268+ .as_ref()
1269+ .and_then(|config| config.soft_wrap.as_ref());
1270+ let enable_soft_wrap = language_soft_wrap
1271+ .and_then(|soft_wrap| soft_wrap.enable)
1272+ .or(editor_soft_wrap.enable)
1273+ .unwrap_or(false);
1274+ let max_wrap = language_soft_wrap
1275+ .and_then(|soft_wrap| soft_wrap.max_wrap)
1276+ .or(config.soft_wrap.max_wrap)
1277+ .unwrap_or(20);
1278+ let max_indent_retain = language_soft_wrap
1279+ .and_then(|soft_wrap| soft_wrap.max_indent_retain)
1280+ .or(editor_soft_wrap.max_indent_retain)
1281+ .unwrap_or(40);
1282+ let wrap_indicator = language_soft_wrap
1283+ .and_then(|soft_wrap| soft_wrap.wrap_indicator.clone())
1284+ .or_else(|| config.soft_wrap.wrap_indicator.clone())
1285+ .unwrap_or_else(|| "↪ ".into());
1249 let tab_width = self.tab_width() as u16;
1286 let tab_width = self.tab_width() as u16;
1250 TextFormat {
1287 TextFormat {
1251− soft_wrap: soft_wrap.enable && viewport_width > 10,
1288+ soft_wrap: enable_soft_wrap && viewport_width > 10,
1252 tab_width,
1289 tab_width,
1253− max_wrap: soft_wrap.max_wrap.min(viewport_width / 4),
1290+ max_wrap: max_wrap.min(viewport_width / 4),
1254− max_indent_retain: soft_wrap.max_indent_retain.min(viewport_width * 2 / 5),
1291+ max_indent_retain: max_indent_retain.min(viewport_width * 2 / 5),
1255 // avoid spinning forever when the window manager
1292 // avoid spinning forever when the window manager
1256 // sets the size to something tiny
1293 // sets the size to something tiny
1257 viewport_width,
1294 viewport_width,
1258− wrap_indicator: soft_wrap.wrap_indicator.clone().into_boxed_str(),
1295+ wrap_indicator: wrap_indicator.into_boxed_str(),
1259 wrap_indicator_highlight: theme
1296 wrap_indicator_highlight: theme
1260 .and_then(|theme| theme.find_scope_index("ui.virtual.wrap"))
1297 .and_then(|theme| theme.find_scope_index("ui.virtual.wrap"))
1261 .map(Highlight),
1298 .map(Highlight),
helix-view/src/editor.rs+4−38
@@ -42,7 +42,7 @@ pub use helix_core::diagnostic::Severity;
42pub use helix_core::register::Registers;
42pub use helix_core::register::Registers;
43use helix_core::{
43use helix_core::{
44 auto_pairs::AutoPairs,
44 auto_pairs::AutoPairs,
45− syntax::{self, AutoPairConfig},
45+ syntax::{self, AutoPairConfig, SoftWrap},
46 Change,
46 Change,
47};
47};
48use helix_core::{Position, Selection};
48use helix_core::{Position, Selection};
@@ -241,6 +241,8 @@ pub struct Config {
241 pub auto_format: bool,
241 pub auto_format: bool,
242 /// Automatic save on focus lost. Defaults to false.
242 /// Automatic save on focus lost. Defaults to false.
243 pub auto_save: bool,
243 pub auto_save: bool,
244+ /// Set a global text_width
245+ pub text_width: usize,
244 /// Time in milliseconds since last keypress before idle timers trigger.
246 /// Time in milliseconds since last keypress before idle timers trigger.
245 /// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
247 /// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
246 #[serde(
248 #[serde(
@@ -276,43 +278,6 @@ pub struct Config {
276 pub soft_wrap: SoftWrap,
278 pub soft_wrap: SoftWrap,
277}
279}
278
280
279−#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280−#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
281−pub struct SoftWrap {
282− /// Soft wrap lines that exceed viewport width. Default to off
283− pub enable: bool,
284− /// Maximum space left free at the end of the line.
285− /// This space is used to wrap text at word boundaries. If that is not possible within this limit
286− /// the word is simply split at the end of the line.
287− ///
288− /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
289− ///
290− /// Default to 20
291− pub max_wrap: u16,
292− /// Maximum number of indentation that can be carried over from the previous line when softwrapping.
293− /// If a line is indented further then this limit it is rendered at the start of the viewport instead.
294− ///
295− /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
296− ///
297− /// Default to 40
298− pub max_indent_retain: u16,
299− /// Indicator placed at the beginning of softwrapped lines
300− ///
301− /// Defaults to ↪
302− pub wrap_indicator: String,
303−}
304−
305−impl Default for SoftWrap {
306− fn default() -> Self {
307− SoftWrap {
308− enable: false,
309− max_wrap: 20,
310− max_indent_retain: 40,
311− wrap_indicator: "↪ ".into(),
312− }
313− }
314−}
315−
316#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
281#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
282#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
318pub struct TerminalConfig {
283pub struct TerminalConfig {
@@ -772,6 +737,7 @@ impl Default for Config {
772 indent_guides: IndentGuidesConfig::default(),
737 indent_guides: IndentGuidesConfig::default(),
773 color_modes: false,
738 color_modes: false,
774 soft_wrap: SoftWrap::default(),
739 soft_wrap: SoftWrap::default(),
740+ text_width: 80,
775 }
741 }
776 }
742 }
777}
743}
languages.toml+1−1
@@ -1104,7 +1104,7 @@ file-types = ["COMMIT_EDITMSG"]
1104comment-token = "#"
1104comment-token = "#"
1105indent = { tab-width = 2, unit = " " }
1105indent = { tab-width = 2, unit = " " }
1106rulers = [50, 72]
1106rulers = [50, 72]
1107−max-line-length = 72
1107+text-width = 72
1108
1108
1109[[grammar]]
1109[[grammar]]
1110name = "git-commit"
1110name = "git-commit"