-
Notifications
You must be signed in to change notification settings - Fork 106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Text Reflowing/Justification #220
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just reviewed it to help @jmacdonald a bit.
I made some small comments on things that can be improved, nothing major though.
One overall comment as well: It would be nice if you could squash and regroup all the commits into more clean bits. Maybe like this:
- First one adding the comments in
src/view/mod.rs
andsrc/models/application/mod.rs
. - Next one splitting out
range_from()
fromdelete()
insrc/commands/selection.rs
. - After that, one adding the
justify
command itself + test. - Finally, one for adding the command to the default keymap.
But I can't quite figure out how exactly is supposed to work. More tests should be a must, testing all the possible combinations this command can be used in.
As example, if I select the full line, it works as it supposed to (other than inserting extra newlines at the end, that shouldn't happen). But if I select nothing (or just a few words), it completely messes up. My suggestion would be to either:
- Only allow
justify()
to be called inselect_line
mode or - Always use the full line range in every allowed mode.
pub fn new(preferences: Rc<RefCell<Preferences>>, event_channel: Sender<Event>) -> Result<View> { | ||
let terminal = build_terminal().chain_err(|| "Failed to initialize terminal")?; | ||
/// Return a new view. This will initialize the terminal, load the theme, | ||
/// and begin to listen for events. | ||
pub fn new( | ||
preferences: Rc<RefCell<Preferences>>, | ||
event_channel: Sender<Event> | ||
) -> Result<View> { | ||
let terminal = build_terminal() | ||
.chain_err(|| "Failed to initialize terminal")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, completely unrelated change?
Not needed IMHO since this line just about tickles the 100 character barrier, which is reasonable to default to for todays standards.
The comment again is nice and can stay, but the reformatting is somewhat unneeded. Especially since there are more occurences of this sort in this file and elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah sorry, I was playing with the codebase a bit trying to understand it, and I think I accidentally commited this instead of starting from the original codebase when I tried to implement line wrapping.
let preferences = initialize_preferences(); | ||
let preferences = | ||
Rc::new(RefCell::new( | ||
Preferences::load().unwrap_or_else(|_| Preferences::new(None)), | ||
)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should stay as-is. This does not provide any benefits and not having all things crammed into one function is very good for readability and maintainability.
The comment is all well and can stay of course, such things are always nice to have.
@@ -226,6 +226,7 @@ select: | |||
ctrl-a: selection::select_all | |||
ctrl-z: application::suspend | |||
ctrl-c: application::exit | |||
a: selection::justify |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably should be added to the select_line
mode as well, would be useful to have there too.
src/commands/selection.rs
Outdated
/// around it. | ||
fn justify_string(text: &String, max_len: usize, potential_prefix: Regex) -> String { | ||
let mut justified = String::new(); | ||
for paragraph in text.split("\n\n") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why split only on double newlines? A comment explaining how this works in general would be nice.
src/commands/selection.rs
Outdated
justified.push(' '); | ||
} | ||
|
||
justified += "\n\n"; // add the paragraph delim |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why add some additional newlines here? When justifying a paragraph I wouldn't expect it to suddenly add superfluous lines after the paragraph.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whitespace collapsing removes interprets paragraph breaks (\n\n) the same as line breaks and word breaks, so paragraph breaks would be removed without it. However, I can see how that would be very sub-optimal on single paragraphs. I suppose the solution to that is to only add the paragraph breaks if the current paragraph isn't the final. I'll ammend this so that it only runs if the current paragraph is not the last, and add relevant tests.
Thank you for the thorough review, and sorry my code is such a mess!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I understand! Yeah, that makes sense!
Don't worry, that's what review is for. Thanks for doing the work, it's something very useful to have! 👍
Co-authored-by: Christoph Heiss <[email protected]>
…ex for recognizing comments Co-authored-by: Christoph Heiss <[email protected]>
Co-authored-by: Christoph Heiss <[email protected]>
…d of String::new() Co-authored-by: Christoph Heiss <[email protected]>
Co-authored-by: Christoph Heiss <[email protected]>
Co-authored-by: Christoph Heiss <[email protected]>
if app.workspace.current_buffer().is_none() { | ||
bail!(BUFFER_MISSING); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that the buffer
declaration is only used in a single branch of the match
statement, let's remove this guard clause and inline the existence check; see below for the suggestion on that.
if app.workspace.current_buffer().is_none() { | |
bail!(BUFFER_MISSING); | |
} |
match app.mode { | ||
Mode::Select(_) | Mode::SelectLine(_) | Mode::Search(_) => { | ||
let delete_range = range_from(app)?; | ||
let buffer = app.workspace.current_buffer().unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let buffer = app.workspace.current_buffer().unwrap(); | |
let buffer = app.workspace.current_buffer().ok_or(BUFFER_MISSING)?; |
buffer.insert( | ||
&justify_string( | ||
&text, | ||
app.preferences.borrow().line_length_guide().unwrap_or(80), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already specify a default value in the preferences file itself. If someone has explicitly disabled the line length guide, let's raise an error here:
app.preferences.borrow().line_length_guide().unwrap_or(80), | |
app.preferences.borrow().line_length_guide().ok_or("Cannot justify without line length guide") |
/// Wrap a string at a given maximum length (generally 80 characters). If the | ||
/// line begins with a comment (matches potential_prefix), the text is wrapped | ||
/// around it. | ||
fn justify_string(text: &str, max_len: usize, potential_prefix: &str) -> String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes more sense to move the default value handling for a prefix into this function, and the Option
type makes the "potential" aspect of this argument self-describing.
fn justify_string(text: &str, max_len: usize, potential_prefix: &str) -> String { | |
fn justify_string(text: &str, max_len: usize, prefix: Option<&str>) -> String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm in agreement with @christoph-heiss's feedback, and I've added some of my own as well. That said, this is a feature I didn't know I wanted until this PR; I'd love to get this merged if you have the bandwidth to revisit these suggestions! 😁
Co-authored-by: Jordan MacDonald <[email protected]>
Yes - thanks so much for the feedback! I'm looking back at the code I wrote a few months ago, and just started to rewrite it because, as I'm sure you've noticed, it's pretty bad. I've got a justify function that works properly on strings, but I'm running into some issues when I try to get the contents of a selection. I wrote a fn sel_to_range(app: &mut Application) -> std::result::Result<Range, Error> {
let buf = match app.workspace.current_buffer() {
Some(b) => b,
None => bail!(BUFFER_MISSING),
};
match app.mode {
Mode::Select(ref sel) => Ok(Range::new(buf.cursor.position, sel.anchor)),
Mode::SelectLine(ref sel) => Ok(sel.to_range(&*buf.cursor)),
_ => bail!("Can't access a selection outside of select mode.")
}
} Then, when I call that with the below, there's a panic due to let sel = sel_to_range(app)?;
let buf = app.workspace.current_buffer().unwrap(); // above call guaruntees existence of buffer
let txt = buf.read(&sel).unwrap(); If it's not too much trouble, do you think you'd be able to tell me what's going on here? I'd assume that In addition, I'm not quite sure how this should get at prefixes. There are two types of prefixes I'm identifying:
I believe that supporting just the first would require some modifications to the configuration API, as the Supporting the second would be tricky too, as the pattern parameter would have to have state. In a vacuum, I'd go about that by defining a trait Prefix requiring |
That logic mirrors what's used for the
What if we opted for a simple heuristic to start? Something like "if there is a leading symbol/non-alphanumeric character, use that as the prefix". That won't cover all use cases, but if it gets us 90% without needing to use lexical scoping rules and syntax definitions to determine the comment prefix, I think that could be sufficient. That also wouldn't support variable prefixes, but FWIW, I don't use those often enough to see that as a major shortcoming, personally. |
That logic mirrors what's used for the `commands::selection::delete`
method, which works fine.
Not sure what I was doing incorrectly, but I extracted the code from
delete and all the tests pass.
What if we opted for a simple heuristic to start? Something like "if
there is a leading symbol/non-alphanumeric character, use that as the
prefix". That won't cover all use cases, but if it gets us 90% without
needing to use lexical scoping rules and syntax definitions to
determine the comment prefix, I think that could be sufficient.
Yep, that seems like a good solution. I can't really think of any
alphanumeric prefixes that would break this, so I'll start implementing
that!
That also wouldn't support variable prefixes, but FWIW, I don't use
those often enough to see that as a major shortcoming, personally.
I personally use `/* * */` style comments pretty frequently, and would
want to explicitly support them. I agree that the closure solution is
*probably* an over-complication, though. What would you think of a
hard-coded dictionary of `first line prefix: (middle line prefix, last
line prefix)` to handle these things?
For example, to support some of the more common comment styles, we could
have:
```txt
{
"/*": (" *", "\n */"), // c-style multiline
r#"""""#: ("", r#"""""#), // python-style multiline
"#+BEGIN_COMMENT": ("", "\n#+END_COMMENT"), // org-mode multiline
...
}
```
I'd imagine it could be pretty small (and thus hard-coded), and, if it's
ever too small, woud be able to be refactored into the main config file
without too much effort. How's this?
***
The code I've got going for this version is available on
[gitlab](https://gitlab.com/lincolnauster/amp) just so that I don't
overwrite the previous fork yet.
…--
lincoln auster [they/them]
|
I think I'd prefer to table multi-line comment support for now. I could only find one reflow plug-in for Sublime Text, and it too doesn't support multi-line comments. The complexity that this functionality would add to Amp wouldn't be worth it, in my opinion. We'd be forced to add another per-type configuration with a complex syntax that users are unlikely to configure (or that we are forced to maintain). Working with multi-line comments is still possible, provided the opening and closing symbols are put on dedicated lines (making the comment content appear without prefixes). In my experience, that's what most best practices encourage, anyway. It does mean that single-line comments in HTML and CSS would need to be manually converted to multi-line beforebeing able to be reflowed, but I think it's okay if the implementation doesn't cover that case. |
Okay, sounds good! The code on gitlab is feature complete. I'll move that over to github, close this PR, and open one for that. |
This fork introduces 'justification' functionality, where a line may be reformatted from
to
The command will justify around comments, for instance
to
Line length is determined by the line_length_guide property in the preferences, or a hard-coded value of 80 if the guide is not present. Characters that count as a comment (referred to as a prefix in the justify_text function) are determined by a hard-coded regex, as the
line_comment_prefix
option is limited to only one style (and thus would break on, for instance, rustdoc comments).This addresses #219.